Skip to main content
Watch: Claude Code Just Killed After Effects

Next.js & Web

E2E Route Testing with Playwright

Use when adding end-to-end coverage to a Next.js app with Playwright, especially a route smoke suite proving every public page renders. Covers config, a data-driven route check, auth-gated routes, and keeping the suite deterministic with real waits instead of sleeps. Also use when a suite is flaky, or a page 500s in production that no test covered. If a route can break without a test noticing, use this skill.

playwrighte2etestingciroutes

1 file

Description

Use when adding end-to-end coverage to a Next.js app with Playwright, especially a route smoke suite proving every public page renders. Covers config, a data-driven route check, auth-gated routes, and keeping the suite deterministic with real waits instead of sleeps. Also use when a suite is flaky, or a page 500s in production that no test covered. If a route can break without a test noticing, use this skill.

Install
SKILL.md

name: e2e-testing-playwright description: Use when adding end-to-end coverage to a Next.js app with Playwright, especially a route smoke suite that proves every public page renders. Covers config, a data-driven route check, auth-gated routes, and keeping the suite deterministic.

E2E Route Testing with Playwright

When to trigger

Wiring the first Playwright suite, adding a route-coverage gate to CI, or a deploy that shipped a 500 page a unit test never saw.

The highest-value suite is route coverage

Before elaborate flow tests, prove every route returns a real page. One data-driven test catches the class of bug where a new page compiles but throws at request time.

import { test, expect } from "@playwright/test";

const PUBLIC_ROUTES = ["/", "/blog", "/library", "/library/skills", "/tools"];

for (const path of PUBLIC_ROUTES) {
  test(\`GET \${path} returns 200\`, async ({ request }) => {
    const res = await request.get(path);
    expect(res.status(), path).toBe(200);
  });
}

Generate the route list from the same source the app uses (the content loaders, the tools array) so a new entry is covered automatically instead of by hand.

Config that runs against a real server

Point webServer in playwright.config.ts at the app so the suite boots it:

export default defineConfig({
  webServer: { command: "pnpm build && pnpm start", url: "http://localhost:3000", reuseExistingServer: !process.env.CI },
  use: { baseURL: "http://localhost:3000" },
});

Test against a production build, not pnpm dev. The dev server hides errors (looser types, on-demand compile) that the built app surfaces.

Auth-gated routes

For pages behind auth, either expect the redirect (assert a 3xx to the sign-in URL) or seed a test session. Do not assert 200 on a protected route without a session; that hides a broken gate.

Pitfalls

  • page.waitForTimeout (a fixed sleep) makes the suite flaky and slow. Wait on waitForLoadState or an element instead.
  • Running the suite against the dev server passes locally and misses production-only build errors. Use the built app in the webServer command.
  • Hardcoding the route list rots the moment someone adds a page. Derive it from the app's own content sources.
  • Two builds racing in CI (the e2e build plus a separate typecheck build) collide on .next. Serialize them.
AI Development Stack

Get Smarter About AI Dev

New tutorials, open-source projects, and deep dives on coding agents - delivered weekly.

One email per weekReal code, not theoryFree forever