Network Interception Utility
- Network Interception Utility
The Network Interception utility provides a powerful way to observe, intercept, and mock network requests in Playwright tests. This utility significantly improves upon Playwright's built-in network handling capabilities by offering a more intuitive API, automatic response parsing, and a cleaner fixture-based approach.
Why Use This Instead of Native Playwright?
While Playwright offers built-in network interception via page.route() and page.waitForResponse(), our utility addresses several common pain points:
| Native Playwright | Our Network Interception Utility |
|---|---|
| Route setup and response waiting in separate steps | Single declarative call that returns a Promise |
Manual JSON parsing with await response.json() | Automatic JSON parsing with strongly-typed results |
| Complex filter predicates for response matching | Simple declarative options with powerful glob pattern matching using picomatch |
| Verbose syntax, especially for conditional handling | Concise, readable API with flexible handler options |
| Limited type safety for response data | Full TypeScript support with type assertions |
Comparison with Native Playwright
Spying on the network
With Native Playwright:
test('Spy on the network - original', async ({ page }) => {
// Set up the interception before navigating
await page.route('*/**/api/v1/fruits', (route) => route.continue())
await page.goto('https://demo.playwright.dev/api-mocking')
// Wait for the intercepted response
const fruitsResponse = await page.waitForResponse('*/**/api/v1/fruits')
// verify the network
const fruitsResponseBody = await fruitsResponse.json()
const status = fruitsResponse.status()
expect(fruitsResponseBody.length).toBeGreaterThan(0)
expect(status).toBe(200)
})With Our Utility:
test('Spy on the network', async ({ page, interceptNetworkCall }) => {
// Set up the interception before navigating
const fruitsResponse = interceptNetworkCall({
url: '**/fruits'
})
await page.goto('https://demo.playwright.dev/api-mocking')
// Wait for the intercepted response
const { responseJson, status } = await fruitsResponse
// verify the network
expect(responseJson.length).toBeGreaterThan(0)
expect(status).toBe(200)
})Stubbing the network
With Native Playwright:
test('Stub the network - original', async ({ page }) => {
const fruit = { name: 'Guava', id: 12 }
// Set up the interception before navigating
await page.route('*/**/api/v1/fruits', (route) =>
route.fulfill({
json: [fruit]
})
)
await page.goto('https://demo.playwright.dev/api-mocking')
// Wait for the intercepted response
const fruitsResponse = await page.waitForResponse('*/**/api/v1/fruits')
// verify the network
const fruitsResponseBody = await fruitsResponse.json()
expect(fruitsResponseBody).toEqual([fruit])
await expect(page.getByText(fruit.name)).toBeVisible()
})With Our Utility:
test('Stub the network', async ({ page, interceptNetworkCall }) => {
const fruit = { name: 'Guava', id: 12 }
// Set up the interception before navigating
const fruitsResponse = interceptNetworkCall({
url: '/api/*/fruits', // just a specificity on '**/fruits'
fulfillResponse: {
body: [fruit]
}
})
await page.goto('https://demo.playwright.dev/api-mocking')
// Wait for the intercepted response
const { responseJson } = await fruitsResponse
// verify the network
expect(responseJson).toEqual([fruit])
await expect(page.getByText(fruit.name)).toBeVisible()
})URL Pattern Matching Simplification
One of the most significant improvements our utility offers is the use of picomatch for URL pattern matching. This dramatically simplifies how you target specific network requests:
With Native Playwright:
// Complex predicate with multiple conditions to match similar URLs
const predicate = (response) => {
const url = response.url()
return (
// Match exact endpoint
url.endsWith('/api/users') ||
// Match user by ID pattern
url.match(/\/api\/users\/\d+/) ||
// Match specific subpaths
(url.includes('/api/users/') && url.includes('/profile'))
)
}
// Have to use this complex predicate in every listener
page.waitForResponse(predicate)With Our Utility:
// Simple, readable glob patterns to match the same URLs
interceptNetworkCall({ url: '/api/users' }) // Exact endpoint
interceptNetworkCall({ url: '/api/users/*' }) // User by ID pattern
interceptNetworkCall({ url: '/api/users/*/profile' }) // Specific sub-paths
// Or even match all of them with a single pattern
interceptNetworkCall({ url: '/api/users/**' })This makes tests more maintainable, less error-prone, and much easier to read and understand.
Usage
Direct Import
The network interception utility works by setting up interceptions that return promises which resolve when the network call is made:
import { interceptNetworkCall } from '@seontechnologies/playwright-utils'
import { test } from '@playwright/test'
test('intercept example', async ({ page }) => {
// Set up interception before navigating
const dataCall = interceptNetworkCall({
page,
method: 'GET',
url: '/api/data',
fulfillResponse: {
status: 200,
body: { data: [{ id: 1, name: 'Test Item' }] }
}
})
// Navigate to the page that will trigger the network call
await page.goto('https://example.com')
// Wait for the network call to complete and access the result
const { responseJson, status } = await dataCall
})As a Fixture
The fixture version simplifies your test code by automatically injecting the page context:
import { test } from '@seontechnologies/playwright-utils/fixtures'
test('intercept fixture example', async ({ page, interceptNetworkCall }) => {
// Set up interception - notice 'page' is not needed in the options
const dataCall = interceptNetworkCall({
method: 'GET',
url: '/api/data',
fulfillResponse: {
status: 200,
body: { data: [] }
}
})
// Navigate to the page
await page.goto('https://example.com')
// Wait for the network call to complete
await dataCall
// You can also access the response data with type assertions
const {
responseJson: { data }
} = await dataCall
})API Reference
interceptNetworkCall(options)
The main function to intercept network requests.
Options
| Parameter | Type | Description |
|---|---|---|
page | Page | Required: Playwright page object |
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) |
Return Value
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) |
URL Pattern Matching
Under the hood, this utility uses picomatch for powerful glob pattern matching of URLs. This makes it easy to match URLs using patterns like:
'/api/users'- Exact path matching'**/users/**'- Match any URL containing 'users''/api/users/*'- Match all endpoints under users
Glob patterns are much more concise and readable than complex regex or function predicates required by native Playwright.
Examples
Observing a Network Request
// Set up the interception before triggering the request
const usersCall = interceptNetworkCall({
page,
method: 'GET',
url: '/api/users'
})
// Trigger the request (for example, by navigation or user action)
await page.goto('/users-page')
// Wait for the request to complete and get the result
const result = await usersCall
// Work with the response (with type assertion for better type safety)
const {
status,
responseJson: { data }
} = (await usersCall) as { status: number; responseJson: { data: User[] } }
expect(status).toBe(200)
expect(data).toHaveLength(10)Mocking a Response
const mockUserData = { id: 1, name: 'Test User', email: 'test@example.com' }
// Set up the mock before navigation
const userCall = interceptNetworkCall({
page,
method: 'GET',
url: '/api/users/1',
fulfillResponse: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: { data: mockUserData }
}
})
// Navigate to a page that would call this API
await page.goto('/user/1')
// Optionally wait for the mock to be used
await userCall
// The page will receive the mocked data instead of making a real API callUsing a Custom Handler
If the response need to be customized / partially modified:
// Set up a handler for dynamic request processing
const loginCall = interceptNetworkCall({
page,
url: '/api/login', // Note: no method specified to catch all methods
handler: async (route, request) => {
if (request.method() === 'POST') {
const data = JSON.parse(request.postData() || '{}')
if (data.username === 'testuser' && data.password === 'password') {
await route.fulfill({
status: 200,
body: JSON.stringify({ token: 'fake-token-123' })
})
} else {
await route.fulfill({
status: 401,
body: JSON.stringify({ error: 'Invalid credentials' })
})
}
} else {
// Allow other methods to pass through
await route.continue()
}
}
})
// Perform login action
await page.fill('#username', 'testuser')
await page.fill('#password', 'password')
await page.click('#login-button')
// Wait for the login call to complete
await loginCallUsing URL Patterns
The utility supports glob patterns for URL matching:
// Match any URL containing 'users'
await interceptNetworkCall({
page,
url: '**/users/**',
fulfillResponse: {
/* ... */
}
})
// Match exact API endpoint
await interceptNetworkCall({
page,
url: '/api/v1/products',
method: 'GET',
fulfillResponse: {
/* ... */
}
})Order Matters
Order matters significantly when using network interception. The interceptor must be set up before the network request occurs. If you set up the interception after the page is already loaded and the network request has completed, the interceptor won't catch it.
❌ Incorrect Approach
// THIS WON'T WORK - interceptor set up too late
await page.goto('https://example.com') // Request already happened
// Too late - the network call already occurred
const networkCall = interceptNetworkCall({
url: '**/api/data'
})
// This will hang indefinitely waiting for a request that already completed
await networkCall✅ Correct Approach
// CORRECT - Set up interception first
const networkCall = interceptNetworkCall({
url: '**/api/data'
})
// Then trigger the network request
await page.goto('https://example.com')
// Then wait for completion
const result = await networkCallThis pattern follows the classic test spy/stub pattern:
- Define the spy/stub (set up interception)
- Perform the action (trigger the network request)
- Assert on the spy/stub (await and verify the response)
Capturing Multiple Requests to the Same Endpoint
Capturing a Known Number of Requests
By default, each interceptNetworkCall captures only the first matching request. When the same endpoint is hit multiple times, you need multiple interceptors to capture each occurrence.
// First interceptor for the first request
const firstRequest = interceptNetworkCall({
url: '/api/data'
})
// Second interceptor for the second request
const secondRequest = interceptNetworkCall({
url: '/api/data'
})
// Trigger actions that cause multiple requests
await page.click('#load-data-button')
// Wait for and process each request in sequence
const firstResponse = await firstRequest
const secondResponse = await secondRequest
// Now you can verify both responses
expect(firstResponse.status).toBe(200)
expect(secondResponse.status).toBe(200)Handling an Unknown Number of Requests
// Function to get a fresh interceptor
// We use a function here because each call to interceptNetworkCall()
// creates a NEW interceptor that watches for the NEXT matching request
const getDataRequestInterceptor = () =>
interceptNetworkCall({
url: '/api/data',
timeout: 1000 // Short timeout to detect when no more requests are coming
})
// Initial interceptor
let currentInterceptor = getDataRequestInterceptor()
// Array to collect all responses
const allResponses = []
// Trigger the action that causes requests
await page.click('#load-multiple-data-button')
// Collect responses until there are no more or timeout
while (true) {
try {
// Wait with a short timeout to see if there's another request
const response = await currentInterceptor
allResponses.push(response)
// Set up another interceptor for potential next request
currentInterceptor = getDataRequestInterceptor()
} catch (error) {
// No more requests (timeout)
break
}
}
console.log(`Captured ${allResponses.length} requests to /api/data`)Error Simulation
Simulate error responses for testing error handling:
// Set up an error response simulation
const errorCall = interceptNetworkCall({
page,
method: 'GET',
url: '/api/data',
fulfillResponse: {
status: 500,
body: { error: 'Internal Server Error' }
}
})
// Navigate to page that will trigger the API call
await page.goto('/data-page')
// Wait for the error response
await errorCall
// Verify error handling in the UI
await expect(page.locator('.error-message')).toBeVisible()Using Timeout
// Set a timeout for waiting on a network request
const dataCall = interceptNetworkCall({
page,
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 // Unexpected error
}
}