Write end-to-end tests with Playwright Test for full user flow verification. Consult this skill whenever testing complete user flows, verifying multi-page interactions, testing authentication flows, or creating E2E tests that exercise the full application stack.
Install
npx skillscat add kvnwolf/devtools/create-e2e-test Install via the SkillsCat registry.
E2E Tests
Write end-to-end tests with Playwright Test for full user flow verification. E2E tests exercise the real application — real server, real browser, real data.
Convention
E2E tests live in e2e/ at the project root, NOT co-located with source files. They are separate from unit and component tests.
Run with bun run test:e2e (not included in validate — run separately).
Imports
import { expect, test } from "@playwright/test";Core Patterns
Navigation
test("navigates to about page", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "About" }).click();
await expect(page).toHaveURL("/about");
await expect(page.getByRole("heading")).toHaveText("About Us");
});Form Submission
test("creates a new user", async ({ page }) => {
await page.goto("/users/new");
await page.getByLabel("Name").fill("Alice");
await page.getByLabel("Email").fill("alice@test.com");
await page.getByRole("button", { name: "Create" }).click();
await expect(page).toHaveURL(/\/users\/.+/);
await expect(page.getByText("Alice")).toBeVisible();
});Data CRUD Flow
test("creates, edits, and deletes an item", async ({ page }) => {
// Create
await page.goto("/items");
await page.getByRole("button", { name: "New Item" }).click();
await page.getByLabel("Name").fill("Test Item");
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Test Item")).toBeVisible();
// Edit
await page.getByText("Test Item").click();
await page.getByLabel("Name").fill("Updated Item");
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Updated Item")).toBeVisible();
// Delete
await page.getByRole("button", { name: "Delete" }).click();
await page.getByRole("button", { name: "Confirm" }).click();
await expect(page.getByText("Updated Item")).not.toBeVisible();
});Auth Persistence
Login once and reuse the session across tests:
// e2e/auth.setup.ts
import { test as setup } from "@playwright/test";
const AUTH_FILE = "e2e/.auth/user.json";
setup("authenticate", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("test@test.com");
await page.getByLabel("Password").fill("password");
await page.getByRole("button", { name: "Sign in" }).click();
await page.waitForURL("/dashboard");
await page.context().storageState({ path: AUTH_FILE });
});Configure in playwright.config.ts:
export default defineConfig({
projects: [
{ name: "setup", testMatch: /auth\.setup\.ts/ },
{
name: "tests",
dependencies: ["setup"],
use: { storageState: "e2e/.auth/user.json" },
},
],
});When E2E vs Component Test
| Scenario | Test Type |
|---|---|
| Multi-page user flow | E2E |
| Server-side rendering verification | E2E |
| Authentication and authorization | E2E |
| Isolated UI component behavior | Component test |
| Form validation feedback | Component test |
| Component state management | Component test |
| API integration across pages | E2E |
Rule of thumb: If the test needs the real server or navigates between pages, use E2E. If it tests isolated UI behavior, use a component test.