NextjsEnd-to-End Testing (Playwright/Cypress)

End-to-End Testing (Playwright/Cypress)

Unit and component tests check a function or a component in isolation, often with mocked dependencies. An end-to-end (E2E) test takes the opposite approach: it starts a real browser, points it at your actual running application, and drives it the way a user would — clicking buttons, filling in forms, and asserting on what actually appears on the screen. Nothing is mocked. If a database call, an API route, a Server Action, and three layers of components all have to work together for a checkout flow to succeed, an E2E test is the only kind of test that actually proves they do.
Note
Next.js does not ship a built-in E2E runner. The two dominant tools in the ecosystem are Playwright (built by Microsoft) and Cypress. Both work well with the App Router; Next.js official examples and documentation now lean toward Playwright, but either is a reasonable choice.
Playwright vs Cypress

Both tools launch a real browser and let you script interactions against it, but they differ in architecture and day-to-day feel.

Playwright

Cypress

Browser engines

Chromium, Firefox, WebKit — all first-class

Chromium-family and Firefox; WebKit support is more limited

Execution model

Runs outside the browser, drives it over a protocol; supports true parallel workers out of the box

Runs inside the browser event loop alongside your app

Multi-tab / multi-origin

Native support for multiple tabs, windows, and origins

Historically limited to a single origin per test (improving over time)

Auto-waiting

Built-in — waits for elements to be actionable before interacting

Built-in — similar retry/waiting behavior

Language support

JavaScript, TypeScript, Python, Java, .NET

JavaScript, TypeScript

Debugging experience

Trace viewer, codegen recorder, VS Code extension

Time-travel debugger in its own dashboard-style runner

In practice, both catch the same class of bugs. Playwright tends to win teams that need cross-browser coverage or heavy parallelization; Cypress has a longer track record and a very approachable developer experience for teams that only need to cover Chromium-family browsers.

A worked example with Playwright
After installing Playwright (npm init playwright@latest), tests live in a tests/ or e2e/ directory and run against a real server — either one you start yourself or one Playwright starts for you via its config.

e2e/homepage.spec.ts

TS
import { test, expect } from '@playwright/test'

test('homepage has a working search link', async ({ page }) => {
  // 1. Visit the real, running app
  await page.goto('http://localhost:3000')

  // 2. Interact with it like a user would
  await page.getByRole('link', { name: 'Search' }).click()

  // 3. Assert on the resulting state
  await expect(page).toHaveURL(/\/search/)
  await expect(
    page.getByRole('heading', { name: 'Search' })
  ).toBeVisible()
})

playwright.config.ts — start Next.js automatically

TS
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  webServer: {
    command: 'npm run build && npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
  use: {
    baseURL: 'http://localhost:3000',
  },
})

Run the suite

Bash
npx playwright test
npx playwright test --ui   # interactive UI mode
npx playwright show-report # HTML report from the last run

The same scenario in Cypress reads similarly — visit, interact, assert — just with Cypress's own chained API:

cypress/e2e/homepage.cy.ts

TS
describe('homepage', () => {
  it('has a working search link', () => {
    cy.visit('http://localhost:3000')
    cy.contains('a', 'Search').click()
    cy.url().should('include', '/search')
    cy.contains('h1', 'Search').should('be.visible')
  })
})
Tip
E2E tests are slow and comparatively expensive to run and maintain — each one boots a real browser and a real app. Use them sparingly, to cover a handful of critical user journeys (sign up, checkout, login) end to end. Push everything else — component logic, edge cases, validation rules — down into faster unit and component tests. A healthy test suite looks like a pyramid: many unit tests, fewer integration tests, and just a handful of E2E tests at the top.
  • E2E tests drive a real browser against a real running app — no mocks — so they verify that every layer actually works together.

  • Playwright and Cypress are the two mainstream choices; Playwright offers broader cross-browser and parallelization support, Cypress offers a very approachable developer experience.

  • Both provide auto-waiting so tests do not need manual sleeps while the UI catches up.

  • Because E2E tests are slow to run and can be flaky, reserve them for a small set of critical user flows rather than every possible interaction.