Playwright E2E Setup
Install and configure Playwright for end-to-end testing of your Next.js application.
Recipe
Quick-reference recipe card -- copy-paste ready.
# Install Playwright
npm init playwright@latest
# Or install manually
npm install -D @playwright/test
npx playwright install// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? "github" : "html",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});# Run all tests
npx playwright test
# Run in headed mode (see the browser)
npx playwright test --headed
# Run with Playwright UI
npx playwright test --ui
# Run a specific file
npx playwright test e2e/home.spec.ts
# Debug mode (step through)
npx playwright test --debug
# Show HTML report
npx playwright show-reportWhen to reach for this: When you need to test full user flows in a real browser -- navigation, form submissions, authentication, and cross-page interactions.
Working Example
// e2e/home.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Home Page", () => {
test("has correct title", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/My App/);
});
test("navigates to about page", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: /about/i }).click();
await expect(page).toHaveURL("/about");
await expect(page.getByRole("heading", { level: 1 })).toHaveText("About Us");
});
test("search works", async ({ page }) => {
await page.goto("/");
await page.getByRole("searchbox").fill("react testing");
await page.getByRole("button", { name: /search/i }).click();
await expect(page).toHaveURL(/q=react\+testing/);
await expect(page.getByRole("heading")).toContainText("Search Results");
});
test("responsive navigation", async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("/");
// Mobile menu should be hidden initially
await expect(page.getByRole("navigation")).not.toBeVisible();
// Open mobile menu
await page.getByRole("button", { name: /menu/i }).click();
await expect(page.getByRole("navigation")).toBeVisible();
});
});What this demonstrates:
- Basic Playwright config with Next.js dev server
- Multi-browser project setup
- E2E tests with navigation, form interaction, and responsive testing
- Locator strategies matching Testing Library patterns
- CI-friendly configuration (retries, single worker, GitHub reporter)
Deep Dive
How It Works
- Playwright launches real browser instances (Chromium, Firefox, WebKit) and controls them via the DevTools Protocol
- The
webServerconfig starts your Next.js dev server before tests run and waits for it to be ready - Each test gets a fresh
BrowserContext(isolated cookies, storage) -- no state leaks between tests fullyParallel: trueruns tests in different files concurrently for speed- Traces capture a timeline of network requests, DOM snapshots, and console logs -- invaluable for debugging failures
Variations
Using a production build for testing:
// playwright.config.ts
webServer: {
command: "npm run build && npm run start",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},GitHub Actions CI configuration:
# .github/workflows/e2e.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test --project=chromium
env:
CI: true
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 7Running only chromium in CI for speed:
// playwright.config.ts
projects: process.env.CI
? [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }]
: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
],TypeScript Notes
// Playwright tests use TypeScript natively -- no extra config needed
// The @playwright/test package includes all types
import { test, expect, Page } from "@playwright/test";
// Typed page fixture
test("example", async ({ page }: { page: Page }) => {
// page is fully typed with all Playwright methods
});Gotchas
-
Port conflicts -- If port 3000 is already in use, Playwright's
webServerfails to start. Fix: UsereuseExistingServer: truelocally, or configure a different port. -
Slow CI startup -- Building Next.js before each E2E run is slow. Fix: Cache the
.nextbuild directory in CI, or run E2E tests only on merge to main. -
Browser installation missing --
npx playwright installdownloads browsers. In CI, use--with-depsto install system dependencies too. Fix: Addnpx playwright install --with-depsto your CI script. -
Flaky tests from timing -- E2E tests that rely on exact timing break. Fix: Use Playwright's built-in auto-waiting and
expectassertions which retry automatically. -
Test data pollution -- Tests that create real data in a shared database can conflict. Fix: Use a test database, seed data before tests, and clean up after.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Cypress | You prefer Cypress's API or need its component testing | You want multi-browser support or faster execution |
| Testing Library (unit) | You want fast component tests without a browser | You need to test real browser behavior (navigation, cookies) |
| Selenium | You have existing Selenium infrastructure | You are starting fresh (Playwright is more modern) |
FAQs
What does the webServer config do in playwright.config.ts?
It automatically starts your Next.js dev (or production) server before tests run and waits for it to be ready. It also shuts it down when tests complete.
What is the difference between --headed and --ui mode?
--headedshows the browser window while tests run.--uiopens Playwright's interactive UI for selecting, running, and debugging individual tests with a timeline view.
How do I run tests in only one browser for speed?
npx playwright test --project=chromiumOr configure projects in playwright.config.ts to include only chromium in CI.
Gotcha: What happens if port 3000 is already in use?
Playwright's webServer fails to start. Use reuseExistingServer: true locally to reuse a running dev server, or configure a different port.
How do I install Playwright browsers in CI?
npx playwright install --with-deps chromiumThe --with-deps flag installs system dependencies (fonts, libraries) needed for browser rendering.
What does fullyParallel: true do?
It runs tests in different files concurrently. Each test gets a fresh browser context, so there is no state leakage between parallel tests.
How do I debug a failing Playwright test?
npx playwright test --debugThis opens the browser with a step-through debugger. You can also use --trace on to capture traces for post-mortem debugging.
Gotcha: Why are my E2E tests flaky in CI?
Common causes: timing issues, missing environment variables, or slow CI machines. Use Playwright's built-in auto-waiting and expect assertions which retry automatically. Avoid page.waitForTimeout().
How do I test responsive layouts with different viewport sizes?
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("/");
await expect(page.getByRole("button", { name: /menu/i })).toBeVisible();How do I type Playwright tests in TypeScript?
No extra configuration needed. The @playwright/test package includes all types:
import { test, expect, Page } from "@playwright/test";How do I use a production build for E2E tests instead of dev mode?
Change the webServer command:
webServer: {
command: "npm run build && npm run start",
url: "http://localhost:3000",
},How do I upload the Playwright report as a CI artifact?
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 7Related
- Playwright E2E Patterns -- Page Object Model and common patterns
- Advanced Playwright -- auth, API testing, and mocking
- Testing Strategy & Best Practices -- when to use E2E vs unit tests
- Vitest Setup -- unit testing alongside E2E