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.
Vanilla Playwright's network interception requires multiple steps:
page.route() to setup, page.waitForResponse() to captureThe interceptNetworkCall utility provides:
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:
{ responseJson, status, requestBody }** matches any path segment)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 APIContext: 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 logicroute and request objectsContext: 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:
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:
Promise.all() to wait for multiple callsSupported 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.
| 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 navigatenetwork-recorder.md - HAR-based offline testingoverview.md - Fixture composition basics❌ 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