Playwright E2e Audit
Purpose
Run or create minimal Playwright checks for browser and local app flows. The scope is intentionally narrow: cover the 1 to 3 highest-value user journeys with stable locators and web-first assertions that auto-wait — not an exhaustive regression suite. Existing flaky suites are audited and repaired (broken selectors, blind waitForTimeout) rather than rewritten wholesale.
When to use
- A browser-based local app or web UI has no E2E coverage and the primary happy path has never been automatically verified.
- An existing Playwright suite is flaky, uses
waitForTimeout, or has broken selectors after a UI refactor — audit and repair them. - A feature touches a critical user flow (login, checkout, form submission) and a targeted E2E spec is needed to prevent regression before merge.
- The local dev server needs wiring into
playwright.config.tsviawebServerso the suite starts and waits for readiness automatically in CI.
When not to use
- The task is unrelated to testing and QA work.
- The work would require production deploys, destructive data actions, or secret disclosure.
- A narrower skill already covers the need:
52-smoke-test-and-repairfor the install/build/test gate chain,55-api-test-suite-reviewfor API-only contracts,56-visual-regression-reviewfor pixel diffs.
Procedure
- Detect existing setup. Look for
playwright.config.{ts,js},@playwright/testin devDependencies, and existingtests//e2e/specs. Reuse the project's config and conventions before adding anything. - Install minimally if absent and E2E is warranted:
npm i -D @playwright/test, thennpx playwright install --with-deps chromium(one browser unless cross-browser is explicitly required). - Pick the 1 to 3 highest-value flows: app loads, primary happy path, one critical action. Keep the suite minimal, not exhaustive.
- Wire the local app through the config
webServerblock so the run starts or reuses the server and waits for readiness. - Write specs with role/text/test-id locators and web-first assertions that auto-wait. Avoid arbitrary
waitForTimeout. - Run headless first; on failure re-run with
--trace on(or--debug) and open the trace to diagnose. - Report covered flows, pass/fail, and artifact paths (trace, screenshot, video).
Concrete checks
- Locators use
getByRole,getByLabel,getByText, orgetByTestId— not brittle CSS/XPath likediv > span:nth-child(3). - Assertions are web-first (
await expect(locator).toBeVisible()) so Playwright auto-waits and auto-retries; noexpect(await locator.count())snapshots of a racing DOM. - No
page.waitForTimeout(...)is used to "wait for things to settle"; wait on a condition instead. baseURLis set in config so specs usepage.goto('/'), not hardcodedhttp://localhost:3000/...repeated per file.webServer.urlmatchesbaseURL;reuseExistingServer: !process.env.CIso local runs reuse a running dev server but CI starts fresh.trace: 'on-first-retry'(or'on'while debugging) is set so a failure produces a viewable trace.- Test data uses fixtures or test accounts; no real credentials are embedded in specs.
- Each spec is independent — no spec depends on state left behind by a previous spec.
Commands or Templates
// tests/smoke.spec.ts — minimal, stable, web-first
import { test, expect } from '@playwright/test';
test('app loads and primary flow works', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
await page.getByRole('button', { name: 'New item' }).click();
await page.getByLabel('Title').fill('Hello');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Hello')).toBeVisible();
});
// playwright.config.ts — auto-start the local app
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry' },
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
npx playwright test # headless run
npx playwright test --ui # interactive UI mode
npx playwright test -g "primary flow" # filter by title
npx playwright test --headed --project=chromium
npx playwright test --trace on # force trace capture on every test
npx playwright show-trace trace.zip # open the trace viewer for a failure
npx playwright codegen http://localhost:3000 # record stable selectors
Flaky-test repair map
When auditing an existing flaky suite, match the symptom to the fix rather than adding retries blindly:
| Symptom | Root cause | Fix |
|---|---|---|
| Passes alone, fails in the suite | shared state between specs | isolate each spec; reset DB/storage in a fixture |
| Fails only in CI, passes locally | CI is slower; element not ready | replace waitForTimeout with await expect(...).toBeVisible() |
| Selector not found after a refactor | brittle CSS/XPath | switch to getByRole/getByTestId |
| Intermittent click misses | clicking before hydration | assert the element is enabled, then click |
| Login step times out randomly | re-authenticating per test | reuse a saved storageState (below) |
| Assertion races the DOM | count() snapshot read |
use toHaveCount, which auto-retries |
Auth-state reuse pattern
Logging in inside every spec is slow and a common flake source. Authenticate once in global setup, save the storage state, and reuse it:
// auth.setup.ts — runs once, persists the logged-in session
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.E2E_USER!);
await page.getByLabel('Password').fill(process.env.E2E_PASS!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
// in playwright.config.ts, a project that depends on setup and reuses the state
{ name: 'chromium', dependencies: ['setup'],
use: { storageState: 'playwright/.auth/user.json' } }
Credentials come from E2E_USER/E2E_PASS env vars (a test account), never hardcoded. The .auth/ directory must be gitignored — the saved state is a live session token.
Common issues & anti-patterns
waitForTimeoutas a fix. A fixed sleep masks a race; it is slow and still flaky. Wait on a visible element or network response instead.- CSS/XPath positional selectors.
nth-child, deep descendant chains, and generated class names break on any layout change. Prefer role/label/test-id. - Asserting on a snapshot count.
expect(await page.locator('.row').count()).toBe(3)reads once with no retry; the DOM may still be rendering. Useawait expect(page.locator('.row')).toHaveCount(3). - Hardcoded absolute URLs in every spec. Duplicates the base URL and breaks when the port changes. Set
baseURLand use relative paths. - Inter-dependent specs. Spec B assumes spec A created a record; running B alone or in parallel fails. Make each spec self-contained.
- Cross-browser by default. Installing and running 3 browsers triples runtime for a smoke suite. Start with chromium; add browsers only when required.
- Real credentials in the repo. Login specs with a real password leak secrets and break when the password rotates. Use a test account from env.
- No trace on failure. A red CI run with no artifact is hard to diagnose; enable
trace: 'on-first-retry'.
Required output
Return: setup state (existing vs added), flows covered, the run command, pass/fail per spec, and artifact paths for any failure (trace/screenshot/video). Recommend the smallest next fix or the next flow worth covering, and state explicitly if coverage was intentionally limited.
Safety
- Run against local/dev URLs only; never point E2E at production data or live payment flows.
- Use test accounts and fixtures, not real credentials; keep secrets in env, not in specs.
playwright installdownloads browser binaries — note it; do not run it without need.- Keep the suite deterministic; flag flaky waits instead of papering over them with blind sleeps.
- Do not commit traces, screenshots, or videos that may contain sensitive data.
Completion criteria
Done means the highest-value flows have runnable specs using stable locators and web-first assertions, the result is reported with artifacts for any failure, and the local app is wired through webServer so the suite starts cleanly.