JS Sketchbooksee JavaScript think ✏️
← back to phase 11

11.3 — The config, decoded

Every Playwright question that begins “why does my test…” ends in this file. So before locators, before actions, playwright.config.ts gets the 8.6 treatment: every line decoded, until nothing in it is mystery furniture.

You’ve done this twice (package.json in 8.6; the CI file comes in 11.16). Same detective method, richest file yet.

watch it happen
import { defineConfig } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  timeout: 30_000,
  expect: { timeout: 5_000 },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: "html",
  use: {
    baseURL: process.env.BASE_URL
             ?? "http://localhost:3000",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  },
});

The method is 8.6’s, verbatim: read every line, pin its job to the board, trust nothing you can’t explain. This file is the suite’s CONTRACT — the runner consults it before touching a single test, so understanding it means understanding every run you’ll ever start.

the config case board(step through — every line becomes a sticky)third case for the detective method: 8.6 read package.json, 11.16 will read theCI file — today, the config
under the hood

The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”

Where env values come from in practice: teams load a local .env file at the top of the config (import "dotenv/config" — 9.4’s layered sources), while CI injects the same names from its vault (11.16). The config reads process.env.* either way — one reading site, two suppliers.

Settings cascade: the config’s use{} is the base layer, a project (11.12) can override it, a file can (test.use({…})), and a single test can too. Most-specific wins — the same mental model as CSS specificity (7.x), applied to test settings.

Two more residents you’ll meet in real configs: projects (the cross-browser array — all of 11.12) and webServer (start the app before testing — also 11.12). And globalSetup/globalTeardown for run-once work — 11.11 uses the projects flavor of that idea for login.

Job note: when a suite behaves differently on CI than locally, diff the EFFECTIVE config first — nearly always it’s a process.env.CI ? … : … line doing exactly what it says (retries masking a flake, workers changing order). You now read those lines at a glance; that instinct alone will save you afternoons.

your turn

⌨️ the config resolver

The config asks the environment two questions (9.4’s plumbing). Build the resolver that answers them, and print the effective settings for a local run and a CI run.

requirements:

  • Write resolveRetries(env): 2 when env.CI is set, 0 otherwise (a ternary reads best).
  • Write resolveBaseURL(env): env.BASE_URL with the honest default "http://localhost:3000" (8.4’s operator, exactly as the real config uses it).
  • Print one line per world with a template literal: local: 0 retries → http://localhost:3000 for an empty env, and ci: 2 retries → https://staging.shop.com for { CI: "true", BASE_URL: "https://staging.shop.com" }.

when you press RUN, the console must show exactly:

local: 0 retries → http://localhost:3000
ci: 2 retries → https://staging.shop.com

✏️ Quick check 1

timeout: 30_000 and expect.timeout: 5_000 — which one is the budget for a SINGLE retrying assertion?

✏️ Quick check 2

retries is 2 on CI but 0 locally. Why zero at your desk? (short phrase)

✏️ Quick check 3

With baseURL set, what does page.goto("/") navigate to?

teach it back

🗣️ Now teach it back

Give the full config briefing: the two nested timeouts, why retries and workers read process.env.CI, what the use block is (and how baseURL gets aimed), and the artifacts trio’s policy logic.

Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.

a few sentences, minimum — you’ve got this
to remember
The config = the suite’s contract, read with 8.6’s method. Frame: export default + defineConfig (typing help). testDir aims find-by-convention.
TWO nested clocks: timeout 30s per TEST ⊃ expect.timeout 5s per ASSERTION. CI-conditionals everywhere (9.4 live): retries 2/0 (raw truth locally), workers 1/auto, forbidOnly guards .only.
use{} = defaults for every test — baseURL: env ?? localhost makes goto("/") target-aware. Artifacts trio = evidence POLICIES (trace on-first-retry, screenshot/video on failure) → test-results/ (9.5).
next: 11.4 Locators →