# E2E Testing Next.js Apps with Playwright: Setup, Patterns, and CI Integration

> How to set up Playwright for a Next.js app, write tests that don't break on every deploy, and run them in GitHub Actions CI.

URL: https://www.matthewswong.com/en/blog/e2e-testing-playwright-nextjs/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2025-06-10
- Updated: 2025-06-10
- Category: Web Dev

Unit tests verify components in isolation. E2E tests verify that your actual application works the way users experience it — with a real browser, real navigation, and real network requests. Playwright has become the dominant E2E testing tool for Next.js in 2025: it's fast (up to 2x faster than competitors in benchmarks), auto-waits for elements without manual sleep calls, and handles Next.js's server components, streaming, and hydration correctly. I've run Playwright tests on a Next.js ERP dashboard and reduced test suite time by 40% compared to Cypress by switching to Playwright's parallel execution.

## Why Playwright for Next.js

Playwright tests run against a real Chromium, Firefox, or WebKit browser — not a simulated environment. For Next.js apps with server-side rendering, this matters: Playwright sees exactly what the user sees, including the initial server-rendered HTML before hydration. It handles dynamic content, loading states, and navigation correctly through its built-in auto-waiting — `page.getByRole('button', '{name: ''Submit''}')` waits for the button to be visible and enabled before clicking. No more `waitForTimeout(1000)` hacks that make tests slow and brittle.

### Setup: Test Against a Production Build

The most important Playwright setup decision: test against a production build, not the development server. The development server includes hot reloading middleware, source maps, and development-only error overlays that don't exist in production. Tests that pass in development can fail in production if a build-only optimization or different code path is triggered. Configure Playwright's `webServer` option to build and serve a production Next.js build before tests run.

### The Page Object Model Pattern

The Page Object Model (POM) wraps Playwright's low-level API in a domain-specific interface. Instead of `page.getByLabel('Email').fill('user@example.com')` scattered through every test, you write `loginPage.fillEmail('user@example.com')`. When the email input's label changes, you update one file (the LoginPage class) instead of every test that uses it. For an ERP dashboard with 50+ tests across 10 user flows, POM is the difference between a maintainable test suite and a brittle mess.

> From my experience running Playwright on a Next.js ERP app: set up auth state persistence using Playwright's storageState feature. Log in once in a setup project, save the session to a file, and load that session in every test that needs authentication. Without this, every test that requires login (which is almost all of them in an ERP app) performs a full login flow — adding 2-3 seconds per test. With saved auth state, authenticated tests start with the session already active. In a 100-test suite, this saves 3-5 minutes of CI time.

## Writing Tests That Don't Break

Flaky tests are the main reason teams abandon E2E testing. The causes: hard-coded waits (sleep(1000)), selectors based on CSS classes that change during refactoring, tests that depend on execution order, and tests that share state. The Playwright best practices: prefer semantic selectors (getByRole, getByLabel, getByText) over CSS or XPath selectors that break on styling changes; use `expect(page).toHaveURL()` instead of checking URLs manually; never use `page.waitForTimeout()`; reset test state in `beforeEach` so tests are order-independent.

### Testing Server Components and Streaming

Next.js 13+ App Router with server components and Suspense streaming means page content loads progressively. Playwright handles this correctly because it uses real browser behavior — it will wait for network requests to complete and DOM to stabilize before assertions, provided you use auto-waiting assertions. The key: use `expect(page.getByText('Invoice List')).toBeVisible()` rather than checking the DOM immediately after navigation. Playwright's auto-waiter retries the assertion for up to 5 seconds by default, which covers most streaming scenarios.

> **Never Test Implementation Details — Test User Behavior**

> A common Playwright anti-pattern: testing that a button has a specific CSS class, that a React state variable has a certain value, or that a specific DOM node exists. These tests break whenever you refactor the implementation, even when the user experience is unchanged. Write tests from the user's perspective: 'When I click Submit, I see a success message' — not 'when the form's submit handler is called, the successState variable is true'. Tests that describe behavior are resilient to refactoring. Tests that describe implementation break on every non-functional change.

## GitHub Actions CI Integration

Playwright runs headlessly in CI out of the box. The GitHub Actions workflow: checkout code, install dependencies (including Playwright browsers with `npx playwright install --with-deps`), build the Next.js app, and run `npx playwright test`. Playwright reports failures as GitHub Actions annotations — failed tests appear inline in the PR diff. Enable Playwright's HTML reporter and upload the report as a GitHub Actions artifact — on failure, download and open locally to see screenshots and traces of each failure.

## Parallel Test Execution

Playwright runs tests in parallel by default — each test file gets its own browser context, and multiple contexts run simultaneously across worker processes. Configure `workers` in playwright.config.ts to control parallelism. For a CI machine with 4 cores, `workers: 4` runs 4 browser contexts in parallel. Playwright's isolation guarantee: each test context has its own cookies, localStorage, and network state — no cross-test contamination. For tests that must run in a specific order (e.g., create, then edit, then delete), put them in the same spec file and use test.describe.serial.

## Component Testing with Playwright

Playwright also supports component testing — rendering individual React components in a real browser, in isolation. This is useful for UI components that are complex to test with jsdom (React Testing Library's environment): drag-and-drop, canvas rendering, complex animations, or components that depend on browser APIs. Component tests in Playwright mount the component with a fixture server, so you can test the actual rendered browser output. For most components, React Testing Library is faster and simpler — use Playwright component testing only when you need real browser behavior.

## Sources & Further Reading

- Next.js — Playwright Testing Guide
- DeviQA — Playwright E2E Testing Guide 2026
- JS Mastery — Test Next.js Apps with Playwright Best Practices

## Source links

- https://nextjs.org/docs/app/guides/testing/playwright
- https://www.deviqa.com/blog/guide-to-playwright-end-to-end-testing-in-2025/
- https://jsmastery.com/blogs/test-next-js-apps-with-playwright-5-best-practices

## FAQ

### Why should you test against a production build rather than the Next.js development server?

The development server includes hot reloading middleware, source maps, and development-only error overlays that don't exist in production. Tests that pass in development can fail in production if a build-only optimization or different code path is triggered. Playwright's webServer option can be configured to build and serve a production Next.js build before tests run.

### What is the Page Object Model pattern and why does it matter for large test suites?

The Page Object Model (POM) wraps Playwright's low-level API in a domain-specific interface, so instead of repeating raw selectors across every test you call methods on a dedicated page class. When a selector changes, you update one file rather than every test that uses it. For an ERP dashboard with 50+ tests across 10 user flows, POM is the difference between a maintainable suite and a brittle mess.

### How does Playwright's storageState feature reduce CI time for authenticated apps?

By logging in once in a setup project and saving the session to a file, every subsequent test can load that session instead of performing a full login flow. Without this, each test that requires authentication adds 2–3 seconds for the login step. In a 100-test suite, using saved auth state can save 3–5 minutes of CI time.

### What are the main causes of flaky Playwright tests, and how should they be avoided?

The primary culprits are hard-coded waits like sleep(1000), selectors tied to CSS classes that change during refactoring, tests that depend on execution order, and tests that share state. Playwright best practice is to use semantic selectors (getByRole, getByLabel, getByText), rely on auto-waiting assertions instead of page.waitForTimeout(), and reset test state in beforeEach so tests are order-independent.

### When should you choose Playwright component testing over React Testing Library?

React Testing Library runs in jsdom and is faster and simpler for most component tests. Playwright component testing mounts the component in a real browser, making it the right choice for scenarios that require actual browser behavior — such as drag-and-drop, canvas rendering, complex animations, or components that depend on browser APIs not available in jsdom.

---

- HTML version: https://www.matthewswong.com/en/blog/e2e-testing-playwright-nextjs/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/e2e-testing-playwright-nextjs/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
