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: YesCLAUDE.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 changesplaywright.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 openKey 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 failsvideo: '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 testafternpm run buildcatches both build errors and runtime errors- Console error assertion (
expect(errors).toHaveLength(0)) catches React hydration mismatches
Source
Conversation βApp-Testingβ β 2026-06-01