E2E Testing React Apps with Playwright
Playwright setup, locators, assertions, mocking API, and CI integration..
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
- ✓Node.js 18+, npm or yarn, a React app (Vite or Create React App), basic knowledge of TypeScript, familiarity with testing concepts (assertions, mocks), and a GitHub account for CI examples.
E2E Testing React Apps with Playwright: A core React concept for building modern user interfaces. It helps you structure your components efficiently and handle data flow predictably.
React is a JavaScript library for building user interfaces. This article covers e2e playwright — a key concept for building modern web applications.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
A comprehensive guide to e2e testing react apps with playwright with production examples and best practices.
Why Playwright Over Cypress for React E2E
Playwright has become the de facto standard for end-to-end testing of React applications in production environments. Unlike Cypress, which runs inside the browser's JavaScript context and suffers from limitations like same-origin restrictions and lack of native mobile emulation, Playwright operates at the protocol level. This gives you true multi-browser support (Chromium, Firefox, WebKit), network interception, and the ability to test scenarios like file downloads, pop-ups, and multi-tab workflows. For React apps, Playwright's auto-waiting mechanism eliminates flaky waitFor calls by automatically retrying actions until elements are stable. In production, teams at Microsoft and Google use Playwright to run thousands of tests in parallel across browsers, catching regressions before they hit users. If you're building a React app that needs to work reliably across browsers, Playwright is the right choice.
Setting Up Playwright for a React App
Start by installing Playwright Test and configuring it for your React project. The playwright.config.ts file is where you define browser projects, base URL, and global settings. For a typical React app (Vite or Create React App), set the baseURL to your dev server (e.g., http://localhost:5173). Use webServer to automatically start your dev server before tests. Configure projects for Chromium, Firefox, and WebKit to ensure cross-browser coverage. In production, we also add a project for mobile Safari using device emulation. Set retries to 2 for CI to handle transient failures. Use fullyParallel to run tests in parallel across workers. This setup catches environment-specific bugs early.
reuseExistingServer: !process.env.CI speeds up local development by reusing the dev server, while CI always starts fresh.Writing Your First React E2E Test
A Playwright test for a React component typically involves navigating to a page, interacting with elements, and asserting expected outcomes. Use to navigate, page.goto() to find elements, and page.locator() for assertions. Playwright's auto-waiting means you don't need explicit waits for most actions. For React apps, prefer expect()getByRole and getByText over CSS selectors to align with accessibility best practices. Write tests that mirror user behavior: click buttons, fill forms, and verify UI updates. Avoid testing implementation details like component state directly; instead, test what the user sees. This makes tests resilient to refactors.
page.evaluate() to check React state. Instead, assert on visible DOM elements. This keeps tests decoupled from your component internals.Mocking API Calls with Playwright
In E2E tests, you often need to control backend responses to test edge cases. Playwright's allows you to intercept network requests and return mock data. This is essential for testing loading states, error handling, and empty states without relying on a real backend. Use page.route() to return a custom response, or route.fulfill() to simulate network failures. For React apps that use React Query or SWR, mocking at the network level ensures your data fetching logic is exercised. In production, we mock all external APIs in CI to make tests deterministic and fast.route.abort()
page.route() to mock API responses and test loading, error, and empty states reliably.Testing React Component Interactions
Playwright excels at testing complex user interactions like drag-and-drop, keyboard navigation, and multi-step forms. Use page.dragAndDrop() for drag-and-drop, page.keyboard.press() for keyboard shortcuts, and page.locator().selectOption() for dropdowns. For React apps with state management (Redux, Zustand), test the UI behavior rather than the store directly. For example, to test a todo list, add items, mark them complete, and verify the list updates. Playwright's locator API supports chaining and filtering, making it easy to target specific elements in lists.
input.press('Enter') instead of clicking a submit button to test keyboard submission, which is more realistic for accessibility.Handling Authentication in Tests
Many React apps require authentication. Playwright provides browser.newContext() with storageState to persist cookies and localStorage across tests. For login flows, use to authenticate via API and save the state. This avoids repeating login steps in every test. In production, we create a global setup that logs in once and saves the storage state to a file. Each test then uses that state, reducing test time by 30%. For multi-role testing, create separate storage states for admin, user, etc. Remember to clear state between test suites to avoid cross-test contamination.page.request()
.gitignore and regenerate them in CI using environment variables for credentials.storageState to persist authentication across tests, reducing boilerplate and test execution time.Visual Regression Testing with Playwright
Visual regression tests catch unintended UI changes by comparing screenshots. Playwright's expect(page).toHaveScreenshot() captures a screenshot and compares it to a baseline. For React apps, this is invaluable for detecting CSS regressions, layout shifts, or icon changes. Use fullPage: true to capture the entire page. In CI, run visual tests only on changed components to save time. Playwright's built-in pixel matching handles anti-aliasing differences across browsers. In production, we run visual tests on every PR and require approval for baseline updates.
--update-snapshots flag to update baselines when intentional changes occur.toHaveScreenshot catch CSS and layout regressions automatically.Running Tests in CI/CD Pipelines
Integrating Playwright tests into CI/CD is critical for catching regressions early. Use the Playwright Docker image (mcr.microsoft.com/playwright:v1.40.0-focal) for consistent browser environments. Configure your CI to run tests in parallel across multiple shards to reduce feedback time. For GitHub Actions, use the playwright-community/action-test action. Set retries to 2 for flaky tests. In production, we run E2E tests on every pull request and block merges if any test fails. Use --reporter=github for annotations directly on PRs. Cache browser binaries to speed up CI.
--shard flag.Debugging Flaky Tests
Flaky tests are the enemy of reliable CI. Playwright provides several tools to debug them: --trace for trace viewer, --video for video recordings, and --screenshot for screenshots on failure. Enable trace: 'on-first-retry' in config to capture traces only when a test fails on retry. Use to step through tests interactively. In production, we analyze traces to identify race conditions, network timing issues, or incorrect assertions. Common fixes include using page.pause()toHaveText instead of toBeVisible, adding waitForLoadState('networkidle'), or increasing timeouts for slow API calls.
page.waitForTimeout(). Instead, use auto-waiting or explicit waitForSelector with a timeout. Fixed waits make tests slow and flaky.waitForLoadState('networkidle') stabilized it.Testing Accessibility with Playwright
Accessibility testing ensures your React app is usable by people with disabilities. Playwright integrates with @axe-core/playwright to run automated accessibility audits. Use to scan the page and assert no violations. In production, we run accessibility tests on every page and fail the build if critical violations are found. Combine with manual keyboard navigation tests using page.axe()page.keyboard. For React apps, ensure all interactive elements have accessible names and roles. This catches issues like missing labels, low contrast, or missing ARIA attributes.
expect(results.violations).toEqual([]) to enforce a11y standards.@axe-core/playwright to catch violations early.Testing React Router Navigation
Single-page applications rely on client-side routing. Playwright can test navigation without full page reloads. Use page.waitForU to wait for a specific route after clicking a link. For React Router, test that the correct component renders for each route. Also test error routes (404 pages) and redirects. In production, we test that protected routes redirect to login when unauthenticated. Use RL()page.goBack() and page.goForward() to test browser history. This ensures your routing logic works correctly across browsers.
waitForURL instead of waitForLoadState to wait for client-side navigation to complete.Performance Testing with Playwright
Playwright can capture performance metrics like page load time, time to interactive, and largest contentful paint. Use to get performance data. For React apps, monitor component render times and network request durations. In production, we set performance budgets in CI: fail the test if load time exceeds 3 seconds. Use page.metrics()page.coverage to measure JavaScript and CSS coverage. This helps identify unused code. Combine with Lighthouse CI for comprehensive performance audits. Performance tests should run on a dedicated, consistent environment to avoid variance.
Playwright Component Testing
Playwright Component Testing allows you to test individual React components in isolation using the same Playwright API. With @playwright/experimental-ct-react, you can mount components directly in a browser environment, enabling interaction and assertions without a full app. This is ideal for testing component behavior, props, and state changes. To get started, install the package and configure Playwright to use React. Then, write test files that import and mount components. For example, you can test a button component by mounting it, clicking it, and verifying the resulting state or DOM changes. This approach provides faster feedback and more focused tests compared to full-page E2E tests. Component tests run in real browsers, ensuring realistic rendering and event handling. They also support Playwright's locators and assertions, making them powerful for UI-driven development. Use component testing to complement your E2E suite, especially for complex or reusable components.
Playwright UI Mode
Playwright UI Mode provides an interactive interface for developing and debugging tests. Instead of running tests headlessly, you can open a browser window where tests execute step by step. This mode allows you to inspect the DOM, view locator suggestions, and see the test timeline. To use UI Mode, run npx playwright test --ui. The UI shows a list of tests, their status, and a timeline of actions. You can hover over each action to see the corresponding screenshot and DOM snapshot. This is invaluable for debugging flaky tests or writing new tests. You can also use the locator picker to generate robust selectors. UI Mode supports watch mode, re-running tests on file changes. It integrates with VS Code via the Playwright extension. For CI, you can still run tests headlessly; UI Mode is strictly for local development. Adopting UI Mode can significantly speed up test creation and debugging, especially for complex interactions.
Sharding and Parallel Execution
Sharding and parallel execution are essential for reducing CI test times. Playwright supports sharding by splitting tests across multiple machines. Use the --shard flag to specify the shard index and total shards. For example, npx playwright test --shard=1/4 runs the first quarter of tests. In CI, you can configure parallel jobs, each running a different shard. Playwright also supports parallel execution within a single machine using multiple workers. Set workers in the Playwright config to the number of CPU cores. For large test suites, sharding across machines provides linear scaling. Combine sharding with test retries and project dependencies for optimal CI performance. Ensure tests are independent and can run in any order. Use test.describe.configure({ mode: 'parallel' }) for within-file parallelization. Sharding is particularly useful when tests are slow or numerous. It reduces the feedback loop, allowing developers to get results faster.
| File | Command / Code | Purpose |
|---|---|---|
| install.sh | npm init playwright@latest -- --ct | Why Playwright Over Cypress for React E2E |
| playwright.config.ts | export default defineConfig({ | Setting Up Playwright for a React App |
| e2e | test('user can log in with valid credentials', async ({ page }) => { | Writing Your First React E2E Test |
| e2e | test('displays loading state while fetching data', async ({ page }) => { | Mocking API Calls with Playwright |
| e2e | test('user can add and complete todos', async ({ page }) => { | Testing React Component Interactions |
| global-setup.ts | async function globalSetup(config: FullConfig) { | Handling Authentication in Tests |
| e2e | test('homepage matches snapshot', async ({ page }) => { | Visual Regression Testing with Playwright |
| .github | name: E2E Tests | Running Tests in CI/CD Pipelines |
| e2e | test('flaky test example', async ({ page }) => { | Debugging Flaky Tests |
| e2e | test('homepage has no accessibility violations', async ({ page }) => { | Testing Accessibility with Playwright |
| e2e | test('navigates to about page', async ({ page }) => { | Testing React Router Navigation |
| e2e | test('homepage loads within performance budget', async ({ page }) => { | Performance Testing with Playwright |
| Button.spec.tsx | test('should increment count on click', async ({ mount }) => { | Playwright Component Testing |
| Terminal | npx playwright test --ui | Playwright UI Mode |
Key takeaways
page.route() to mock API responses and test loading, error, and empty states deterministically without a backend.storageState to avoid repeating login steps, reducing test time and complexity.toHaveScreenshot to catch CSS and layout regressions before they reach production.Common mistakes to avoid
3 patternsNot understanding React component re-rendering
Ignoring the rules of hooks
Mutating state directly instead of using setState
Interview Questions on This Topic
What is the Virtual DOM and how does React use it?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
That's React. Mark it forged?
6 min read · try the examples if you haven't