Unit and integration tests pass, but the UI breaks in production. End-to-end tests with Playwright catch what other tests miss. Claude Code generates complete test scenarios when you define the right patterns.
CLAUDE.md for E2E Testing Standards
## E2E Testing Rules
### Tools
- Playwright (cross-browser, headless and headed)
- Test directory: e2e/ (separate from src/)
- Config: playwright.config.ts
### Test design principles
- Describe what the user does ("click the submit button")
- Don't depend on implementation details (no class names, no internal IDs)
- Selector priority: data-testid > role > text > CSS
### Test data management
- Reset test DB before each test run (beforeAll)
- Use fixtures/factories for test data (no hardcoding)
- Never use production data
### CI configuration
- Run E2E on every PR
- On failure: save screenshots and videos
- Run on Chromium, Firefox, and WebKit
- Target: under 5 minutes (use parallel workers)
### Prohibited
- sleep/waitForTimeout (use waitForSelector/waitForResponse)
- Sharing state between tests (tests must be independent)
- Requests to production API endpoints (use mocks)
Generating Playwright Config
Generate a Playwright configuration file.
Requirements:
- Browsers: Chromium, Firefox, WebKit
- Parallel execution with half the CPU cores as workers
- Start Next.js dev server before tests
- On failure: screenshot + video
- Timeouts: action 10s, test 60s
- Base URL: http://localhost:3000
Save to: playwright.config.ts
Generated:
// 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 ? 2 : undefined,
reporter: [['html', { open: 'never' }], ['list']],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10_000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Generating Login Flow E2E Tests
Generate E2E tests for the user login flow.
Scenarios:
1. Happy path: email + password → redirect to dashboard
2. Wrong password: show error message, stay on login page
3. Invalid email format: show form validation error
4. Password reset: "forgot password" flow
Selectors: use data-testid attributes
Post-login assertions: role-based
Save to: e2e/auth/login.spec.ts
Generated:
// e2e/auth/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('logs in with correct credentials', async ({ page }) => {
await page.getByTestId('email-input').fill('user@example.com');
await page.getByTestId('password-input').fill('correct-password');
await page.getByRole('button', { name: 'Log in' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
test('shows error on wrong password', async ({ page }) => {
await page.getByTestId('email-input').fill('user@example.com');
await page.getByTestId('password-input').fill('wrong-password');
await page.getByRole('button', { name: 'Log in' }).click();
await expect(page.getByTestId('error-message')).toHaveText(
'Invalid email or password'
);
await expect(page).toHaveURL('/login');
});
test('validates email format', async ({ page }) => {
await page.getByTestId('email-input').fill('not-an-email');
await page.getByTestId('password-input').fill('any-password');
await page.getByRole('button', { name: 'Log in' }).click();
await expect(page.getByTestId('email-error')).toBeVisible();
});
});
Page Object Model
Generate a Page Object Model for the login page.
Requirements:
- Hide selectors from test code
- Composition pattern (don't extend Page class)
- All interactions as methods
- Assertion methods included
Save to: e2e/pages/LoginPage.ts
Generated:
// e2e/pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByTestId('email-input');
this.passwordInput = page.getByTestId('password-input');
this.submitButton = page.getByRole('button', { name: 'Log in' });
this.errorMessage = page.getByTestId('error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async assertErrorMessage(message: string) {
await expect(this.errorMessage).toHaveText(message);
}
async assertRedirectedToDashboard() {
await expect(this.page).toHaveURL('/dashboard');
}
}
Using it in tests:
// e2e/auth/login-pom.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('logs in successfully', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'correct-password');
await loginPage.assertRedirectedToDashboard();
});
Summary
Design E2E tests with Claude Code:
- CLAUDE.md — Selector priority, test data rules, prohibited patterns
- Playwright config — 3 browsers, parallel workers, CI-optimized
- User scenario tests — Describe behavior, not implementation
- Page Object Model — Hide selectors, enable reuse
Code Review Pack (¥980) includes /code-review for E2E test quality — fragile selectors, test interdependence, missing assertions.
Myouga (@myougatheaxo) — Claude Code engineer focused on test automation.
Top comments (0)