E2E tests open a real browser, navigate, click buttons, and verify the rendered output. Use them for critical user journeys — signup, checkout, the main feature loop.
1npm init playwright@latestThe init wizard sets up:
tests/ folderplaywright.config.ts1// tests/home.spec.ts
2import { test, expect } from "@playwright/test";
3
4test("home page loads and shows hero", async ({ page }) => {
5 await page.goto("/");
6 await expect(page.getByRole("heading", { name: /learn anything/i })).toBeVisible();
7});1// tests/signup.spec.ts
2test("user signs up, enrolls, completes lesson 1", async ({ page }) => {
3 await page.goto("/signup");
4
5 const email = `test+${Date.now()}@example.com`;
6 await page.fill('[name="email"]', email);
7 await page.fill('[name="password"]', "SuperSafe!23");
8 await page.click('button:has-text("Create account")');
9
10 await expect(page).toHaveURL(/\/dashboard/);
11
12 await page.goto("/courses/nextjs-16-complete-developer-guide");
13 await page.click('button:has-text("Enroll")');
14 await expect(page.getByText(/enrolled/i)).toBeVisible();
15
16 await page.click('a:has-text("What\'s New in Next.js 16")');
17 await page.click('button:has-text("Mark complete")');
18 await expect(page.getByText(/lesson complete/i)).toBeVisible();
19});Reduces test time dramatically:
1// tests/setup/auth.setup.ts
2import { test as setup } from "@playwright/test";
3
4const STORAGE = "tests/.auth/user.json";
5
6setup("authenticate", async ({ page }) => {
7 await page.goto("/login");
8 await page.fill('[name="email"]', "test@example.com");
9 await page.fill('[name="password"]', "Password123!");
10 await page.click('button:has-text("Sign in")');
11 await page.waitForURL("/dashboard");
12 await page.context().storageState({ path: STORAGE });
13});In playwright.config.ts:
1projects: [
2 { name: "setup", testMatch: /.*\.setup\.ts/ },
3 {
4 name: "chromium",
5 use: { ...devices["Desktop Chrome"], storageState: "tests/.auth/user.json" },
6 dependencies: ["setup"],
7 },
8],Now every test starts already logged in.
1await page.route("**/api/courses", (route) => {
2 route.fulfill({ status: 200, json: [{ id: "1", title: "Stub" }] });
3});Mock flaky third-party calls for deterministic tests.
playwright.config.ts:
1webServer: {
2 command: "npm run dev",
3 url: "http://localhost:3000",
4 reuseExistingServer: !process.env.CI,
5},1npx playwright test # headless
2npx playwright test --headed # see the browser
3npx playwright test --ui # interactive runner
4npx playwright codegen localhost:3000 # record interactions to scaffold a test| Layer | E2E | Unit |
|---|---|---|
| Critical flows (signup, checkout) | ✅ | ❌ |
| Edge cases in validation | ❌ | ✅ |
| Visual regressions | Playwright visual | ❌ |
| Permission rules | ❌ | ✅ |
| Server Action happy paths | ✅ | ✅ |
Aim for 5–15 high-leverage E2E tests and dozens of unit tests. Don't try to E2E everything — they're slow.