DEV Community

myougaTheAxo
myougaTheAxo

Posted on

E2E Testing with Claude Code: Playwright Test Design and Page Object Model

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

Summary

Design E2E tests with Claude Code:

  1. CLAUDE.md — Selector priority, test data rules, prohibited patterns
  2. Playwright config — 3 browsers, parallel workers, CI-optimized
  3. User scenario tests — Describe behavior, not implementation
  4. Page Object Model — Hide selectors, enable reuse

Code Review Pack (¥980) includes /code-review for E2E test quality — fragile selectors, test interdependence, missing assertions.

👉 prompt-works.jp

Myouga (@myougatheaxo) — Claude Code engineer focused on test automation.

Top comments (0)