Files
FancyWordle/.agents/skills/playwright-best-practices/core/test-suite-structure.md
T
2026-05-16 13:57:02 -04:00

9.3 KiB

Test Suite Structure

Table of Contents

  1. Configuration
  2. E2E Tests
  3. Component Tests
  4. API Tests
  5. Visual Regression Tests
  6. Directory Structure
  7. Tagging & Filtering

Project Setup

npm init playwright@latest

Configuration

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("guest@example.com");
    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 -- --ct

For 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-snapshots

Directory 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.ts

Anti-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

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.