Day 6: Network Control and Mocking
What You Will Learn Today
- Monitoring network requests with page.on('request') and page.on('response')
- Waiting for network events: waitForRequest(), waitForResponse()
- Route interception with page.route()
- Mocking API responses with route.fulfill()
- Modifying requests with route.continue()
- Aborting requests with route.abort()
- HAR recording and replay with page.routeFromHAR()
- Emulating network conditions (offline, slow 3G)
- API testing with request context (request.get(), request.post())
- Hands-on: testing with a mocked backend
The Network Control Landscape
Playwright provides a rich set of APIs for complete control over browser network communication.
flowchart TB
subgraph Monitor["Monitoring"]
OnReq["page.on('request')"]
OnRes["page.on('response')"]
end
subgraph Wait["Waiting"]
WaitReq["waitForRequest()"]
WaitRes["waitForResponse()"]
end
subgraph Intercept["Interception"]
Route["page.route()"]
Fulfill["route.fulfill()"]
Continue["route.continue()"]
Abort["route.abort()"]
end
subgraph Advanced["Advanced"]
HAR["routeFromHAR()"]
Offline["Offline\nEmulation"]
API["API Testing\nrequest context"]
end
Monitor --> Wait --> Intercept --> Advanced
style Monitor fill:#3b82f6,color:#fff
style Wait fill:#8b5cf6,color:#fff
style Intercept fill:#f59e0b,color:#fff
style Advanced fill:#22c55e,color:#fff
Monitoring Network Requests
page.on('request') and page.on('response')
You can observe all network requests and responses in real time as they occur on the page.
import { test, expect } from '@playwright/test';
test('monitor network requests', async ({ page }) => {
const requests: string[] = [];
// All requests
page.on('request', (request) => {
console.log(`>> ${request.method()} ${request.url()}`);
requests.push(request.url());
});
// All responses
page.on('response', (response) => {
console.log(`<< ${response.status()} ${response.url()}`);
});
await page.goto('https://example.com');
expect(requests.length).toBeGreaterThan(0);
});
Detecting Failed Requests
page.on('requestfailed', (request) => {
console.log(`FAILED: ${request.url()} - ${request.failure()?.errorText}`);
});
Waiting for Requests
waitForRequest() and waitForResponse()
You can wait for specific requests or responses to occur before proceeding.
test('wait for specific API call', async ({ page }) => {
// Wait by URL string
const responsePromise = page.waitForResponse('**/api/users');
await page.goto('/users');
const response = await responsePromise;
expect(response.status()).toBe(200);
const data = await response.json();
expect(data).toHaveLength(3);
});
Conditional Waiting
test('wait with predicate', async ({ page }) => {
// Wait with a predicate function
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/api/users') &&
response.status() === 200
);
await page.click('#load-users');
const response = await responsePromise;
const users = await response.json();
expect(users.length).toBeGreaterThan(0);
});
Waiting for POST Requests
test('wait for POST request', async ({ page }) => {
const requestPromise = page.waitForRequest(
(request) =>
request.url().includes('/api/users') &&
request.method() === 'POST'
);
await page.fill('#name', 'John Smith');
await page.click('#submit');
const request = await requestPromise;
const postData = request.postDataJSON();
expect(postData.name).toBe('John Smith');
});
Route Interception with page.route()
Basic Usage
page.route() is the central API for intercepting requests and choosing to mock, modify, or abort them.
flowchart LR
Browser["Browser"] --> |"Request"| Route["page.route()"]
Route --> |"fulfill()"| Mock["Mock Response"]
Route --> |"continue()"| Server["Server\n(modified)"]
Route --> |"abort()"| Block["Blocked"]
style Browser fill:#3b82f6,color:#fff
style Route fill:#f59e0b,color:#fff
style Mock fill:#8b5cf6,color:#fff
style Server fill:#22c55e,color:#fff
style Block fill:#ef4444,color:#fff
URL Pattern Matching
// Glob patterns
await page.route('**/api/users', handler);
await page.route('**/api/users/*', handler);
// Regular expressions
await page.route(/\/api\/users\/\d+/, handler);
// Predicate function
await page.route(
(url) => url.pathname.startsWith('/api/'),
handler
);
route.fulfill() - Mocking Responses
Returning JSON Responses
test('mock API response', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'John Smith', email: 'john@example.com' },
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' },
]),
});
});
await page.goto('/users');
await expect(page.locator('.user-card')).toHaveCount(2);
await expect(page.locator('.user-card').first()).toContainText('John Smith');
});
Simulating Error Responses
test('handle 500 error', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/users');
await expect(page.locator('.error-message')).toBeVisible();
});
Returning Responses from Files
test('mock with file', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
path: 'tests/fixtures/users.json',
});
});
await page.goto('/users');
});
route.continue() - Modifying Requests
You can forward requests to the server while modifying headers, URLs, or other properties.
test('modify request headers', async ({ page }) => {
await page.route('**/api/**', async (route) => {
await route.continue({
headers: {
...route.request().headers(),
'X-Custom-Header': 'test-value',
'Authorization': 'Bearer mock-token',
},
});
});
await page.goto('/dashboard');
});
URL Rewriting
test('rewrite API URL', async ({ page }) => {
// Redirect staging API to local API
await page.route('**/api.staging.example.com/**', async (route) => {
const url = route.request().url().replace(
'api.staging.example.com',
'localhost:3001'
);
await route.continue({ url });
});
});
Modifying Responses
test('modify response data', async ({ page }) => {
await page.route('**/api/users', async (route) => {
const response = await route.fetch();
const json = await response.json();
// Add a test user to the response
json.push({ id: 999, name: 'Test User', email: 'test@example.com' });
await route.fulfill({
response,
body: JSON.stringify(json),
});
});
await page.goto('/users');
});
route.abort() - Blocking Requests
Block unnecessary requests (images, analytics, etc.) to speed up tests.
test('block images and analytics', async ({ page }) => {
await page.route('**/*.{png,jpg,jpeg,gif,svg}', (route) => route.abort());
await page.route('**/analytics/**', (route) => route.abort());
await page.route('**/ads/**', (route) => route.abort());
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
Filtering by Resource Type
await page.route('**/*', async (route) => {
const resourceType = route.request().resourceType();
if (['image', 'font', 'stylesheet'].includes(resourceType)) {
await route.abort();
} else {
await route.continue();
}
});
| Use Case for abort() | Description |
|---|---|
| Speed up tests | Skip loading unnecessary resources |
| Remove external dependencies | Block third-party scripts |
| Error testing | Simulate network errors |
HAR Recording and Replay
What is HAR?
HAR (HTTP Archive) is a format for recording browser network traffic. You can capture real API responses and replay them during tests.
flowchart LR
subgraph Record["Recording Phase"]
R1["Real API traffic"] --> R2["Save to HAR file"]
end
subgraph Replay["Replay Phase"]
P1["Load HAR file"] --> P2["Return recorded responses"]
end
Record --> Replay
style Record fill:#3b82f6,color:#fff
style Replay fill:#22c55e,color:#fff
Recording HAR
test('record HAR', async ({ page }) => {
// Start recording
await page.routeFromHAR('tests/fixtures/api.har', {
update: true, // Record mode
url: '**/api/**',
});
await page.goto('/dashboard');
await page.click('#load-data');
await page.waitForResponse('**/api/data');
});
Replaying HAR
test('replay from HAR', async ({ page }) => {
// Replay mode (default)
await page.routeFromHAR('tests/fixtures/api.har', {
url: '**/api/**',
});
await page.goto('/dashboard');
await expect(page.locator('.data-table')).toBeVisible();
});
Emulating Network Conditions
Offline Mode
test('offline mode', async ({ page, context }) => {
await page.goto('/');
// Go offline
await context.setOffline(true);
await page.click('#load-data');
await expect(page.locator('.offline-message')).toBeVisible();
// Go back online
await context.setOffline(false);
await page.click('#retry');
await expect(page.locator('.data-loaded')).toBeVisible();
});
Slow Network Simulation
You can emulate slow connections using CDP (Chrome DevTools Protocol) with Chromium-based browsers.
test('slow 3G simulation', async ({ page }) => {
const cdpSession = await page.context().newCDPSession(page);
await cdpSession.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: (500 * 1024) / 8, // 500kb/s
uploadThroughput: (500 * 1024) / 8,
latency: 400, // 400ms RTT
});
await page.goto('/');
// Slow loading behavior can be verified here
});
API Testing with Request Context
Playwright can test APIs directly without a browser, using the built-in request context.
Basic API Tests
import { test, expect } from '@playwright/test';
test.describe('API Tests', () => {
const BASE_URL = 'https://jsonplaceholder.typicode.com';
test('GET /users', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`);
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const users = await response.json();
expect(users).toHaveLength(10);
expect(users[0]).toHaveProperty('name');
});
test('POST /posts', async ({ request }) => {
const response = await request.post(`${BASE_URL}/posts`, {
data: {
title: 'Test Post',
body: 'This is a test.',
userId: 1,
},
});
expect(response.status()).toBe(201);
const post = await response.json();
expect(post.title).toBe('Test Post');
});
test('PUT /posts/1', async ({ request }) => {
const response = await request.put(`${BASE_URL}/posts/1`, {
data: {
title: 'Updated Title',
body: 'Updated body.',
userId: 1,
},
});
expect(response.ok()).toBeTruthy();
const post = await response.json();
expect(post.title).toBe('Updated Title');
});
test('DELETE /posts/1', async ({ request }) => {
const response = await request.delete(`${BASE_URL}/posts/1`);
expect(response.ok()).toBeTruthy();
});
});
Authenticated API Tests
test('authenticated API request', async ({ request }) => {
// Login to get token
const loginResponse = await request.post('/api/login', {
data: { email: 'user@example.com', password: 'password123' },
});
const { token } = await loginResponse.json();
// Use token for subsequent requests
const response = await request.get('/api/profile', {
headers: { Authorization: `Bearer ${token}` },
});
expect(response.ok()).toBeTruthy();
const profile = await response.json();
expect(profile.email).toBe('user@example.com');
});
Hands-On: Testing with a Mocked Backend
Complete TODO App Test Suite
import { test, expect, Page } from '@playwright/test';
const mockTodos = [
{ id: 1, title: 'Go shopping', completed: false },
{ id: 2, title: 'Clean the house', completed: true },
{ id: 3, title: 'Cook dinner', completed: false },
];
async function setupMockAPI(page: Page) {
// GET /api/todos
await page.route('**/api/todos', async (route) => {
if (route.request().method() === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTodos),
});
} else if (route.request().method() === 'POST') {
const body = route.request().postDataJSON();
await route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({ id: 4, ...body }),
});
}
});
// PUT or DELETE /api/todos/:id
await page.route('**/api/todos/*', async (route) => {
if (route.request().method() === 'PUT') {
const body = route.request().postDataJSON();
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(body),
});
} else if (route.request().method() === 'DELETE') {
await route.fulfill({ status: 204, body: '' });
}
});
}
test.describe('TODO App with Mocked Backend', () => {
test.beforeEach(async ({ page }) => {
await setupMockAPI(page);
await page.goto('/todos');
});
test('display todo list', async ({ page }) => {
await expect(page.locator('.todo-item')).toHaveCount(3);
await expect(page.locator('.todo-item').first()).toContainText('Go shopping');
});
test('add new todo', async ({ page }) => {
await page.fill('#new-todo', 'Study');
await page.click('button.add');
const response = await page.waitForResponse('**/api/todos');
expect(response.status()).toBe(201);
});
test('show error on server failure', async ({ page }) => {
// Override the POST handler with an error
await page.route('**/api/todos', async (route) => {
if (route.request().method() === 'POST') {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server Error' }),
});
} else {
await route.continue();
}
});
await page.fill('#new-todo', 'Test');
await page.click('button.add');
await expect(page.locator('.error-message')).toBeVisible();
});
});
Summary
| Category | API | Purpose |
|---|---|---|
| Monitor | page.on('request') |
Observe outgoing requests |
| Monitor | page.on('response') |
Observe incoming responses |
| Wait | waitForRequest() |
Wait for a specific request |
| Wait | waitForResponse() |
Wait for a specific response |
| Mock | route.fulfill() |
Return a mocked response |
| Modify | route.continue() |
Forward with modifications |
| Block | route.abort() |
Block a request entirely |
| HAR | routeFromHAR() |
Record and replay traffic |
| Offline | context.setOffline() |
Simulate offline mode |
| API Test | request.get() etc. |
Test APIs without a browser |
Key Takeaways
- page.route() is the core - Playwright's network control revolves around page.route() with three actions: fulfill, continue, and abort
- Stabilize tests with mocks - Avoid backend dependencies by using deterministic mock responses for reliable tests
- Speed up with route.abort() - Block unnecessary resources like images and analytics to improve test execution time
- Use HAR for real data - Record and replay actual API responses to maintain realistic test data without manual fixture creation
- Combine browser and API tests - Use the request context for fast API-level checks alongside browser-based E2E tests
Exercises
Basics
- Use
page.route()androute.fulfill()to return a mock response for a GET request - Use
waitForResponse()to wait for a specific API response and verify its status code and body - Use
route.abort()to block image requests and compare the difference in test speed
Intermediate
- Use
route.continue()to add a custom authentication token header to requests - Mock error responses (404, 500) and test the error handling behavior of the UI
- Use
context.setOffline(true)to test offline mode and verify recovery after reconnection
Challenge
- Implement HAR file recording and replay using
routeFromHAR() - Build a CRUD API test suite using the
requestcontext and combine it with browser-based tests
References
Next Up
In Day 7, you will learn about Fixtures and Page Object Model. You will master Playwright's fixture system and the Page Object Model pattern to dramatically improve the reusability and maintainability of your test code.