Phuriwaj

Playwright E2E Setup for Next.js + Claude Code

Full setup for Playwright E2E tests in a Next.js project, with a CLAUDE.md Definition of Done that forces Claude Code to run tests before marking any task complete.

Why / When to Use

When Claude Code keeps saying β€œdone” after only reading code. Wiring Playwright into CLAUDE.md creates a hard gate: build must pass, tests must pass, screenshot must be taken β€” before completion is declared.

Core Concept / Commands

Install

npm init playwright@latest
# Choose: TypeScript, tests folder: e2e, no GitHub Actions, install browsers: Yes

CLAUDE.md β€” Definition of Done

Add to project root CLAUDE.md:

## Dev Server
- Start: `npm run dev`
- Port: 3000
 
## Definition of Done
Before saying a task is complete, you MUST:
1. Run `npm run build` β€” must pass with 0 errors
2. Start dev server in background
3. Run Playwright tests: `npx playwright test`
4. If tests fail, fix the code and re-run
5. Take a screenshot of the final result
6. Report: what passed, what failed, what was NOT tested
 
## Testing Rules
- Never say "tested" if you only read the code
- Always run the browser test, not just lint/build
- Check console errors in the browser
- Test mobile viewport (375px) for UI changes

playwright.config.ts β€” with auto dev-server

import { defineConfig } from '@playwright/test';
 
export default defineConfig({
  testDir: './e2e',
  timeout: 30000,
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: true,  // won't double-start if already running
    timeout: 60000,
  },
});

The webServer block is the key: Playwright auto-starts the dev server before running tests β€” Claude Code does not need to manage it manually.

Base smoke test β€” e2e/smoke.spec.ts

import { test, expect } from '@playwright/test';
 
test('homepage loads without errors', async ({ page }) => {
  const errors: string[] = [];
  page.on('console', msg => {
    if (msg.type() === 'error') errors.push(msg.text());
  });
 
  await page.goto('http://localhost:3000');
  await expect(page).toHaveTitle(/.+/);
  expect(errors).toHaveLength(0);
});
 
test('homepage looks correct on mobile', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 812 });
  await page.goto('http://localhost:3000');
  await page.screenshot({ path: 'test-results/mobile-home.png' });
});

Run tests

npx playwright test          # headless
npx playwright test --headed # with browser open

Key Options / Variants

  • reuseExistingServer: true β€” skip dev server startup if it’s already running (fast re-runs)
  • screenshot: 'only-on-failure' β€” saves screenshots only when a test fails
  • video: 'retain-on-failure' β€” saves video recordings of failed tests only

Gotchas

  • Without webServer, Claude Code will start the dev server separately and forget to clean it up
  • npx playwright test after npm run build catches both build errors and runtime errors
  • Console error assertion (expect(errors).toHaveLength(0)) catches React hydration mismatches

Source

Conversation β€œApp-Testing” β€” 2026-06-01