Playwright End-to-End Testing
Write reliable Playwright tests: page object model, network interception, visual testing, CI parallelization, and debugging flaky tests.
Playwright is the leading end-to-end testing framework - fast, reliable, and cross-browser. It supports Chromium, Firefox, and WebKit, with features like network interception, visual testing, and video capture. Writing maintainable Playwright tests requires understanding the page object model and flakiness root causes.
Write E2E tests for critical user flows only - login, checkout, onboarding. Unit-test everything else. Use page objects to avoid test duplication. Mock external APIs to make tests deterministic. Run in parallel in CI for speed.
Workflow
- Write tests with locators and auto-waitingPlaywright's auto-waiting and role-based locators eliminate most flakiness.
- Use the page object modelPage objects centralize selectors and interactions - test changes require updates in one place.
- Intercept network requestsNetwork interception makes tests deterministic - no flakiness from external APIs or slow endpoints.
- Parallelize in CIParallel Playwright runs cut E2E test suite time dramatically.
- Debug flaky testsFlaky tests erode confidence in the test suite - eliminating flakiness is as important as writing tests.
Output Format
Role/label locators, page object per page, `page.route` for API mocking, `playwright.config.ts` with CI worker config, traces on failure, `--shard` for parallel CI runs.
Examples
Full test: `test("checkout flow", async ({ page }) => { const cart = new CartPage(page); await cart.addItem("widget-1"); await cart.checkout(); const checkout = new CheckoutPage(page); await checkout.fillShipping({ address: "123 Main St", city: "Testville", zip: "12345" }); await checkout.payWith({ cardNumber: "4242424242424242" }); await expect(page.getByText("Order confirmed")).toBeVisible() })`API mock: `test.beforeEach(async ({ page }) => { await page.route("**/api/**", async route => { const res = await route.fetch(); const json = await res.json(); await route.fulfill({ json: { ...json, _mocked: true } }) }) })`Wait for request: `const [createReq] = await Promise.all([page.waitForRequest(req => req.url().includes("/api/users") && req.method() === "POST"), page.click("button", { text: "Save" })]); const body = createReq.postDataJSON(); expect(body.email).toBe("test@example.com")`Edge Cases & Gotchas
- Auth state across tests: use `storageState` to persist login cookies between tests. `setup.ts`: log in, save state. Tests: `use: { storageState: "playwright/.auth/user.json" }`. Faster than logging in per test
- iframes: `const frame = page.frameLocator("iframe[title=Payment]"); await frame.getByLabel("Card number").fill("4242...")` - locators chain into iframes
- Mobile testing: `devices["iPhone 12"]` preset - `use: { ...devices["iPhone 12"] }` sets viewport, touch, user agent for mobile browser emulation
When to Break These Rules
- For component-level interaction testing, Playwright component testing or Testing Library is lighter. Use full E2E Playwright for complete user flows, not isolated component behavior.
Reference Files
Playwright docs: https://playwright.dev/Best practices: https://playwright.dev/docs/best-practicesPage objects: https://playwright.dev/docs/pomCI guide: https://playwright.dev/docs/ci