Keep/_bmad/bmm/testarch/knowledge/intercept-network-call.md

8.1 KiB

Intercept Network Call Utility

Principle

Intercept network requests with a single declarative call that returns a Promise. Automatically parse JSON responses, support both spy (observe) and stub (mock) patterns, and use powerful glob pattern matching for URL filtering.

Rationale

Vanilla Playwright's network interception requires multiple steps:

  • page.route() to setup, page.waitForResponse() to capture
  • Manual JSON parsing
  • Verbose syntax for conditional handling
  • Complex filter predicates

The interceptNetworkCall utility provides:

  • Single declarative call: Setup and wait in one statement
  • Automatic JSON parsing: Response pre-parsed, strongly typed
  • Flexible URL patterns: Glob matching with picomatch
  • Spy or stub modes: Observe real traffic or mock responses
  • Concise API: Reduces boilerplate by 60-70%

Pattern Examples

Example 1: Spy on Network (Observe Real Traffic)

Context: Capture and inspect real API responses for validation.

Implementation:

import { test } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';

test('should spy on users API', async ({ page, interceptNetworkCall }) => {
  // Setup interception BEFORE navigation
  const usersCall = interceptNetworkCall({
    url: '**/api/users', // Glob pattern
  });

  await page.goto('/dashboard');

  // Wait for response and access parsed data
  const { responseJson, status } = await usersCall;

  expect(status).toBe(200);
  expect(responseJson).toHaveLength(10);
  expect(responseJson[0]).toHaveProperty('name');
});

Key Points:

  • Intercept before navigation (critical for race-free tests)
  • Returns Promise with { responseJson, status, requestBody }
  • Glob patterns (** matches any path segment)
  • JSON automatically parsed

Example 2: Stub Network (Mock Response)

Context: Mock API responses for testing UI behavior without backend.

Implementation:

test('should stub users API', async ({ page, interceptNetworkCall }) => {
  const mockUsers = [
    { id: 1, name: 'Test User 1' },
    { id: 2, name: 'Test User 2' },
  ];

  const usersCall = interceptNetworkCall({
    url: '**/api/users',
    fulfillResponse: {
      status: 200,
      body: mockUsers,
    },
  });

  await page.goto('/dashboard');
  await usersCall;

  // UI shows mocked data
  await expect(page.getByText('Test User 1')).toBeVisible();
  await expect(page.getByText('Test User 2')).toBeVisible();
});

Key Points:

  • fulfillResponse mocks the API
  • No backend needed
  • Test UI logic in isolation
  • Status code and body fully controllable

Example 3: Conditional Response Handling

Context: Different responses based on request method or parameters.

Implementation:

test('conditional mocking', async ({ page, interceptNetworkCall }) => {
  await interceptNetworkCall({
    url: '**/api/data',
    handler: async (route, request) => {
      if (request.method() === 'POST') {
        // Mock POST success
        await route.fulfill({
          status: 201,
          body: JSON.stringify({ id: 'new-id', success: true }),
        });
      } else if (request.method() === 'GET') {
        // Mock GET with data
        await route.fulfill({
          status: 200,
          body: JSON.stringify([{ id: 1, name: 'Item' }]),
        });
      } else {
        // Let other methods through
        await route.continue();
      }
    },
  });

  await page.goto('/data-page');
});

Key Points:

  • handler function for complex logic
  • Access full route and request objects
  • Can mock, continue, or abort
  • Flexible for advanced scenarios

Example 4: Error Simulation

Context: Testing error handling in UI when API fails.

Implementation:

test('should handle API errors gracefully', async ({ page, interceptNetworkCall }) => {
  // Simulate 500 error
  const errorCall = interceptNetworkCall({
    url: '**/api/users',
    fulfillResponse: {
      status: 500,
      body: { error: 'Internal Server Error' },
    },
  });

  await page.goto('/dashboard');
  await errorCall;

  // Verify UI shows error state
  await expect(page.getByText('Failed to load users')).toBeVisible();
  await expect(page.getByTestId('retry-button')).toBeVisible();
});

// Simulate network timeout
test('should handle timeout', async ({ page, interceptNetworkCall }) => {
  await interceptNetworkCall({
    url: '**/api/slow',
    handler: async (route) => {
      // Never respond - simulates timeout
      await new Promise(() => {});
    },
  });

  await page.goto('/slow-page');

  // UI should show timeout error
  await expect(page.getByText('Request timed out')).toBeVisible({ timeout: 10000 });
});

Key Points:

  • Mock error statuses (4xx, 5xx)
  • Test timeout scenarios
  • Validate error UI states
  • No real failures needed

Example 5: Multiple Intercepts (Order Matters!)

Context: Intercepting different endpoints in same test - setup order is critical.

Implementation:

test('multiple intercepts', async ({ page, interceptNetworkCall }) => {
  // ✅ CORRECT: Setup all intercepts BEFORE navigation
  const usersCall = interceptNetworkCall({ url: '**/api/users' });
  const productsCall = interceptNetworkCall({ url: '**/api/products' });
  const ordersCall = interceptNetworkCall({ url: '**/api/orders' });

  // THEN navigate
  await page.goto('/dashboard');

  // Wait for all (or specific ones)
  const [users, products] = await Promise.all([usersCall, productsCall]);

  expect(users.responseJson).toHaveLength(10);
  expect(products.responseJson).toHaveLength(50);
});

Key Points:

  • Setup all intercepts before triggering actions
  • Use Promise.all() to wait for multiple calls
  • Order: intercept → navigate → await
  • Prevents race conditions

URL Pattern Matching

Supported glob patterns:

'**/api/users'; // Any path ending with /api/users
'/api/users'; // Exact match
'**/users/*'; // Any users sub-path
'**/api/{users,products}'; // Either users or products
'**/api/users?id=*'; // With query params

Uses picomatch library - same pattern syntax as Playwright's page.route() but cleaner API.

Comparison with Vanilla Playwright

Vanilla Playwright intercept-network-call
await page.route('/api/users', route => route.continue()) const call = interceptNetworkCall({ url: '**/api/users' })
const resp = await page.waitForResponse('/api/users') (Combined in single statement)
const json = await resp.json() const { responseJson } = await call
const status = resp.status() const { status } = await call
Complex filter predicates Simple glob patterns

Reduction: ~5-7 lines → ~2-3 lines per interception

  • network-first.md - Core pattern: intercept before navigate
  • network-recorder.md - HAR-based offline testing
  • overview.md - Fixture composition basics

Anti-Patterns

Intercepting after navigation:

await page.goto('/dashboard'); // Navigation starts
const usersCall = interceptNetworkCall({ url: '**/api/users' }); // Too late!

Intercept before navigate:

const usersCall = interceptNetworkCall({ url: '**/api/users' }); // First
await page.goto('/dashboard'); // Then navigate
const { responseJson } = await usersCall; // Then await

Ignoring the returned Promise:

interceptNetworkCall({ url: '**/api/users' }); // Not awaited!
await page.goto('/dashboard');
// No deterministic wait - race condition

Always await the intercept:

const usersCall = interceptNetworkCall({ url: '**/api/users' });
await page.goto('/dashboard');
await usersCall; // Deterministic wait