Playwright E2E skill for Claude Code
End-to-end tests that never need a sleep. Claude writes Playwright tests the way the Playwright team recommends: role and label locators, auto-retrying assertions, API-seeded fixtures with teardown, and one login reused through storageState. When a test fails it reads the trace before touching code, and it ships CI config that shards runs, merges blob reports, and keeps traces for every failure.
- name
playwright-e2e - size220 lines · ≈2,100 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds and stabilizes Playwright end-to-end suites with role-based locators, web-first assertions, custom fixtures, reusable auth state, network mocking, traces, and sharded CI runs. Covers playwright.config.ts setup, page objects vs. fixtures, debugging failures from trace files, and keeping E2E tests fast and deterministic. Use when writing or fixing Playwright tests, when E2E tests are flaky or slow, when setting up login reuse, or when wiring Playwright into GitHub Actions or another CI. Try asking
- Write a Playwright test for our checkout flow using the saved login
- This Playwright test fails in CI one run in five. Here's the trace, fix it
- Set up Playwright in GitHub Actions with 4 shards and one HTML report
Use it when
- Writing or reviewing Playwright end-to-end tests
- E2E tests are slow, flaky, or log in through the UI every time
- Adding Playwright to CI with sharding and artifacts
- Debugging a failure from a Playwright trace file
Skip it when
- You need pixel-level screenshot comparisons: use visual-regression
- You are testing a single component's behavior: use testing-library
How to check the result
Run the new specs with --repeat-each=20 across several workers and get zero failures, then confirm CI uploads a merged HTML report with traces for any failing test.
The common mistake it prevents
Papering over a race with waitForTimeout or extra retries instead of waiting on the network response or visible state the test actually depends on.
A good E2E suite tests a few critical user journeys the way a user experiences them,
finds elements the way assistive technology does, never sleeps, and gives a trace for
every failure. If a test needs waitForTimeout to pass, it is broken.
Ground rules
- Locate by role and accessible name first. CSS/XPath selectors are a last resort.
- Assert with web-first assertions (
await expect(locator).toHaveText(...)); they retry until timeout. Never assert on a value you read once withtextContent(). - No
page.waitForTimeout()in committed tests. Wait for a condition instead. - Each test owns its data and can run alone, in any order, in parallel.
- Log in once per worker or per run via
storageState, not through the UI in every test. - Keep E2E for journeys. Push branch-level logic into unit and component tests.
Workflow
- [ ] 1. Check the existing config, Playwright version, and test layout
- [ ] 2. Pick the journeys worth an E2E test
- [ ] 3. Set up config: baseURL, webServer, projects, trace, retries
- [ ] 4. Add auth setup project and fixtures
- [ ] 5. Write tests with role locators and web-first assertions
- [ ] 6. Run locally in UI mode, then headless with repeats
- [ ] 7. Wire CI with sharding and trace artifacts
1. Inspect first
npx playwright --version
cat playwright.config.ts
ls tests e2e 2>/dev/null
npx playwright test --list | head -30
New project: npm init playwright@latest. Upgrade: bump @playwright/test, then
npx playwright install --with-deps so browser binaries match the package.
2. Choose journeys
E2E is expensive. Cover: sign-up/login, the primary revenue path (checkout, create project), one representative CRUD flow, and anything that broke in production before. Ten solid journeys beat two hundred brittle page-pokes.
3. Config baseline
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? '50%' : undefined,
reporter: process.env.CI ? [['blob'], ['github']] : [['html', { open: 'never' }]],
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
],
webServer: {
command: 'npm run build && npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
Add playwright/.auth/ to .gitignore. Add Firefox/WebKit projects only when the
product supports them and the CI budget allows; run them nightly if too slow per-PR.
4. Auth state and fixtures
// e2e/auth.setup.ts
import { test as setup, expect } 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_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
If tests mutate account-level state, give each worker its own account (worker-scoped
fixture keyed on test.info().parallelIndex) instead of sharing one user.
Prefer fixtures over page-object singletons; they compose and tear down automatically:
// e2e/fixtures.ts
import { test as base, expect } from '@playwright/test';
type Fixtures = { project: { id: string; name: string } };
export const test = base.extend<Fixtures>({
project: async ({ request }, use) => {
const res = await request.post('/api/projects', { data: { name: `e2e-${Date.now()}` } });
expect(res.ok()).toBeTruthy();
const project = await res.json();
await use(project);
await request.delete(`/api/projects/${project.id}`);
},
});
export { expect };
Seed data through the API (request fixture) rather than clicking through setup UI.
5. Locators and assertions
| Need | Use |
|---|---|
| Buttons, links, headings, checkboxes, rows | getByRole('button', { name: 'Save' }) |
| Form fields | getByLabel('Email') |
| Static text | getByText('No results', { exact: true }) |
| Elements with no accessible role/name | getByTestId('chart-canvas') (set testIdAttribute if not data-testid) |
| Scope within a region | page.getByRole('dialog').getByRole('button', { name: 'Confirm' }) |
| Pick a row | page.getByRole('row').filter({ hasText: 'Invoice 42' }) |
import { test, expect } from './fixtures';
test('renames a project', { tag: '@smoke' }, async ({ page, project }) => {
await page.goto(`/projects/${project.id}/settings`);
await page.getByLabel('Project name').fill('Renamed');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('status')).toHaveText('Saved');
await expect(page).toHaveTitle(/Renamed/);
});
- Strict mode: a locator matching several elements throws on action. Narrow it; do not
reach for
.first()unless “first” is the actual requirement. - Waiting for a backend call:
const resp = page.waitForResponse('**/api/save');before the click, thenawait resp. - Values that settle eventually but are not on a locator:
await expect.poll(() => getCount()).toBe(3). - Mock third-party or slow endpoints:
await page.route('**/api/recommendations', r => r.fulfill({ json: [] })); - Time-dependent UI: use
page.clock(await page.clock.install({ time: new Date('2026-01-01T09:00:00Z') })) instead of waiting for real time.
6. Run and debug
npx playwright test --ui # interactive watch + time travel
npx playwright test e2e/projects.spec.ts --headed --project=chromium
npx playwright test --debug -g "renames a project"
npx playwright test --repeat-each=20 --workers=4 -g "renames" # flake check
npx playwright test --last-failed
npx playwright codegen http://localhost:3000 # locator discovery, not final code
npx playwright show-trace test-results/**/trace.zip
npx playwright show-report
Debug from the trace before changing code: check the action log, the DOM snapshot at the failing step, the network tab, and console errors. Most “flaky” failures are a missing wait for a network response or a locator that matched a stale element.
7. CI
Shard across machines with the blob reporter and merge afterwards. A full GitHub Actions workflow with sharding, caching, and report merging is in references/ci-sharding.md; read it when setting up or fixing CI.
npx playwright test --shard=2/4
npx playwright merge-reports --reporter html ./all-blob-reports
Gotchas
retrieshides flakes. Treat any test marked “flaky” in the report as a bug; see flaky-test-triage.reuseExistingServer: truein CI tests whatever is on the port, possibly a stale build.storageStateexpires when the session does. If tests start failing at login mid-run, shorten the run or refresh the state in setup.page.gotowaits forloadby default; SPAs may still be fetching. Assert on visible content, not on navigation completion.- Hover menus and animations:
click()auto-waits for actionability, but CSS transitions can still move targets. Disable animations in test env if they cause flakes. - Do not share a
pageacross tests viabeforeAll; it breaks isolation and parallelism. - Version drift: the Docker image tag (
mcr.microsoft.com/playwright:v1.xx.x-noble) must match the installed@playwright/testversion.
Output
When delivering E2E work, report:
**Journeys covered:** login, checkout with saved card, project rename
**Files:** e2e/auth.setup.ts, e2e/fixtures.ts, e2e/checkout.spec.ts
**Stability:** `--repeat-each=20 --workers=4` → 60/60 passed
**CI:** 4 shards, blob reports merged, traces uploaded on failure
**Not covered by E2E (pushed down):** coupon validation rules → unit tests Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/ci-sharding.md
# Playwright in CI: sharding, reports, artifacts
Read this when setting up or repairing Playwright in CI.
## GitHub Actions with 4 shards and a merged report
```yaml
name: e2e
on:
pull_request:
push:
branches: [main]
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
E2E_USER: ${{ secrets.E2E_USER }}
E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 7
merge-reports:
if: ${{ !cancelled() }}
needs: [test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- run: npx playwright merge-reports --reporter html ./all-blob-reports
- uses: actions/upload-artifact@v4
with:
name: html-report
path: playwright-report
retention-days: 14
```
Notes:
- `fail-fast: false` so one failing shard does not cancel the others' evidence.
- Install only the browsers the projects use (`chromium` above) to save a minute per job.
- Pin action versions to the current major used elsewhere in the repo.
- Traces are inside the blob report; the merged HTML report links them.
## Using the official container instead of install
```yaml
container:
image: mcr.microsoft.com/playwright:v1.56.0-noble
options: --user 1001
```
The image tag must equal the `@playwright/test` version in `package-lock.json`. Check
with `npm ls @playwright/test` and update both together. The version above is an
example; use the one the project installs.
## Choosing the shard count
- Measure total serial test time from one run (`npx playwright test --reporter=list`).
- Target 5–10 minutes wall-clock per shard, including ~1–2 minutes of setup.
- Sharding splits by test file when `fullyParallel` is false; enable `fullyParallel: true`
so shards balance by test instead of by file.
## Running only what changed
- `npx playwright test --only-changed=origin/main` runs test files changed relative to a
git ref (and files importing changed modules within the test tree). Use for a fast
pre-check, never as the only gate on main.
- Tag smoke tests (`{ tag: '@smoke' }`) and run `--grep @smoke` on every PR, full suite on
merge queue or main.
## Environment hygiene
- Point `BASE_URL` at a per-PR preview deployment when one exists; otherwise build and
serve via `webServer`.
- Seed a dedicated test tenant/account; never run E2E against production data.
- Store credentials in CI secrets; the setup project reads them from `process.env`.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.