Testing6 min read

Playwright End-to-End Testing

Write reliable Playwright tests: page object model, network interception, visual testing, CI parallelization, and debugging flaky tests.

playwrighte2etestingautomation

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

  1. Write tests with locators and auto-waitingPlaywright's auto-waiting and role-based locators eliminate most flakiness.
  2. Use the page object modelPage objects centralize selectors and interactions - test changes require updates in one place.
  3. Intercept network requestsNetwork interception makes tests deterministic - no flakiness from external APIs or slow endpoints.
  4. Parallelize in CIParallel Playwright runs cut E2E test suite time dramatically.
  5. 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-practices
  • Page objects: https://playwright.dev/docs/pom
  • CI guide: https://playwright.dev/docs/ci

More skills in this library

Browse all technical skills - Mobile, AI/ML, Web, Backend, Design, and Product.