Playwright Testing
A test should fail only when the app is broken - never because of timing, test order, or a CSS refactor. The costly failure mode this skill prevents is the suite the team stops trusting: once engineers rerun red builds by reflex, the suite catches nothing and its entire cost is waste.
Operating procedure
Step 1: gather inputs
Before writing a test, collect (label guesses as guesses):
- The user behavior under test, phrased as intent ("user can reset password"), not implementation.
- What is in scope: the backend too, or frontend only (decides whether to mock the network).
- How test state is created and destroyed (API seeding, DB reset, per-test user).
- Whether the app renders accessible roles/labels (decides locator strategy).
Step 2: choose locators in strict priority order
getByRole with accessible name - survives any markup refactor that keeps the UI accessible.
getByLabel / getByPlaceholder for form fields.
getByText for non-interactive content.
data-testid only when semantics genuinely cannot identify the element - and then add the attribute to the app rather than falling through to CSS.
- CSS/XPath tied to structure: never in new code.
Bad:
await page.locator('div.form-wrap > div:nth-child(3) button.btn-primary').click();
await page.waitForTimeout(3000);
expect(await page.locator('.msg').count()).toBe(1);
Breaks on any layout change, sleeps a fixed 3 s (too slow when the app is fast, flaky when it is slow), and asserts a non-retrying snapshot.
Good:
await page.getByRole('button', { name: 'Reset password' }).click();
await expect(page.getByRole('alert')).toHaveText(/check your email/i);
Locators encode user-visible semantics; the web-first assertion auto-retries until the alert renders or the timeout hits.
Step 3: eliminate timing from the test
- Never
waitForTimeout with a magic number - zero tolerance, it is the single largest flake source.
- Rely on auto-waiting actions and web-first assertions (
await expect(locator).toBeVisible()), which retry until true.
- Wait for the condition you care about:
page.waitForResponse for a request, expect(page).toHaveURL for navigation, a locator state for rendering.
- Keep the default expect timeout at 5 s and test timeout at 30 s. A test that needs more than 30 s is doing too much - split it.
Step 4: isolate state
- One behavior per test, arrange-act-assert.
- Seed and reset data so no test depends on execution order; each test must pass alone and under
--fully-parallel.
- Use fixtures for shared setup (auth via storage state, base URL) instead of repeating login flows - a UI login in every test adds seconds and a flake surface per test.
- Mock the network where the backend is out of scope; hit the real backend only in tests whose purpose is integration.
Step 5: configure CI for evidence, not retries-as-a-cure
retries: 2 in CI, 0 locally; trace: 'on-first-retry' so every flake produces a trace.
- Headed locally to debug, headless in CI.
- Run new or modified tests 10 times before merging (
--repeat-each=10); all 10 must pass.
Flake policy (red lines)
- A test that fails then passes on retry with no code change is flaky by definition - file it, do not shrug.
- Quarantine any test that flakes more than twice in 30 CI runs (>~1% flake rate); a quarantined test is skipped from merge-blocking runs and gets an owner and a deadline. Hand systematic diagnosis to flaky-test-detangler.
- Suite-level red line: if more than 2% of CI runs need a retry to go green, stop adding tests and fix stability first - trust erodes faster than coverage accrues.
Deliverable
Produce the test file(s) plus the config deltas: role-based locators throughout, zero fixed sleeps, fixtures for auth/setup, CI retry and trace settings, and a one-line intent-named title per test.
Do NOT
- Do not use
waitForTimeout - it is either wasted seconds or a flake, never correct.
- Do not chain assertions on
.count() or other non-retrying snapshots; use web-first expect matchers.
- Do not share mutable state between tests or depend on run order - parallelism will eventually expose it at the worst time.
- Do not name tests after implementation ("clicks #submit-btn") - name the user intent so failures read as broken behavior.
- Do not treat CI retries as the fix for flakiness; they are instrumentation for capturing traces, not a cure.
Quality bar
- Every locator is role/label/text-based or a justified
data-testid; zero structural CSS/XPath.
- Zero fixed sleeps anywhere in the diff.
- Each test passes in isolation and repeated 10 times.
- Failure output alone (title + assertion + trace) tells a reader what user behavior broke.
- CI config captures a trace on first retry.
1---2name: playwright-testing3description: Writes and reviews end-to-end Playwright tests that survive UI refactors and never fail on timing - resilient locators, web-first assertions, isolated state, and a flake policy with quarantine rules. Use when someone asks "write a Playwright test for this flow", "why is this e2e test flaky", "how do I wait for this element", "what selector should I use", or is setting up a Playwright suite and CI config. Do NOT use for consumer/provider contract tests of API boundaries - use contract-test-writer instead; for turning product requirements into an e2e scenario inventory before any code - use e2e-scenario-author instead; for systematically diagnosing an already-flaky suite - use flaky-test-detangler instead; for designing reusable test fixtures and factories - use test-data-builder instead.4---56# Playwright Testing78A test should fail only when the app is broken - never because of timing, test order, or a CSS refactor. The costly failure mode this skill prevents is the suite the team stops trusting: once engineers rerun red builds by reflex, the suite catches nothing and its entire cost is waste.910## Operating procedure1112### Step 1: gather inputs1314Before writing a test, collect (label guesses as guesses):15161. The user behavior under test, phrased as intent ("user can reset password"), not implementation.172. What is in scope: the backend too, or frontend only (decides whether to mock the network).183. How test state is created and destroyed (API seeding, DB reset, per-test user).194. Whether the app renders accessible roles/labels (decides locator strategy).2021### Step 2: choose locators in strict priority order22231. `getByRole` with accessible name - survives any markup refactor that keeps the UI accessible.242. `getByLabel` / `getByPlaceholder` for form fields.253. `getByText` for non-interactive content.264. `data-testid` only when semantics genuinely cannot identify the element - and then add the attribute to the app rather than falling through to CSS.275. CSS/XPath tied to structure: never in new code.2829Bad:3031```ts32await page.locator('div.form-wrap > div:nth-child(3) button.btn-primary').click();33await page.waitForTimeout(3000);34expect(await page.locator('.msg').count()).toBe(1);35```3637Breaks on any layout change, sleeps a fixed 3 s (too slow when the app is fast, flaky when it is slow), and asserts a non-retrying snapshot.3839Good:4041```ts42await page.getByRole('button', { name: 'Reset password' }).click();43await expect(page.getByRole('alert')).toHaveText(/check your email/i);44```4546Locators encode user-visible semantics; the web-first assertion auto-retries until the alert renders or the timeout hits.4748### Step 3: eliminate timing from the test4950- Never `waitForTimeout` with a magic number - zero tolerance, it is the single largest flake source.51- Rely on auto-waiting actions and web-first assertions (`await expect(locator).toBeVisible()`), which retry until true.52- Wait for the condition you care about: `page.waitForResponse` for a request, `expect(page).toHaveURL` for navigation, a locator state for rendering.53- Keep the default expect timeout at 5 s and test timeout at 30 s. A test that needs more than 30 s is doing too much - split it.5455### Step 4: isolate state5657- One behavior per test, arrange-act-assert.58- Seed and reset data so no test depends on execution order; each test must pass alone and under `--fully-parallel`.59- Use fixtures for shared setup (auth via storage state, base URL) instead of repeating login flows - a UI login in every test adds seconds and a flake surface per test.60- Mock the network where the backend is out of scope; hit the real backend only in tests whose purpose is integration.6162### Step 5: configure CI for evidence, not retries-as-a-cure6364- `retries: 2` in CI, `0` locally; `trace: 'on-first-retry'` so every flake produces a trace.65- Headed locally to debug, headless in CI.66- Run new or modified tests 10 times before merging (`--repeat-each=10`); all 10 must pass.6768## Flake policy (red lines)6970- A test that fails then passes on retry with no code change is flaky by definition - file it, do not shrug.71- Quarantine any test that flakes more than twice in 30 CI runs (>~1% flake rate); a quarantined test is skipped from merge-blocking runs and gets an owner and a deadline. Hand systematic diagnosis to flaky-test-detangler.72- Suite-level red line: if more than 2% of CI runs need a retry to go green, stop adding tests and fix stability first - trust erodes faster than coverage accrues.7374## Deliverable7576Produce the test file(s) plus the config deltas: role-based locators throughout, zero fixed sleeps, fixtures for auth/setup, CI retry and trace settings, and a one-line intent-named title per test.7778## Do NOT7980- Do not use `waitForTimeout` - it is either wasted seconds or a flake, never correct.81- Do not chain assertions on `.count()` or other non-retrying snapshots; use web-first `expect` matchers.82- Do not share mutable state between tests or depend on run order - parallelism will eventually expose it at the worst time.83- Do not name tests after implementation ("clicks #submit-btn") - name the user intent so failures read as broken behavior.84- Do not treat CI retries as the fix for flakiness; they are instrumentation for capturing traces, not a cure.8586## Quality bar8788- Every locator is role/label/text-based or a justified `data-testid`; zero structural CSS/XPath.89- Zero fixed sleeps anywhere in the diff.90- Each test passes in isolation and repeated 10 times.91- Failure output alone (title + assertion + trace) tells a reader what user behavior broke.92- CI config captures a trace on first retry.