Playwright stability & real-user auth
Two jobs: (1) stop flakes, (2) authenticate like a real user without paying the
login cost per test. Both raise realism and reliability at once.
Anti-flaky checklist
- Locators by role first.
getByRole(name) → getByLabel → getByText → getByTestId. CSS/XPath last. Role locators double as an accessibility check.
- Web-first assertions only.
await expect(locator).toBeVisible() auto-waits. Never waitForTimeout, never assert on a snapshot you grabbed manually.
- No manual sleeps. Replace every fixed wait with an assertion on the state you actually need (
toHaveURL, toBeEnabled, toHaveText).
- Trace on first retry + screenshot on failure. Open the Trace Viewer before editing flaky code — DOM/network/console at each step finds the cause in minutes.
retries: 2 in CI, 0 locally (a local flake is a bug to fix, not retry).
- Isolate. Each test sets up its own data; no shared mutable state, no order dependency.
- One clean server. Kill stray dev servers and confirm the port is free before a full run — a stale server serves old assets and fakes failures.
Real-user auth via storageState (stop mocking the session)
Mocking auth is the least realistic part of a suite. Instead log in once,
save the browser state, and every test starts already authenticated — real
tokens, real session, near-zero per-test cost.
See templates/playwright/auth.setup.ts
and the config snippet beside it.
- Run the provider's real login (WorkOS / Cognito / Auth0 / your own form) in a
setup project, then context.storageState({ path }).
- Other projects depend on that setup and load
storageState.
- Keep the state file out of git (it holds live tokens) and regenerate it at
the start of each CI run — never commit and reuse across days.
- MFA / bot-protected SSO: do the login interactively once, persist the state,
refresh when it expires. Credentials come from env vars, never hardcoded.
Config (the bits that matter)
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: { trace: 'on-first-retry', screenshot: 'only-on-failure' },
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{ name: 'chromium', dependencies: ['setup'],
use: { storageState: 'playwright/.auth/user.json' } },
],
})
1---2name: playwright-stability3description: Make a Playwright E2E suite stable and realistic — kill flaky tests and authenticate like a real user via storageState (login once, reuse). Use when E2E tests are flaky, slow, re-login in every test, mock auth instead of using it, or when hardening a suite before relying on it.4---56# Playwright stability & real-user auth78Two jobs: (1) stop flakes, (2) authenticate like a real user without paying the9login cost per test. Both raise realism and reliability at once.1011## Anti-flaky checklist12131. **Locators by role first.** `getByRole(name)` → `getByLabel` → `getByText` → `getByTestId`. CSS/XPath last. Role locators double as an accessibility check.142. **Web-first assertions only.** `await expect(locator).toBeVisible()` auto-waits. Never `waitForTimeout`, never assert on a snapshot you grabbed manually.153. **No manual sleeps.** Replace every fixed wait with an assertion on the state you actually need (`toHaveURL`, `toBeEnabled`, `toHaveText`).164. **Trace on first retry + screenshot on failure.** Open the Trace Viewer before editing flaky code — DOM/network/console at each step finds the cause in minutes.175. **`retries: 2` in CI**, 0 locally (a local flake is a bug to fix, not retry).186. **Isolate.** Each test sets up its own data; no shared mutable state, no order dependency.197. **One clean server.** Kill stray dev servers and confirm the port is free before a full run — a stale server serves old assets and fakes failures.2021## Real-user auth via storageState (stop mocking the session)2223Mocking auth is the least realistic part of a suite. Instead log in **once**,24save the browser state, and every test starts already authenticated — real25tokens, real session, near-zero per-test cost.2627See [`templates/playwright/auth.setup.ts`](../../../../templates/playwright/auth.setup.ts)28and the config snippet beside it.2930- Run the provider's real login (WorkOS / Cognito / Auth0 / your own form) in a31 `setup` project, then `context.storageState({ path })`.32- Other projects depend on that setup and load `storageState`.33- **Keep the state file out of git** (it holds live tokens) and regenerate it at34 the start of each CI run — never commit and reuse across days.35- MFA / bot-protected SSO: do the login interactively once, persist the state,36 refresh when it expires. Credentials come from env vars, never hardcoded.3738## Config (the bits that matter)3940```ts41export default defineConfig({42 retries: process.env.CI ? 2 : 0,43 use: { trace: 'on-first-retry', screenshot: 'only-on-failure' },44 projects: [45 { name: 'setup', testMatch: /auth\.setup\.ts/ },46 { name: 'chromium', dependencies: ['setup'],47 use: { storageState: 'playwright/.auth/user.json' } },48 ],49})50```