End-to-End Testing
Build reliable E2E test suites with Playwright - page object model, auto-waiting, network interception, CI integration, and flakiness prevention.
skill: End-to-End Testing category: Engineering tags: [E2E Testing, Playwright, Cypress, Browser Testing, Test Automation] readingTime: 5
E2E tests are the most expensive tests to write and maintain - they're slow, flaky, and require a running application. They provide the highest confidence: if an E2E test passes, a real user can accomplish that task. Cover critical paths (signup, purchase, core workflow) with E2E. Cover edge cases and units with cheaper tests.
Workflow
- Set up Playwright and structure testsPlaywright auto-waits for elements, supports multiple browsers, has excellent debugging tools, and runs in CI natively.
- Use the Page Object ModelPage Object Model (POM) encapsulates page interactions in reusable classes. When the UI changes, you update one place - not every test that touches that page.
- Use locator best practicesFragile selectors (CSS classes, XPath) break when developers refactor UI. Role-based and label-based selectors are resilient and also test accessibility.
- Mock network requestsE2E tests hitting real backends are slow and flaky (network latency, test data contamination). Mock APIs for deterministic, fast tests. Use real backends only for critical integration scenarios.
- Prevent flakiness and debug failuresFlaky tests (pass sometimes, fail sometimes) destroy trust in the test suite. Debugging tools (traces, screenshots, video) reduce mean time to fix from hours to minutes.
Output Format
E2E: Playwright config with projects + webServer, Page Object Model with role-based locators, network route mocking, waitForURL for navigation, and PWDEBUG inspector for flakiness debugging.
playwright.config: fullyParallel + retries:2 on CI + trace on-first-retry
LoginPage: getByLabel + getByRole + goto + login methods
page.route("**/api/orders", route.fulfill({ status:201, body: ... }))
page.waitForURL("**/dashboard") after navigation actions
PWDEBUG=1 for interactive debugging; npx playwright show-trace for failuresExamples
Edge Cases & Gotchas
- Authentication state sharing - Logging in for every test is slow. Use Playwright's storageState to save authentication cookies once per test suite, then reuse across tests: await page.context().storageState({ path: "auth.json" }).
- File upload testing - await page.setInputFiles("input[type=file]", "test-fixtures/document.pdf") triggers file input without OS dialog.
- iFrame content - Content inside iframes requires frame.locator() not page.locator(): const frame = page.frameLocator("iframe[name=payment]"); await frame.getByLabel("Card number").fill("4242...").
- Mobile viewport and touch events - Test mobile-specific behavior with devices["iPhone 14"] which sets viewport, userAgent, and emulates touch. Use page.tap() instead of page.click() for touch-specific interactions.
When to Break These Rules
- Use Cypress instead of Playwright if your team is React-heavy and already has Cypress setup - migrating doesn't justify the productivity gain difference between them.
- Use API-level integration tests instead of E2E for backend logic - E2E tests are 10× slower than API tests for the same coverage. E2E tests should verify the UI layer, not business logic.
Reference Files
https://playwright.dev/docs/introhttps://playwright.dev/docs/best-practices