Rule Writing

CLAUDE.md for Playwright Testing

AI generates Playwright tests with Cypress patterns. Rules for auto-waiting, locators, page objects, API mocking with route, and multi-browser testing.

7 min read·January 23, 2025

Playwright auto-waits on everything — AI adds explicit waits from Cypress habits

Semantic locators, auto-waiting, page objects, route() mocking, and multi-browser CI

Why Playwright Needs Rules Distinct from Cypress

Playwright and Cypress serve the same purpose — E2E browser testing — but their APIs are fundamentally different. Playwright uses async/await (not chaining), has built-in auto-waiting (no need for explicit waits or retries), uses locator-based queries (not DOM queries), and runs tests in parallel across multiple browsers by default. AI mixes Cypress patterns into Playwright: cy.get syntax, explicit waits, no parallel configuration, and Cypress-style custom commands instead of Playwright fixtures.

Playwright's biggest advantage is its locator system: getByRole, getByText, getByLabel, getByTestId — these are semantic, accessible, and resilient to DOM changes. AI generates page.$("css-selector") from Puppeteer training data instead of Playwright's recommended locators.

These rules target Playwright 1.40+ with TypeScript. They cover locators, auto-waiting, page object model, API mocking, fixtures, and multi-browser CI configuration.

Rule 1: Semantic Locators, Not CSS Selectors

The rule: 'Use Playwright's semantic locators: page.getByRole("button", { name: "Submit" }) for buttons, page.getByLabel("Email") for labeled inputs, page.getByText("Welcome back") for text content, page.getByTestId("sidebar") for test IDs. Never use page.$("css-selector") or page.locator(".class-name") unless no semantic locator fits. Semantic locators are accessible, resilient, and self-documenting.'

For priority order: '1. getByRole — most resilient, tests accessibility too. 2. getByLabel — for form inputs. 3. getByText — for visible text. 4. getByTestId — for elements without semantic meaning. 5. locator("css") — last resort only. Each step down is less resilient. getByRole("button") survives class renames, refactors, and component library changes.'

AI generates page.locator(".btn-submit") or page.$("#submit-button") from Puppeteer/Cypress training data. Playwright's getByRole is a fundamentally better approach — it queries the accessibility tree, not the DOM. If getByRole can't find the element, the element has an accessibility problem.

  • getByRole('button', { name: 'Submit' }) — most resilient, tests a11y
  • getByLabel('Email') — for labeled form inputs
  • getByText('Welcome') — for visible text content
  • getByTestId('sidebar') — for elements without semantic role
  • Never page.$('css') — last resort locator('css') only when semantic fails
💡 getByRole Tests A11y Too

getByRole('button', { name: 'Submit' }) queries the accessibility tree — if it can't find the element, the element has an accessibility problem. Semantic locators test two things at once: behavior AND accessibility.

Rule 2: Trust Auto-Waiting — No Explicit Waits

The rule: 'Playwright auto-waits on every action: click waits for the element to be visible, enabled, and stable. fill waits for the input to be editable. expect(...).toBeVisible() retries until the assertion passes or times out. Never use page.waitForTimeout(ms) — it's the same anti-pattern as cy.wait(ms). Never use page.waitForSelector unless you need to wait for an element before a non-action operation.'

For assertions: 'Use Playwright's web-first assertions: await expect(page.getByText("Success")).toBeVisible(). These auto-retry until the assertion passes (default 5s timeout). Never use node assertions (expect(await element.textContent()).toBe("Success")) — they don't retry and fail on timing issues.'

For navigation: 'Use await page.goto("/path") — Playwright waits for the load event. Use await page.waitForURL("**/dashboard") for client-side navigation. Use expect(page).toHaveURL(/dashboard/) for URL assertions. Never setTimeout or waitForTimeout for navigation timing.'

⚠️ Never waitForTimeout

page.waitForTimeout(3000) is Playwright's version of cy.wait(ms) — always wrong. Playwright auto-waits on click, fill, and expect. Web-first assertions (expect(locator).toBeVisible()) retry automatically. Trust the auto-wait.

Rule 3: Page Object Model

The rule: 'Use the Page Object Model for test organization: one class per page/feature that encapsulates locators and actions. class LoginPage { constructor(private page: Page) {} emailInput = () => this.page.getByLabel("Email"); passwordInput = () => this.page.getByLabel("Password"); submitButton = () => this.page.getByRole("button", { name: "Sign in" }); async login(email: string, password: string) { await this.emailInput().fill(email); await this.passwordInput().fill(password); await this.submitButton().click(); } }.'

For test usage: 'Instantiate in the test: const loginPage = new LoginPage(page); await loginPage.login("user@test.com", "password"). Page objects keep tests readable (loginPage.login vs 5 lines of locator + fill + click) and maintainable (if the login form changes, update one class, not 50 tests).'

AI generates inline locators in every test — duplicating selectors and actions across the entire test suite. Page objects extract duplication into typed, reusable classes. One selector change updates every test that uses the page object.

Rule 4: route() for API Mocking

The rule: 'Use page.route for API mocking: await page.route("**/api/users", route => route.fulfill({ json: [{ id: 1, name: "Alice" }] })). This intercepts network requests and returns mock data — tests don't depend on a real backend. Use route.fulfill for static responses, route.continue for pass-through with modification, route.abort for simulating network errors.'

For patterns: 'Mock in beforeEach for all tests in a file. Use different responses per test for scenario coverage: route.fulfill({ status: 401 }) for unauthorized, route.fulfill({ json: [] }) for empty state, route.abort() for network failure. Name routes for debugging: page.route("**/api/users", route => ..., { times: 1 }) for one-time mocking.'

For recording: 'Use HAR recording for complex API scenarios: await page.routeFromHAR("./tests/fixtures/api.har"). Record once, replay forever. This captures real API responses for deterministic test playback without manual fixture creation.'

  • page.route for API interception — fulfill, continue, or abort
  • Mock in beforeEach — different responses per test for scenarios
  • route.fulfill({ status: 401 }) for error testing — abort() for network failure
  • HAR recording: routeFromHAR for complex multi-request scenarios
  • Tests don't depend on backend — self-contained, CI-ready

Rule 5: Fixtures, Parallel Tests, and CI

The rule: 'Use Playwright fixtures for test setup: test.extend<{ loginPage: LoginPage }>({ loginPage: async ({ page }, use) => { const loginPage = new LoginPage(page); await use(loginPage); } }). Fixtures compose: combine page objects, auth state, and test data. Use storageState for authenticated test contexts — login once, reuse across tests.'

For parallelism: 'Playwright runs tests in parallel by default — each test gets its own browser context (isolated cookies, storage, state). Configure workers in playwright.config.ts: workers: process.env.CI ? 2 : undefined. Tests must be independent — no shared state between tests. Use test.describe.serial only when tests genuinely depend on order (rare).'

For CI: 'Configure multiple browsers: projects: [{ name: "chromium" }, { name: "firefox" }, { name: "webkit" }]. Use the Playwright GitHub Action: microsoft/playwright-github-action. Generate HTML reports: reporter: "html". Upload trace files on failure for debugging: use: { trace: "on-first-retry" }. Trace viewer shows every action, network request, and DOM snapshot.'

ℹ️ Trace Viewer = Debugging

trace: 'on-first-retry' captures every action, network request, and DOM snapshot on failure. Open with npx playwright show-trace trace.zip. It's like a time-travel debugger for your E2E tests — replay the exact sequence that failed.

Complete Playwright Rules Template

Consolidated rules for Playwright testing projects.

  • Semantic locators: getByRole > getByLabel > getByText > getByTestId > locator('css')
  • Auto-waiting: never waitForTimeout — web-first assertions retry automatically
  • Page Object Model: one class per page — locators + actions — typed, reusable
  • page.route for API mocking — fulfill, abort, continue — HAR for complex scenarios
  • Fixtures for setup: test.extend — storageState for auth — composable
  • Parallel by default — independent tests — no shared state
  • Multi-browser CI: chromium + firefox + webkit — trace on failure — HTML reporter
  • TypeScript — microsoft/playwright-github-action — codegen for recording tests