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: The interceptor must be set up before the network request occurs.
Implementation:
// INCORRECT - interceptor set up too late
await page.goto('https://example.com'); // Request already happened
const networkCall = interceptNetworkCall({ url: '**/api/data' });
await networkCall; // Will hang indefinitely!
// CORRECT - Set up interception first
const networkCall = interceptNetworkCall({ url: '**/api/data' });
await page.goto('https://example.com');
const result = await networkCall;
This pattern follows the classic test spy/stub pattern:
Context: Intercepting different endpoints in same test - setup order is critical.
Implementation:
test('multiple intercepts', async ({ page, interceptNetworkCall }) => {
// 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 callsContext: Each interceptNetworkCall captures only the first matching request.
Implementation:
// Capturing a known number of requests
const firstRequest = interceptNetworkCall({ url: '/api/data' });
const secondRequest = interceptNetworkCall({ url: '/api/data' });
await page.click('#load-data-button');
const firstResponse = await firstRequest;
const secondResponse = await secondRequest;
expect(firstResponse.status).toBe(200);
expect(secondResponse.status).toBe(200);
// Handling an unknown number of requests
const getDataRequestInterceptor = () =>
interceptNetworkCall({
url: '/api/data',
timeout: 1000, // Short timeout to detect when no more requests are coming
});
let currentInterceptor = getDataRequestInterceptor();
const allResponses = [];
await page.click('#load-multiple-data-button');
while (true) {
try {
const response = await currentInterceptor;
allResponses.push(response);
currentInterceptor = getDataRequestInterceptor();
} catch (error) {
// No more requests (timeout)
break;
}
}
console.log(`Captured ${allResponses.length} requests to /api/data`);
Context: Set a timeout for waiting on a network request.
Implementation:
const dataCall = interceptNetworkCall({
method: 'GET',
url: '/api/data-that-might-be-slow',
timeout: 5000, // 5 seconds timeout
});
await page.goto('/data-page');
try {
const { responseJson } = await dataCall;
console.log('Data loaded successfully:', responseJson);
} catch (error) {
if (error.message.includes('timeout')) {
console.log('Request timed out as expected');
} else {
throw error;
}
}
The utility uses picomatch for powerful glob pattern matching, dramatically simplifying URL targeting:
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
Comparison with vanilla Playwright:
// Vanilla Playwright - complex predicate
const predicate = (response) => {
const url = response.url();
return (
url.endsWith('/api/users') ||
url.match(/\/api\/users\/\d+/) ||
(url.includes('/api/users/') && url.includes('/profile'))
);
};
page.waitForResponse(predicate);
// With interceptNetworkCall - simple glob patterns
interceptNetworkCall({ url: '/api/users' }); // Exact endpoint
interceptNetworkCall({ url: '/api/users/*' }); // User by ID pattern
interceptNetworkCall({ url: '/api/users/*/profile' }); // Specific sub-paths
interceptNetworkCall({ url: '/api/users/**' }); // Match all
interceptNetworkCall(options)| Parameter | Type | Description |
|---|---|---|
page |
Page |
Required when using direct import (not needed with fixture) |
method |
string |
Optional: HTTP method to match (e.g., 'GET', 'POST') |
url |
string |
Optional: URL pattern to match (supports glob patterns via picomatch) |
fulfillResponse |
object |
Optional: Response to use when mocking |
handler |
function |
Optional: Custom handler function for the route |
timeout |
number |
Optional: Timeout in milliseconds for the network request |
fulfillResponse Object| Property | Type | Description |
|---|---|---|
status |
number |
HTTP status code (default: 200) |
headers |
Record<string, string> |
Response headers |
body |
any |
Response body (will be JSON.stringified if an object) |
Returns a Promise<NetworkCallResult> with:
| Property | Type | Description |
|---|---|---|
request |
Request |
The intercepted request |
response |
Response |
The response (null if mocked) |
responseJson |
any |
Parsed JSON response (if available) |
status |
number |
HTTP status code |
requestJson |
any |
Parsed JSON request body (if available) |
| 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 basicsDON'T intercept after navigation:
await page.goto('/dashboard'); // Navigation starts
const usersCall = interceptNetworkCall({ url: '**/api/users' }); // Too late!
DO intercept before navigate:
const usersCall = interceptNetworkCall({ url: '**/api/users' }); // First
await page.goto('/dashboard'); // Then navigate
const { responseJson } = await usersCall; // Then await
DON'T ignore the returned Promise:
interceptNetworkCall({ url: '**/api/users' }); // Not awaited!
await page.goto('/dashboard');
// No deterministic wait - race condition
DO always await the intercept:
const usersCall = interceptNetworkCall({ url: '**/api/users' });
await page.goto('/dashboard');
await usersCall; // Deterministic wait