Playwright·Cookbook Field Manual
Part 01 · Network Mocking Card 10

Per-Test Overrides

Card 10: Per-Test Overrides (Error Scenarios)

What This Pattern Solves

Most tests need the same “happy path” mock (200 + valid data). But you also need to test error scenarios: 500 errors, 404s, timeouts, malformed responses. Duplicating the happy-path setup in every test is wasteful. Instead, set up a default handler once, then override it in specific tests that need different behavior.

How It Works

  1. Register a default route handler in beforeEach (200 + happy path data)
  2. Most tests just navigate—they get the default mock automatically
  3. Error tests register a second route handler BEFORE navigating (same URL pattern)
  4. Playwright’s “last registered runs first” means the override takes precedence
  5. Test asserts on error UI behavior
  6. Each test gets a fresh page/context, so overrides don’t leak

This is the DRY approach to scenario testing: one default, many overrides.

Code Example

import { test, expect } from '@playwright/test';
import type { SwapiPerson } from '../swapi/schema';

const luke: SwapiPerson = {
  name: 'Luke Skywalker',
  height: '172',
  mass: '77',
  url: 'https://swapi.dev/api/people/1/',
  films: [],
};

test.describe('10-per-test-overrides: Scenario-based route override', () => {
  // Default: happy path for all tests
  test.beforeEach(async ({ page }) => {
    await page.route('**/swapi.dev/api/people/1/**', (route) =>
      route.fulfill({ json: luke }),
    );
  });

  test('GET people/1 returns 200 and person by default', async ({ page }) => {
    await page.goto('/cards/10');
    await expect(page.getByTestId('person-name')).toHaveText('Luke Skywalker');
  });

  test('handles SWAPI 500 when overridden', async ({ page }) => {
    // Override: register BEFORE goto
    await page.route('**/swapi.dev/api/people/1/**', (route) =>
      route.fulfill({ status: 500, body: '' }),
    );

    await page.goto('/cards/10');

    await expect(page.getByTestId('error')).toBeVisible();
    await expect(page.getByTestId('error')).toContainText('500');
  });

  test('handles SWAPI 404 when overridden', async ({ page }) => {
    await page.route('**/swapi.dev/api/people/1/**', (route) =>
      route.fulfill({ status: 404, body: 'Not Found' }),
    );

    await page.goto('/cards/10');

    await expect(page.getByTestId('error')).toBeVisible();
    await expect(page.getByTestId('error')).toContainText('404');
  });

  test('handles network timeout via route.abort', async ({ page }) => {
    await page.route('**/swapi.dev/api/people/1/**', (route) =>
      route.abort('timedout'),
    );

    await page.goto('/cards/10');

    await expect(page.getByTestId('error')).toBeVisible();
  });
});

Run This Example

pnpm test src/10-per-test-overrides

Prerequisites

Key Concepts

When to Use This Pattern

Common Mistakes

  1. Overriding AFTER navigation (too late):

    // ❌ WRONG - page already loaded with default mock
    await page.goto('/');
    await page.route('**/*', errorHandler);
    
    // ✓ CORRECT - override before navigation
    await page.route('**/*', errorHandler);
    await page.goto('/');
  2. Not understanding route order:

    // Routes run in reverse order of registration
    await page.route('**/*', defaultHandler);  // Runs SECOND
    await page.route('**/*', errorHandler);    // Runs FIRST (overrides)
  3. Forgetting default can still match:

    • If override pattern is narrower, default might still run
    • Make override pattern match or be broader than default
  4. Sharing mutable state (if using shared context):

    // ❌ WRONG - shared context, routes leak between tests
    const context = await browser.newContext();
    
    // ✓ CORRECT - each test gets fresh page from fixture
    test('...', async ({ page }) => {
      // page is fresh per test
    });

Flow Diagram

sequenceDiagram
    participant beforeEach
    participant Test1 (default)
    participant Test2 (override)
    participant Page

    Note over beforeEach: Register default: 200 + Luke

    beforeEach->>Page: route('**/people/1/**', 200 handler)

    Test1 (default)->>Page: goto('/')
    Page->>Page: fetch /people/1/ → default handler runs
    Page-->>Test1 (default): Renders Luke ✓

    beforeEach->>Page: route('**/people/1/**', 200 handler)
    Test2 (override)->>Page: route('**/people/1/**', 500 handler)
    Note over Page: Last registered runs FIRST
    Test2 (override)->>Page: goto('/')
    Page->>Page: fetch /people/1/ → 500 handler runs (override!)
    Page-->>Test2 (override): Shows error UI ✓

Live Demo

👇 Tests override default mocks for error scenarios:

Loading…

Run This Example

pnpm test src/10-per-test-overrides