React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

playwrighte2eend-to-endsetupcigithub-actions

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-report

When 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 webServer config 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: true runs 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: 7

Running 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 webServer fails to start. Fix: Use reuseExistingServer: true locally, or configure a different port.

  • Slow CI startup -- Building Next.js before each E2E run is slow. Fix: Cache the .next build directory in CI, or run E2E tests only on merge to main.

  • Browser installation missing -- npx playwright install downloads browsers. In CI, use --with-deps to install system dependencies too. Fix: Add npx playwright install --with-deps to your CI script.

  • Flaky tests from timing -- E2E tests that rely on exact timing break. Fix: Use Playwright's built-in auto-waiting and expect assertions 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

AlternativeUse WhenDon't Use When
CypressYou prefer Cypress's API or need its component testingYou want multi-browser support or faster execution
Testing Library (unit)You want fast component tests without a browserYou need to test real browser behavior (navigation, cookies)
SeleniumYou have existing Selenium infrastructureYou 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?
  • --headed shows the browser window while tests run.
  • --ui opens 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=chromium

Or 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 chromium

The --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 --debug

This 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: 7