Make Playwright yours
Basic configuration options for your Playwright setup.
Up to here you've been running Playwright with whatever npm init playwright handed you. That works, but playwright.config.ts is where the test runner becomes yours — shorter goto calls, the right viewport, fewer flakes, sane timeouts, an auto-started dev server.
We won't cover everything in this file (the full reference is huge). The goal here is to walk through the options you'll actually touch in your first few weeks.
The shape of the file
Open playwright.config.ts at the root of your project. The whole thing is one call to defineConfig:
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: "https://next-example-store-stefan-judis.vercel.app",
trace: "on-first-retry",
},
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
});
Two things to notice:
- The top-level keys (
testDir,retries,workers, …) configure the runner. - The
useblock configures the browser context every test gets — viewport, locale, headless mode, what to record, the base URL.
Everything below is just a tour of those keys.
testDir — where your tests live
export default defineConfig({
testDir: "./tests",
});
By default Playwright walks ./tests looking for *.spec.ts files. Move your tests, change this, done. There's also testMatch / testIgnore if you need to be picky — we'll come back to those when we talk about projects.
use.baseURL — stop typing the host
This is the single most useful option in the file.
export default defineConfig({
use: {
baseURL: "https://next-example-store-stefan-judis.vercel.app",
},
});
With a baseURL set, every relative page.goto() and every relative URL in await expect(page).toHaveURL(...) resolves against it:
// before
await page.goto("https://next-example-store-stefan-judis.vercel.app/cart");
// after
await page.goto("/cart");
You also get to swap environments from the outside without touching a single test:
$ PLAYWRIGHT_BASE_URL=https://staging.example.com npx playwright test
…if you wire it up:
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "https://next-example-store-stefan-judis.vercel.app",
},
Browser defaults under use
The use block decides what kind of browser context every test starts with.
use: {
headless: false, // see the browser locally
viewport: { width: 1280, height: 720 },
locale: "en-US",
timezoneId: "Europe/Berlin",
colorScheme: "dark", // emulate prefers-color-scheme: dark
},
A few that come up often:
headless— setfalsewhile you're developing so you can watch the test. CI keeps ittrue.viewport— sets the window size. Want a mobile run? Spreaddevices["iPhone 13"]here.locale/timezoneId— important if your site formats dates, currencies, or numbers. The default is whatever your machine reports, which makes "works on my laptop" a real failure mode.colorScheme— flips the page into dark mode without a real OS setting.
Anything you set under top-level use can be overridden per-project (mobile
vs. desktop) or per-test (test.use({ viewport: ... })). We'll cover that
layering in a later lesson.
How long to wait — timeout & expect.timeout
There are two timeouts you'll hit early:
export default defineConfig({
timeout: 30_000, // each test gets 30s total
expect: {
timeout: 5_000, // each expect() gets 5s to become true
},
});
timeout— the budget for a whole test. IfbeforeEach+ the test body together take longer, the test fails.expect.timeout— how long any single web-first assertion (await expect(locator).toBeVisible()) keeps retrying before giving up.
Don't bump these globally to "fix" flakiness. Most of the time the test is right and the page is genuinely slow — the timeout is just the messenger.
Reliability — retries, workers, fullyParallel, forbidOnly
export default defineConfig({
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
});
fullyParallel: true— runs tests within the same file in parallel, not just files in parallel. Great for speed, brutal if your tests share state. Default since recent Playwright versions.forbidOnly: !!process.env.CI— fails the run if atest.only(...)slipped into a commit. Cheap insurance against accidentally landing a one-test PR.retries— retry failed tests N times. Pattern is0 locally, 2 in CIso you see flake during development but the pipeline doesn't go red on the first hiccup. If the second run passes, the test is marked flaky in the report — investigate it, don't ignore it.workers— how many browsers run in parallel. Locally Playwright picks a number; in CI you may want to cap it for stability or runner CPU budget.
Retries hide flakiness, they don't fix it. Treat any test that needs retries as a bug to investigate, not a test that "works now."
reporter — what you see when tests run
export default defineConfig({
reporter: "html",
});
Built-in reporters worth knowing:
"list"— one line per test, good for local runs."line"— single-line counter that overwrites itself; minimal noise."dot"— one character per test; great for very long suites."html"— the rich HTML report (npx playwright show-report). Default when you bootstrap."github"— emits GitHub Actions annotations on failures.
You can stack them:
reporter: [["list"], ["html", { open: "never" }], ["github"]],
A common combo: list so you see tests scroll by locally, html so you can dig in afterwards.
webServer — start your app for the tests
If your tests need your app running, you don't have to remember to npm run dev in another terminal. Let Playwright start it:
export default defineConfig({
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
- Playwright runs
command, then waits untilurlresponds before kicking off any test. reuseExistingServer: true(locally) means "if it's already running, just use it" — way faster while you iterate.- In CI, force a fresh start by leaving
reuseExistingServerfalse.
Hands-on — make the config yours
Tighten one thing
- Pick one option from this lesson that bugs you about your current setup (most common: no
baseURL). - Change it.
- Update the affected tests if needed (e.g. switch
page.goto("https://...")topage.goto("/...")). - Re-run the suite — should still be green.
Fail on stray `test.only`
- Add
forbidOnly: !!process.env.CIto your config. - Add a
test.only(...)to one of your spec files. - Run
CI=1 npx playwright test— Playwright should refuse to run. - Remove the
.onlyand the run is green again.