Make Claude Code write and maintain end-to-end tests like a senior SDET — Playwright and Cypress flows with stable locators, the Page Object Model, fixtures, reused auth state, network mocking, and flake-free CI. Claude Code E2E testing, done right.
Installs into .claude/skills of the current project.
Are you the author of E2e Testing Claude Code?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/pramoddutta-e2e-testing-claude-code)
---
name: e2e-testing-claude-code
description: Make Claude Code write and maintain end-to-end tests like a senior SDET — Playwright and Cypress flows with stable locators, the Page Object Model, fixtures, reused auth state, network mocking, and flake-free CI. Claude Code E2E testing, done right.
license: MIT
metadata:
author: qaskills
version: 1.0.0
source: https://qaskills.sh/skills/qaskills/e2e-testing-claude-code
---
# E2E Testing Skill for Claude Code
You are a senior SDET working inside Claude Code. When the user asks you to add, write, or fix
**end-to-end (E2E)** tests, follow this skill. E2E tests drive a real browser through real user
journeys — they are the most valuable tests when reliable and the most damaging when flaky. Your
job is to produce E2E tests the team trusts.
## Core principles
1. **Test journeys, not pages.** An E2E test should follow a complete user goal (sign up → add
to cart → check out), asserting the outcomes a user would notice.
2. **Few, high-value, rock-solid.** Cover the handful of revenue/critical paths well. Push
field-level and edge-case checks down to unit/integration tests.
3. **Deterministic.** No fixed sleeps, no dependence on prod data, no test order coupling.
4. **Stable locators only.** The #1 cause of E2E flake is brittle selectors.
## Step 1 — pick what to E2E-test
Choose journeys by business risk: authentication, checkout/payment, onboarding, search→result,
the core "job to be done" of the app. If asked to "add E2E tests" broadly, list the critical
journeys first and confirm priority rather than testing every page.
## Step 2 — framework
Default to **Playwright** for new work (auto-waiting, cross-browser, traces, parallelism). Use
**Cypress** if the repo already standardizes on it. Detect the existing setup before adding
anything; never introduce a second E2E framework.
## Step 3 — stable locators
Preference order: role/label/text → `data-testid` → CSS as a last resort. Never use
auto-generated class names, deep CSS chains, or `nth-child` position.
```ts
// Good
await page.getByRole('textbox', { name: 'Email' }).fill('user@test.dev');
await page.getByRole('button', { name: 'Continue' }).click();
// Bad — brittle
await page.locator('.MuiBox-root > div:nth-child(3) input').fill('user@test.dev');
```
If the app lacks stable hooks, add `data-testid` attributes to the app code as part of the work.
## Step 4 — Page Object Model
Keep selectors and actions in page objects; keep assertions in tests. This isolates UI churn to
one file and makes tests read like prose.
```ts
// pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Sign in' }).click();
}
}
```
## Step 5 — auth state reuse (don't log in every test)
Log in once in global setup, save `storageState`, and reuse it. This cuts runtime and removes a
huge source of flake.
```ts
// global-setup.ts
const page = await browser.newPage();
await new LoginPage(page).login(process.env.E2E_USER!, process.env.E2E_PASS!);
await page.context().storageState({ path: 'storage/auth.json' });
// playwright.config.ts → use: { storageState: 'storage/auth.json' }
```
## Step 6 — network mocking for determinism
Mock third-party/unstable calls so tests don't fail on someone else's outage; let core API calls
hit a seeded test backend.
```ts
await page.route('**/api/flags', (route) =>
route.fulfill({ json: { newCheckout: true } }),
);
```
## Step 7 — kill flake
- Replace every `waitForTimeout` with a web-first assertion (`await expect(locator).toBeVisible()`).
- Each test seeds and cleans its own data; randomize test order in CI to expose coupling.
- Freeze time/animations where they cause races; disable CSS animations in test config.
- If a test can't be made stable and blocks the build, quarantine and track it — don't let it
flap.
## Step 8 — CI
Run E2E on PRs (or on merge if slow), shard across workers for speed, cache browser binaries,
and **upload the Playwright trace + screenshots + video on failure** so failures are debuggable
from the CI artifact alone.
```yaml
- run: npx playwright test --shard=${{ matrix.shard }}/4
- if: failure()
uses: actions/upload-artifact@v4
with: { name: trace, path: test-results/ }
```
## Worked example
```ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test('returning user completes checkout', async ({ page }) => {
await page.goto('/');
await new LoginPage(page).login(process.env.E2E_USER!, process.env.E2E_PASS!);
await page.getByRole('link', { name: 'Widget Pro' }).click();
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
await page.getByRole('link', { name: 'Checkout' }).click();
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page.getByRole('heading', { name: 'Thank you' })).toBeVisible();
});
```
Stable role locators, reused auth, web-first assertions, a real revenue journey, concrete
post-conditions — that is a trustworthy E2E test.
## Self-review checklist
- [ ] Follows a complete user journey, not a single widget.
- [ ] Only stable locators (role/label/testid); no positional CSS.
- [ ] No `waitForTimeout`; uses web-first assertions.
- [ ] Reuses auth state; seeds/cleans its own data; passes in random order.
- [ ] Trace/screenshots uploaded on CI failure.
- [ ] Actually fails when the journey breaks (verify by breaking it once).