Test Suite Structure
Table of Contents
- Configuration
- E2E Tests
- Component Tests
- API Tests
- Visual Regression Tests
- Directory Structure
- Tagging & Filtering
Project Setup
npm init playwright@latestConfiguration
Essential Configuration
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [["html"], ["list"]],
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{ name: "setup", testMatch: /.*\.setup\.ts/ },
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
dependencies: ["setup"],
},
],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});E2E Tests
Full user journey tests through the browser.
Structure
// tests/e2e/checkout.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Checkout Flow", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/products");
});
test("complete purchase as guest", async ({ page }) => {
// Add to cart
await page.getByRole("button", { name: "Add to Cart" }).first().click();
await expect(page.getByTestId("cart-count")).toHaveText("1");
// Go to checkout
await page.getByRole("link", { name: "Cart" }).click();
await page.getByRole("button", { name: "Checkout" }).click();
// Fill shipping
await page.getByLabel("Email").fill("[email protected]");
await page.getByLabel("Address").fill("123 Test St");
await page.getByRole("button", { name: "Continue" }).click();
// Payment
await page.getByLabel("Card Number").fill("4242424242424242");
await page.getByRole("button", { name: "Pay Now" }).click();
// Confirmation
await expect(page.getByRole("heading")).toHaveText("Order Confirmed");
});
test("apply discount code", async ({ page }) => {
await page.getByRole("button", { name: "Add to Cart" }).first().click();
await page.getByRole("link", { name: "Cart" }).click();
await page.getByLabel("Discount Code").fill("SAVE10");
await page.getByRole("button", { name: "Apply" }).click();
await expect(page.getByText("10% discount applied")).toBeVisible();
});
});Best Practices
- Test critical user journeys
- Keep tests independent
- Use realistic data
- Clean up test data in teardown
Component Tests
Test individual components in isolation using Playwright Component Testing.
npm init playwright@latest -- --ctFor comprehensive component testing patterns including mounting, props, events, slots, mocking, and framework-specific examples (React, Vue, Svelte), see component-testing.md.
API Tests
Test backend APIs without browser.
API Mocking Patterns
For E2E tests that need to mock API responses:
// Mock single endpoint
test("displays mocked users", async ({ page }) => {
await page.route("**/api/users", (route) =>
route.fulfill({
status: 200,
json: [{ id: 1, name: "Test User" }],
})
);
await page.goto("/users");
await expect(page.getByText("Test User")).toBeVisible();
});
// Mock with different responses
test("handles API errors", async ({ page }) => {
await page.route("**/api/users", (route) =>
route.fulfill({
status: 500,
json: { error: "Server error" },
})
);
await page.goto("/users");
await expect(page.getByText("Server error")).toBeVisible();
});
// Conditional mocking
test("mocks based on request", async ({ page }) => {
await page.route("**/api/users", (route, request) => {
if (request.method() === "GET") {
route.fulfill({ json: [{ id: 1, name: "User" }] });
} else {
route.continue();
}
});
});
// Mock with delay (simulate slow network)
test("handles slow API", async ({ page }) => {
await page.route("**/api/data", (route) =>
route.fulfill({
json: { data: "test" },
delay: 2000, // 2 second delay
})
);
await page.goto("/dashboard");
await expect(page.getByText("Loading...")).toBeVisible();
await expect(page.getByText("test")).toBeVisible();
});For advanced patterns (GraphQL mocking, HAR recording, request modification, network throttling), see network-advanced.md.
Visual Regression Tests
Compare screenshots to detect visual changes.
Basic Visual Test
// tests/visual/homepage.spec.ts
import { test, expect } from "@playwright/test";
test("homepage visual", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveScreenshot("homepage.png");
});
test("component visual", async ({ page }) => {
await page.goto("/components");
const button = page.getByRole("button", { name: "Primary" });
await expect(button).toHaveScreenshot("primary-button.png");
});Visual Test Options
test("dashboard visual", async ({ page }) => {
await page.goto("/dashboard");
await expect(page).toHaveScreenshot("dashboard.png", {
fullPage: true, // Capture entire scrollable page
maxDiffPixels: 100, // Allow up to 100 different pixels
maxDiffPixelRatio: 0.01, // Or 1% difference
threshold: 0.2, // Pixel comparison threshold
animations: "disabled", // Disable animations
mask: [page.getByTestId("date")], // Mask dynamic content
});
});Handling Dynamic Content
test("page with dynamic content", async ({ page }) => {
await page.goto("/profile");
// Mask elements that change
await expect(page).toHaveScreenshot("profile.png", {
mask: [
page.getByTestId("timestamp"),
page.getByTestId("avatar"),
page.getByRole("img"),
],
});
});
// Or hide elements via CSS
test("page hiding dynamic elements", async ({ page }) => {
await page.goto("/profile");
await page.addStyleTag({
content: `
.dynamic-content { visibility: hidden !important; }
[data-testid="ad-banner"] { display: none !important; }
`,
});
await expect(page).toHaveScreenshot("profile-stable.png");
});Visual Test Configuration
// playwright.config.ts
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 50,
animations: "disabled",
},
},
projects: [
{
name: "visual-chrome",
use: {
...devices["Desktop Chrome"],
viewport: { width: 1280, height: 720 },
},
testMatch: /.*visual.*\.spec\.ts/,
},
],
});Update Snapshots
# Update all snapshots
npx playwright test --update-snapshots
# Update specific test
npx playwright test homepage.spec.ts --update-snapshotsDirectory Structure
tests/
├── e2e/ # End-to-end tests
│ ├── auth.spec.ts
│ ├── checkout.spec.ts
│ └── dashboard.spec.ts
├── component/ # Component tests
│ ├── Button.spec.tsx
│ └── Modal.spec.tsx
├── api/ # API tests
│ ├── users.spec.ts
│ └── products.spec.ts
├── visual/ # Visual regression tests
│ └── homepage.spec.ts
├── fixtures/ # Custom fixtures
│ ├── auth.fixture.ts
│ └── api.fixture.ts
└── pages/ # Page objects
├── login.page.ts
└── dashboard.page.tsAnti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Long test files | Hard to maintain, slow to navigate | Split by feature, use POM |
| Tests depend on execution order | Flaky, hard to debug | Keep tests independent |
| Testing multiple features in one test | Hard to debug failures | One feature per test |
Related References
- Component Testing: See component-testing.md for comprehensive CT patterns
- Projects: See projects-dependencies.md for project-based filtering
- Page Objects: See page-object-model.md for organizing page interactions
- Test Data: See fixtures-hooks.md for managing test data
Tagging & Filtering
Using Tags
test("user login @smoke @auth", async ({ page }) => {
// ...
});
test("checkout flow @e2e @critical", async ({ page }) => {
// ...
});
test.describe("API tests @api", () => {
test("create user", async ({ request }) => {
// ...
});
});Running Tagged Tests
# Run smoke tests
npx playwright test --grep @smoke
# Run all except slow tests
npx playwright test --grep-invert @slow
# Combine tags
npx playwright test --grep "@smoke|@critical"For project-based filtering and advanced project configuration, see projects-dependencies.md.