Skip to main content

Mock e network interception avanzati

Articolo della serie Playwright Component Testing Avanzato. Pubblicazione prevista 25 November 2027.

What do you think about this article?

Mock e network interception avanzati
Playwright ComponentsPhotos:SiddePexels

At a Glance

Articolo della serie Playwright Component Testing Avanzato. Pubblicazione prevista 25 November 2027.

Mock and advanced network interception

In Playwright component testing, the ability to control network resources is critical. When a component depends on external APIs, file servers, or third-party services, mocking them directly in the test reduces flakiness, improves execution speed, and enables testing error scenarios that would otherwise be difficult to reproduce. This fourth article in the series explores advanced strategies for network interception and HTTP mocking in Playwright, from built-in tools to sophisticated patterns like dynamic response rewriting and simulated rate limiting.

What You'll Learn

  • Difference between route interception and response mocking in Playwright
  • page.route() API to intercept HTTP requests and modify responses
  • Context-level interception vs page-level: pros and cons
  • Mocking JSON responses, binary data, and multipart form data
  • Simulating HTTP errors (5xx, timeouts, malformed responses)
  • Dynamic interception: based on URL, headers, body
  • Rate limiting and throttling in tests
  • Replay of pre-recorded responses (HAR - HTTP Archive)
  • Advanced patterns: handler chains, priority, and fallback
  • Best practices for readable and maintainable mocking

1. Network Interception in Playwright: fundamentals

Playwright offers two levels of network interception: route (at HTTP level) and network event (passive, tracking only). Route is the most powerful and flexible mechanism.

A route is a rule that intercepts outgoing requests from the browser and allows you to: respond directly from the test without touching the real server, modify the request before it reaches the server, record the request for later assertions, deliberately slow down the response, simulate network errors.

The route operates in two phases: request phase (before the browser sends), and response phase (before the browser receives). At the request level, you can inspect the method, headers, and body and decide to stop or modify. At the response level, you can intercept the real response from the server and modify it, or provide a dummy response without contacting the server.

1.1 page.route() - the fundamental pattern

// Snippet 1 - Basic interception of an API route
import { test, expect } from '@playwright/test';

test('intercept API and mock response', async ({ page }) => {
  // I register a route that intercepts ALL requests to /api/users
  await page.route('**/api/users', async route => {
    // I can inspect the request
    console.log('Intercepted request:', route.request().url());
    console.log('Method:', route.request().method());
    console.log('Headers:', route.request().headers());

    // I provide a mock response without contacting the real server
    await route.abort('internetdisconnected'); // simulate error
    // or:
    // await route.fulfill({
    //   status: 200,
    //   body: JSON.stringify({ id: 1, name: 'John Doe' })
    // });
  });

  // Now we navigate - the route will automatically intercept
  await page.goto('https://example.com');
  // ... assertions ...
});

2. Mocking patterns for common scenarios

2.1 JSON response with Content-Type header

// Snippet 2 - Mock well-formed JSON responses
await page.route('**/api/products', async route => {
  const products = [
    { id: 1, name: 'Product A', price: 29.99 },
    { id: 2, name: 'Product B', price: 49.99 }
  ];

  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify(products),
    headers: {
      'Cache-Control': 'no-cache',
      'X-Custom-Header': 'test'
    }
  });
});

2.2 Simulating HTTP errors

// Snippet 3 - HTTP errors: 5xx, timeouts, network errors
test('handle 500 error from server', async ({ page }) => {
  await page.route('**/api/payment', async route => {
    await route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({
        error: 'Internal Server Error',
        message: 'Database connection failed'
      })
    });
  });

  await page.goto('https://example.com/checkout');
  // Component should show graceful error message
  await expect(page.locator('[data-testid="error-message"]')).toContainText('Database connection failed');
});

test('simulate network timeout', async ({ page }) => {
  await page.route('**/api/slow-endpoint', async route => {
    // Wait 30 seconds, then interrupt with timeout
    await page.waitForTimeout(30000);
    await route.abort('timedout');
  });

  // Or use route.continue() with delay:
  // await route.continue({ timings: { requestStart: 15000 } });
});

3. Dynamic interception based on criteria

Often you don't want to intercept all requests to an endpoint, but only those that satisfy specific criteria. Playwright allows you to inspect request method, URL query string, body, and headers to decide behavior dynamically.

3.1 Routing based on HTTP method

// Snippet 4 - Different mocks for GET vs POST of same endpoint
await page.route('**/api/items', async route => {
  const request = route.request();

  if (request.method() === 'GET') {
    // GET: return list
    await route.fulfill({
      status: 200,
      body: JSON.stringify([
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' }
      ])
    });
  } else if (request.method() === 'POST') {
    // POST: return created object with generated ID
    const bodyString = route.request().postData();
    const bodyObject = JSON.parse(bodyString || '{}');

    await route.fulfill({
      status: 201,
      body: JSON.stringify({
        id: Math.random().toString(36).substr(2, 9),
        ...bodyObject,
        createdAt: new Date().toISOString()
      })
    });
  } else if (request.method() === 'DELETE') {
    // DELETE: success 204 No Content
    await route.fulfill({ status: 204 });
  } else {
    // Unsupported method
    await route.fulfill({
      status: 405,
      body: JSON.stringify({ error: 'Method Not Allowed' })
    });
  }
});

3.2 Routing based on query parameters

// Snippet 5 - Different mock based on query string filters
await page.route('**/api/users**', async route => {
  const url = new URL(route.request().url());
  const role = url.searchParams.get('role');
  const limit = url.searchParams.get('limit') || '10';

  let users = [
    { id: 1, name: 'Alice', role: 'admin' },
    { id: 2, name: 'Bob', role: 'user' },
    { id: 3, name: 'Charlie', role: 'moderator' }
  ];

  if (role) {
    users = users.filter(u => u.role === role);
  }

  users = users.slice(0, parseInt(limit));

  await route.fulfill({
    status: 200,
    body: JSON.stringify(users)
  });
});

4. Context-level vs page-level interception

Playwright allows you to set routes at two levels: context (applies to all pages in the context) and page (applies to one page only). Page-level routes have higher priority, so if a route exists at both context and page level, the page level wins.

// Snippet 6 - Context-level routes for reuse across pages
test.beforeEach(async ({ context }) => {
  // This route applies to ALL pages created in this context
  await context.route('**/api/**', async route => {
    console.log('Context-level route intercepted:', route.request().url());
    await route.continue(); // pass to real server
  });
});

test('page 1', async ({ page }) => {
  // I can add additional page-specific routes
  await page.route('**/api/users', async route => {
    // This wins over the context route
    await route.fulfill({ status: 200, body: '[]' });
  });

  await page.goto('https://example.com/page1');
});

test('page 2', async ({ page }) => {
  // Uses the context route (no override)
  await page.goto('https://example.com/page2');
});

5. Mocking binary files, images, and assets

Not all resources are JSON. Playwright allows you to mock images, PDFs, binary files, and other assets.

5.1 Mock a PNG image

// Snippet 7 - Mock image with Buffer body
import fs from 'fs';

test('mock profile picture', async ({ page }) => {
  // Read a small PNG image from the test file system
  const pngBuffer = fs.readFileSync('./tests/fixtures/profile-pic.png');

  await page.route('**/profile.png', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'image/png',
      body: pngBuffer
    });
  });

  await page.goto('https://example.com');
  const img = page.locator('img[alt="Profile"]');
  await expect(img).toBeVisible();
  // The image is served from the mock, not from the network
});

6. Rate limiting and simulated throttling

Sometimes you want to test component behavior when the network is slow or when the server applies rate limiting. Playwright allows you to simulate both scenarios within a mock.

// Snippet 8 - Rate limiting: reject after N requests
test('rate limiting', async ({ page }) => {
  let requestCount = 0;

  await page.route('**/api/search', async route => {
    requestCount++;

    if (requestCount > 3) {
      // After 3 requests, return 429 Too Many Requests
      await route.fulfill({
        status: 429,
        headers: { 'Retry-After': '5' },
        body: JSON.stringify({ error: 'Rate limit exceeded' })
      });
    } else {
      // Otherwise normal response
      await route.fulfill({
        status: 200,
        body: JSON.stringify({ results: ['item1', 'item2'] })
      });
    }
  });

  // Test here: verify component handles 429 gracefully
});

// Snippet 9 - Throttling: add artificial delay
await page.route('**/api/heavy-compute', async route => {
  // Simulate server taking 5 seconds to process
  await page.waitForTimeout(5000);

  await route.fulfill({
    status: 200,
    body: JSON.stringify({ result: 'computed value' })
  });
});

7. Replay of pre-recorded responses with HAR

Playwright supports the HAR (HTTP Archive) format to record and replay responses. This is useful when you want to test with real server responses, but offline.

// Snippet 10 - Recording responses to a HAR file
test.describe.configure({ mode: 'serial' });

test('record requests to HAR', async ({ page }) => {
  // Playwright automatically records ALL network request/response
  // to a HAR file when you use page.routeFromHAR()

  // First time: record
  await page.routeFromHAR('./tests/fixtures/app.har', {
    url: '**/api/**',
    update: 'missing' // record only requests not already present
  });

  await page.goto('https://example.com');
  // Perform actions that generate network requests
  await page.click('button[data-action="fetch-data"]');
  // All responses are saved to app.har
});

test('use recorded HAR', async ({ page }) => {
  // Later: replay offline without contacting the real server
  await page.routeFromHAR('./tests/fixtures/app.har', {
    url: '**/api/**'
  });

  await page.goto('https://example.com');
  // /api/** requests are served from the HAR file, not from the server
  // Test is deterministic and fast
});

8. Best practices for robust interception

Mocking the network is an art. Here are the patterns that keep tests readable and maintainable over time.

8.1 Fixture helpers for common routes

// Snippet 11 - Extract common routes into reusable fixtures
export async function mockAuthAPI(page) {
  await page.route('**/api/auth/login', async route => {
    const body = JSON.parse(route.request().postData() || '{}');
    if (body.email === 'test@example.com' && body.password === 'correct') {
      await route.fulfill({
        status: 200,
        body: JSON.stringify({ token: 'mock-jwt-token-xyz' })
      });
    } else {
      await route.fulfill({
        status: 401,
        body: JSON.stringify({ error: 'Unauthorized' })
      });
    }
  });

  await page.route('**/api/auth/logout', async route => {
    await route.fulfill({ status: 204 });
  });
}

export async function mockProductsAPI(page, products = []) {
  await page.route('**/api/products', async route => {
    await route.fulfill({
      status: 200,
      body: JSON.stringify(products.length > 0 ? products : [
        { id: 1, name: 'Default Product', price: 99 }
      ])
    });
  });
}

// Usage in tests:
test('login flow', async ({ page }) => {
  await mockAuthAPI(page);
  await mockProductsAPI(page);

  await page.goto('https://example.com/login');
  // ... test login ...
});

9. Conclusion and key takeaways

Network interception in Playwright is the key to reliable and fast tests. In summary: use page.route() to intercept HTTP at the route level, inspect request and response to implement dynamic mocking, distinguish between context-level (for reuse) and page-level (for specific tests), simulate HTTP errors, timeouts and rate limiting, and consider HAR for replay of real responses.

In the next (and final) article of the series we will tackle Accessibility Testing: how to verify that Playwright components comply with WCAG guidelines, how to use automated audit tools, and how to write accessibility tests that capture real-world issues.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects