Home JavaScript E2E Testing React Apps with Playwright
Advanced 6 min · July 13, 2026

E2E Testing React Apps with Playwright

Playwright setup, locators, assertions, mocking API, and CI integration..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 13, 2026
last updated
2,018
articles · all by Naren
Before you start⏱ 36 min read
  • 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.
Quick Answer

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.

✦ Definition~90s read
What is E2E Testing React Apps with Playwright?

E2E Testing React Apps with Playwright is a fundamental concept in React development. It refers to the patterns, APIs, and best practices that React provides for building user interfaces. Understanding this concept is essential for writing clean, efficient, and maintainable React code. This tutorial covers everything from basic usage to advanced patterns with real-world examples.

React is a JavaScript library for building user interfaces.
Plain-English First

React is a JavaScript library for building user interfaces. This article covers e2e playwright — a key concept for building modern web applications.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

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.

install.shBASH
1
2
3
4
npm init playwright@latest -- --ct
# Or add to existing project:
npm install -D @playwright/test
npx playwright install
Output
Installing Playwright Test (v1.40.0)
Downloading browsers...
✔ Chromium (118.0.5993.70)
✔ Firefox (118.0.2)
✔ WebKit (17.0)
🔥Playwright vs Cypress: Key Differences
Playwright runs out-of-process, giving it access to browser contexts, network mocking, and cross-browser testing without vendor lock-in. Cypress is limited to Chromium-based browsers and cannot handle multiple tabs natively.
📊 Production Insight
We migrated from Cypress to Playwright after hitting a same-origin issue with our OAuth flow. Playwright's browser contexts allowed us to test the full login flow without workarounds.
🎯 Key Takeaway
Playwright's protocol-level architecture makes it superior for cross-browser React E2E testing.
react-e2e-playwright THECODEFORGE.IO Playwright E2E Test Workflow for React Apps Step-by-step process from setup to CI execution Install Playwright npm init playwright@latest Configure Test Files Set baseURL and browser options Write Test Scripts Use page.goto, locator, and assertions Mock API Calls Intercept network requests with page.route Run Tests Locally npx playwright test --headed Integrate with CI/CD Run in GitHub Actions with shard ⚠ Forgetting to await page.route can cause flaky tests Always await route interception before navigation THECODEFORGE.IO
thecodeforge.io
React E2E Playwright

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.

playwright.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  retries: 2,
  workers: 4,
  reporter: [['html'], ['list']],
  use: {
    baseURL: 'http://localhost:5173',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'npm run dev',
    port: 5173,
    reuseExistingServer: !process.env.CI,
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    { name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
  ],
});
Try it live
💡Use webServer for CI
Setting reuseExistingServer: !process.env.CI speeds up local development by reusing the dev server, while CI always starts fresh.
📊 Production Insight
We once had a bug that only appeared in Firefox due to a CSS grid layout issue. Running tests across all three browsers in CI caught it before release.
🎯 Key Takeaway
Configure Playwright with multiple browser projects and a webServer to mirror production environments.

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 page.goto() to navigate, page.locator() to find elements, and expect() for assertions. Playwright's auto-waiting means you don't need explicit waits for most actions. For React apps, prefer 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.

e2e/login.spec.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { test, expect } from '@playwright/test';

test('user can log in with valid credentials', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('securePassword123');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByText('Welcome back, User!')).toBeVisible();
});

test('shows error on invalid credentials', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('wrong@example.com');
  await page.getByLabel('Password').fill('wrong');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByText('Invalid email or password')).toBeVisible();
});
Output
Running 2 tests using 2 workers
✓ login.spec.ts:3:1 › user can log in with valid credentials (2.1s)
✓ login.spec.ts:11:1 › shows error on invalid credentials (1.8s)
Try it live
⚠ Avoid Implementation Details
Don't use page.evaluate() to check React state. Instead, assert on visible DOM elements. This keeps tests decoupled from your component internals.
📊 Production Insight
We refactored a form component from class to hooks, and our E2E tests passed without changes because they only relied on accessible labels and roles.
🎯 Key Takeaway
Write tests that simulate real user interactions and assert on visible UI, not internal state.
react-e2e-playwright THECODEFORGE.IO Playwright E2E Test Architecture for React Layered structure from UI to test runner Test Runner Playwright Test Runner | Config File Browser Layer Chromium | Firefox | WebKit Test Scripts Page Object Models | Fixtures | Hooks React App Under Test Components | State Management | Routing Mocking & Network API Route Interception | Mock Data CI/CD Integration GitHub Actions | Docker | Reporters THECODEFORGE.IO
thecodeforge.io
React E2E Playwright

Mocking API Calls with Playwright

In E2E tests, you often need to control backend responses to test edge cases. Playwright's page.route() 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 route.fulfill() to return a custom response, or route.abort() 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.

e2e/dashboard.spec.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { test, expect } from '@playwright/test';

test('displays loading state while fetching data', async ({ page }) => {
  // Delay the API response to show loading
  await page.route('**/api/dashboard', async route => {
    await new Promise(resolve => setTimeout(resolve, 2000));
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ users: [] }),
    });
  });
  await page.goto('/dashboard');
  await expect(page.getByText('Loading...')).toBeVisible();
});

test('shows error message on API failure', async ({ page }) => {
  await page.route('**/api/dashboard', route => route.abort('connectionrefused'));
  await page.goto('/dashboard');
  await expect(page.getByText('Failed to load data')).toBeVisible();
});
Output
Running 2 tests using 2 workers
✓ dashboard.spec.ts:3:1 › displays loading state while fetching data (2.3s)
✓ dashboard.spec.ts:17:1 › shows error message on API failure (1.2s)
Try it live
💡Mock at the Network Level
Avoid mocking at the application level (e.g., via dependency injection). Network-level mocking tests the full integration, including how your app handles HTTP responses.
📊 Production Insight
We once had a bug where the app crashed on a 500 error because the error boundary wasn't triggered. Mocking the API to return 500 caught it immediately.
🎯 Key Takeaway
Use 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.

e2e/todos.spec.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { test, expect } from '@playwright/test';

test('user can add and complete todos', async ({ page }) => {
  await page.goto('/todos');
  const input = page.getByPlaceholder('Add a todo');
  await input.fill('Buy milk');
  await input.press('Enter');
  await expect(page.getByText('Buy milk')).toBeVisible();
  await page.getByRole('checkbox').check();
  await expect(page.getByText('Buy milk')).toHaveClass(/completed/);
});

test('user can delete a todo', async ({ page }) => {
  await page.goto('/todos');
  await page.getByPlaceholder('Add a todo').fill('Learn Playwright');
  await page.getByPlaceholder('Add a todo').press('Enter');
  await page.getByRole('button', { name: 'Delete' }).click();
  await expect(page.getByText('Learn Playwright')).not.toBeVisible();
});
Output
Running 2 tests using 2 workers
✓ todos.spec.ts:3:1 › user can add and complete todos (1.5s)
✓ todos.spec.ts:13:1 › user can delete a todo (1.3s)
Try it live
🔥Keyboard Events
Use input.press('Enter') instead of clicking a submit button to test keyboard submission, which is more realistic for accessibility.
📊 Production Insight
We found a bug where pressing Enter in a form didn't submit because the event handler was on the button, not the form. Testing keyboard submission caught it.
🎯 Key Takeaway
Test user interactions like drag-and-drop and keyboard navigation to ensure a polished UX.

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 page.request() 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.

global-setup.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const { baseURL, storageState } = config.projects[0].use;
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(baseURL + '/login');
  await page.getByLabel('Email').fill('admin@example.com');
  await page.getByLabel('Password').fill('admin123');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.context().storageState({ path: storageState as string });
  await browser.close();
}

export default globalSetup;
Try it live
⚠ Storage State Security
Never commit storage state files to version control. Add them to .gitignore and regenerate them in CI using environment variables for credentials.
📊 Production Insight
Our CI tests were timing out because each test logged in individually. Using global setup cut test suite time from 15 minutes to 5.
🎯 Key Takeaway
Use 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.

e2e/visual.spec.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { test, expect } from '@playwright/test';

test('homepage matches snapshot', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    maxDiffPixelRatio: 0.01,
  });
});

test('dashboard matches snapshot on mobile', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 812 });
  await page.goto('/dashboard');
  await expect(page).toHaveScreenshot('dashboard-mobile.png');
});
Output
Running 2 tests using 2 workers
✓ visual.spec.ts:3:1 › homepage matches snapshot (3.2s)
✓ visual.spec.ts:10:1 › dashboard matches snapshot on mobile (2.8s)
Try it live
💡Baseline Management
Store baseline screenshots in version control. Use --update-snapshots flag to update baselines when intentional changes occur.
📊 Production Insight
A developer accidentally changed a global CSS variable, shifting all button colors. Visual tests caught it before the PR was merged.
🎯 Key Takeaway
Visual regression tests with 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.

.github/workflows/e2e.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
name: E2E Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/playwright:v1.40.0-focal
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - run: npm ci
      - name: Cache Playwright browsers
        uses: actions/cache@v3
        with:
          path: ~/.cache/ms-playwright
          key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
      - run: npx playwright test --shard=${{ matrix.shard }}/${{ strategy.jobs.test.strategy.matrix.shard | length }}
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
🔥Sharding for Speed
Shard your tests across multiple CI runners to reduce total execution time. Playwright supports sharding natively with the --shard flag.
📊 Production Insight
We reduced CI feedback time from 20 minutes to 5 by sharding across 4 runners and caching browser binaries.
🎯 Key Takeaway
Run Playwright tests in CI with sharding and Docker to ensure fast, reliable feedback.

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 page.pause() to step through tests interactively. In production, we analyze traces to identify race conditions, network timing issues, or incorrect assertions. Common fixes include using toHaveText instead of toBeVisible, adding waitForLoadState('networkidle'), or increasing timeouts for slow API calls.

e2e/flaky.spec.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { test, expect } from '@playwright/test';

test('flaky test example', async ({ page }) => {
  await page.goto('/slow-page');
  // Use waitForLoadState to ensure network is idle
  await page.waitForLoadState('networkidle');
  const text = page.getByText('Loaded');
  await expect(text).toBeVisible({ timeout: 10000 });
});

test('debug with trace', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('button').click();
  // page.pause(); // Uncomment to step through
  await expect(page.getByText('Success')).toBeVisible();
});
Try it live
⚠ Avoid Fixed Waits
Never use page.waitForTimeout(). Instead, use auto-waiting or explicit waitForSelector with a timeout. Fixed waits make tests slow and flaky.
📊 Production Insight
A flaky test was passing locally but failing in CI because of a slower API response. Adding waitForLoadState('networkidle') stabilized it.
🎯 Key Takeaway
Use Playwright's trace viewer and video recordings to diagnose and fix flaky tests.

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 page.axe() 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.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.

e2e/accessibility.spec.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('homepage has no accessibility violations', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

test('login form is keyboard navigable', async ({ page }) => {
  await page.goto('/login');
  await page.keyboard.press('Tab');
  await expect(page.getByLabel('Email')).toBeFocused();
  await page.keyboard.press('Tab');
  await expect(page.getByLabel('Password')).toBeFocused();
  await page.keyboard.press('Tab');
  await expect(page.getByRole('button', { name: 'Sign in' })).toBeFocused();
});
Output
Running 2 tests using 2 workers
✓ accessibility.spec.ts:5:1 › homepage has no accessibility violations (2.5s)
✓ accessibility.spec.ts:12:1 › login form is keyboard navigable (1.1s)
Try it live
💡Integrate with CI
Fail the build if any accessibility violations are found. Use expect(results.violations).toEqual([]) to enforce a11y standards.
📊 Production Insight
We missed a missing label on a search input, causing screen readers to announce 'edit text' instead of 'search'. The aXe audit caught it.
🎯 Key Takeaway
Automate accessibility audits with @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.waitForURL() 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 page.goBack() and page.goForward() to test browser history. This ensures your routing logic works correctly across browsers.

e2e/routing.spec.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { test, expect } from '@playwright/test';

test('navigates to about page', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('link', { name: 'About' }).click();
  await expect(page).toHaveURL('/about');
  await expect(page.getByText('About Us')).toBeVisible();
});

test('shows 404 page for unknown routes', async ({ page }) => {
  await page.goto('/nonexistent');
  await expect(page.getByText('Page Not Found')).toBeVisible();
});

test('redirects to login when accessing protected route', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page).toHaveURL('/login');
});
Output
Running 3 tests using 3 workers
✓ routing.spec.ts:3:1 › navigates to about page (1.2s)
✓ routing.spec.ts:9:1 › shows 404 page for unknown routes (0.9s)
✓ routing.spec.ts:14:1 › redirects to login when accessing protected route (1.0s)
Try it live
🔥SPA Navigation
Use waitForURL instead of waitForLoadState to wait for client-side navigation to complete.
📊 Production Insight
We had a bug where a redirect loop occurred for authenticated users on the login page. Testing redirects caught it.
🎯 Key Takeaway
Test client-side routing, including redirects and 404 pages, to ensure navigation works as expected.

Performance Testing with Playwright

Playwright can capture performance metrics like page load time, time to interactive, and largest contentful paint. Use page.metrics() 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.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.

e2e/performance.spec.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { test, expect } from '@playwright/test';

test('homepage loads within performance budget', async ({ page }) => {
  await page.goto('/', { waitUntil: 'networkidle' });
  const metrics = await page.metrics();
  const loadTime = metrics.DomContentLoaded;
  expect(loadTime).toBeLessThan(3000); // 3 seconds
  console.log(`DomContentLoaded: ${loadTime}ms`);
});

test('measures largest contentful paint', async ({ page }) => {
  await page.goto('/');
  const lcp = await page.evaluate(() => {
    return new Promise(resolve => {
      new PerformanceObserver(list => {
        resolve(list.getEntries()[0].startTime);
      }).observe({ type: 'largest-contentful-paint', buffered: true });
    });
  });
  expect(lcp).toBeLessThan(2500);
});
Output
Running 2 tests using 2 workers
✓ performance.spec.ts:3:1 › homepage loads within performance budget (1.8s)
✓ performance.spec.ts:12:1 › measures largest contentful paint (2.1s)
Try it live
⚠ Consistent Environment
Performance tests are sensitive to machine load. Run them on dedicated CI runners or use Docker to minimize variance.
📊 Production Insight
A third-party script was blocking rendering, causing LCP to spike to 5 seconds. Performance tests alerted us before users noticed.
🎯 Key Takeaway
Capture performance metrics in Playwright to enforce budgets and catch regressions.

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.

Button.spec.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
import { test, expect } from '@playwright/experimental-ct-react';
import Button from './Button';

test('should increment count on click', async ({ mount }) => {
  const component = await mount(<Button />);
  await component.click();
  await expect(component).toContainText('Clicked 1 time');
});
Try it live
💡Component Testing vs E2E
📊 Production Insight
In CI, component tests can run in parallel with E2E tests, reducing overall feedback time. They are also faster to debug since failures are scoped to a single component.
🎯 Key Takeaway
Playwright Component Testing with @playwright/experimental-ct-react lets you test React components in isolation using real browser rendering.

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.

TerminalBASH
1
npx playwright test --ui
🔥UI Mode Features
📊 Production Insight
UI Mode is not used in CI; it's a local development tool. However, it reduces the time spent debugging flaky tests, leading to more stable CI pipelines.
🎯 Key Takeaway
Playwright UI Mode offers an interactive test runner with step-by-step execution, DOM inspection, and locator suggestions for faster development.
Playwright vs Cypress for React E2E Testing Key differences in setup, features, and performance Playwright Cypress Browser Support Chromium, Firefox, WebKit Chromium only API Mocking page.route with full control cy.intercept with limitations Parallel Execution Built-in sharding Requires Cypress Dashboard Component Testing Experimental, via Playwright Native support Network Throttling Built-in via context Requires plugin CI/CD Integration Seamless with Docker More complex setup THECODEFORGE.IO
thecodeforge.io
React E2E Playwright

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.

playwright.config.tsTYPESCRIPT
1
2
3
4
5
import { defineConfig } from '@playwright/test';
export default defineConfig({
  workers: process.env.CI ? 4 : undefined,
  retries: process.env.CI ? 2 : 0,
});
Try it live
⚠ Test Isolation
📊 Production Insight
In production CI, use sharding with a matrix strategy (e.g., GitHub Actions) to run shards in parallel. Monitor shard duration to balance test distribution.
🎯 Key Takeaway
Sharding splits tests across multiple CI machines, while parallel execution uses multiple workers per machine, both drastically reducing test duration.
⚙ Quick Reference
14 commands from this guide
FileCommand / CodePurpose
install.shnpm init playwright@latest -- --ctWhy Playwright Over Cypress for React E2E
playwright.config.tsexport default defineConfig({Setting Up Playwright for a React App
e2elogin.spec.tstest('user can log in with valid credentials', async ({ page }) => {Writing Your First React E2E Test
e2edashboard.spec.tstest('displays loading state while fetching data', async ({ page }) => {Mocking API Calls with Playwright
e2etodos.spec.tstest('user can add and complete todos', async ({ page }) => {Testing React Component Interactions
global-setup.tsasync function globalSetup(config: FullConfig) {Handling Authentication in Tests
e2evisual.spec.tstest('homepage matches snapshot', async ({ page }) => {Visual Regression Testing with Playwright
.githubworkflowse2e.ymlname: E2E TestsRunning Tests in CI/CD Pipelines
e2eflaky.spec.tstest('flaky test example', async ({ page }) => {Debugging Flaky Tests
e2eaccessibility.spec.tstest('homepage has no accessibility violations', async ({ page }) => {Testing Accessibility with Playwright
e2erouting.spec.tstest('navigates to about page', async ({ page }) => {Testing React Router Navigation
e2eperformance.spec.tstest('homepage loads within performance budget', async ({ page }) => {Performance Testing with Playwright
Button.spec.tsxtest('should increment count on click', async ({ mount }) => {Playwright Component Testing
Terminalnpx playwright test --uiPlaywright UI Mode

Key takeaways

1
Protocol-Level Architecture
Playwright's out-of-process design enables true cross-browser testing, network interception, and multi-tab workflows, making it superior to Cypress for production React apps.
2
Network Mocking
Use page.route() to mock API responses and test loading, error, and empty states deterministically without a backend.
3
Authentication State
Persist login state with storageState to avoid repeating login steps, reducing test time and complexity.
4
Visual Regression
Automate screenshot comparisons with toHaveScreenshot to catch CSS and layout regressions before they reach production.

Common mistakes to avoid

3 patterns
×

Not understanding React component re-rendering

Fix
Use React.memo, useMemo, and useCallback strategically to prevent unnecessary re-renders.
×

Ignoring the rules of hooks

Fix
Always call hooks at the top level, not inside conditions, loops, or callbacks.
×

Mutating state directly instead of using setState

Fix
Always use the state setter function and treat state as immutable.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the Virtual DOM and how does React use it?
Q02JUNIOR
Explain the difference between state and props.
Q03JUNIOR
What is the purpose of the useEffect hook?
Q04JUNIOR
How does React handle keys in lists?
Q01 of 04JUNIOR

What is the Virtual DOM and how does React use it?

ANSWER
The Virtual DOM is a lightweight JavaScript representation of the real DOM. React diffs the Virtual DOM with the previous version and applies only the changed parts to the real DOM.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
How do I test file downloads in Playwright?
02
Can I test WebSocket connections with Playwright?
03
How do I run a single test file in Playwright?
04
What is the best way to handle environment variables in Playwright?
05
How do I test a React component that uses lazy loading?
06
Can I use Playwright with React Testing Library?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 13, 2026
last updated
2,018
articles · all by Naren
🔥

That's React. Mark it forged?

6 min read · try the examples if you haven't

Previous
React Testing with Jest and React Testing Library
7 / 40 · React
Next
TypeScript with React