8Network & Advanced
What you will master here
page.route— abort, continue, fulfill, fallback- Mocking JSON responses, errors and slow networks
waitForResponse/waitForRequestand the promise-first pattern- HAR replay and recording
- Trace Viewer — the time-travel debugger
- Debugging:
--debug,--ui,codegen,show-trace
8.1 Intercepting network — page.route
Every network request a Page (or the whole BrowserContext) makes can be intercepted. You can let it through, modify it, or fake the response entirely.
// Intercept all requests to /api/users — fulfill with a fake JSON
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]),
});
});
// Now navigate — the network call never reaches the server
await page.goto('/users');
await expect(page.getByText('Alice')).toBeVisible();
The four route actions
| Method | What it does |
|---|---|
route.fulfill() | Reply with a fake response. No network call goes out. |
route.continue() | Let the request proceed — optionally modified (different URL, headers, postData). |
route.abort('failed') | Cancel the request, simulating a network failure / blocked resource. |
route.fallback() | Defer to the next matching route handler. Used to layer handlers. |
Block tracking scripts
await context.route('**/*', route => {
const url = route.request().url();
if (url.includes('google-analytics') || url.includes('hotjar')) {
return route.abort();
}
return route.continue();
});
Block by resource type (fast page loads in tests)
await context.route('**/*', route => {
const block = ['image', 'font', 'media'];
return block.includes(route.request().resourceType()) ? route.abort() : route.continue();
});
Modify request body before sending
await page.route('**/api/orders', async route => {
const req = route.request();
const body = JSON.parse(req.postData() || '{}');
body.coupon = 'TEST50'; // inject coupon
await route.continue({
postData: JSON.stringify(body),
headers: { ...req.headers(), 'x-test': '1' },
});
});
Modify response after the server replies
await page.route('**/api/products', async route => {
const response = await route.fetch();
const json = await response.json();
json.forEach((p: any) => (p.price = 0)); // make everything free
await route.fulfill({ response, json });
});
**/api/**), regex (page.route(/\/api\/users/, …)), or a predicate function.8.2 Simulating errors and slow networks
// 500 server error
await page.route('**/api/checkout', route => route.fulfill({
status: 500,
body: JSON.stringify({ message: 'Internal error' }),
}));
// Network failure
await page.route('**/api/checkout', route => route.abort('failed'));
// Slow response — 5 second latency before responding
await page.route('**/api/products', async route => {
await new Promise(r => setTimeout(r, 5000));
await route.continue();
});
// Test should still pass — UI must show loading spinner
8.3 Waiting for requests/responses (promise-first)
Use waitForResponse when you need to assert against a network call (e.g. "the POST went through and returned 201"). Always use the Promise.all pattern so you start listening before the action triggers the call.
const [response] = await Promise.all([
page.waitForResponse(r => r.url().endsWith('/api/orders') && r.request().method() === 'POST'),
page.getByRole('button', { name: 'Place order' }).click(),
]);
expect(response.status()).toBe(201);
const body = await response.json();
expect(body.orderId).toMatch(/ORD-/);
Same for waitForRequest — useful when you want to assert what the UI sent (payload, headers).
const [request] = await Promise.all([
page.waitForRequest('**/track'),
page.getByRole('button', { name: 'Track' }).click(),
]);
expect(request.postDataJSON()).toMatchObject({ event: 'cta_click' });
8.4 The promise-first pattern, deeply explained
Browser events (popups, downloads, requests, responses) can fire synchronously with the action that triggers them. If you act first and then await the event, the event may have already fired and your listener missed it.
Broken — race condition
// May miss the event if the popup opens before waitForEvent registers
await page.click('#open');
const popup = await context.waitForEvent('page'); // could hang or miss
Correct — promise-first
// Listener attached before click runs
const [popup] = await Promise.all([
context.waitForEvent('page'),
page.click('#open'),
]);
Promise.all kicks off all promises in the array before awaiting any of them. The waitForEvent listener is registered first, then the click runs — event firing is guaranteed to be observed.
8.5 HAR — record and replay real traffic
A HAR file is a JSON capture of every HTTP request/response on a page. Playwright can record one, and later replay tests against it (zero real network).
// Record
await context.routeFromHAR('flow.har', { update: true, updateMode: 'minimal' });
await page.goto('/checkout');
// ...do real flow against real backend
await context.close(); // writes flow.har
// Replay (CI, offline)
await context.routeFromHAR('flow.har', { url: '**/api/**', notFound: 'fallback' });
await page.goto('/checkout');
// network is served from HAR — fast, deterministic
8.6 The Trace Viewer — time-travel debugging
When a test fails, Playwright can save a trace — a zip with: DOM snapshots before/after every action, screenshots, network log, console log, source mapping. You open it in the Trace Viewer and "scrub" through your test.
// In playwright.config.ts
use: {
trace: 'on-first-retry', // generate trace only when a test is retried
}
Open a trace
npx playwright show-trace test-results/.../trace.zip
What you get
- Action timeline — click, fill, navigate, each with start/end
- Before/after DOM snapshots — see exactly what the page looked like at each action
- Locator tab — hover any element in the snapshot to see the locator that would target it
- Network tab — every request, headers, body, status, timing
- Console tab — browser console output
- Source tab — your test code with the failing line highlighted
8.7 Debugging tools
Headed mode
npx playwright test --headed # show the browser
UI mode — the killer feature
npx playwright test --ui
Opens a desktop-app-like runner where you can: pick tests, watch them step by step, scrub the timeline, inspect the DOM at every action, re-run on save. The single best feature for local development.
Inspector / step debugger
npx playwright test --debug # or set PWDEBUG=1 npx playwright test
Pauses at the first line, shows the Playwright Inspector (commands, locator picker, step button). You can also embed await page.pause() inside a test to break there.
Codegen — record a test
npx playwright codegen https://example.com
Opens a browser; everything you click/type is converted to a Playwright test you can copy. Great for boilerplate.
show-trace
npx playwright show-trace test-results/.../trace.zip # or directly via the HTML report npx playwright show-report
Run a single test
npx playwright test tests/login.spec.ts -g "happy path" npx playwright test --project=firefox npx playwright test --workers=1 --headed
8.8 Putting it together — mocked API test
test('checkout shows server error gracefully', async ({ page }) => {
await page.route('**/api/checkout', route => route.fulfill({
status: 500,
body: JSON.stringify({ message: 'Payment provider down' }),
}));
await page.goto('/cart');
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page.getByRole('alert')).toContainText('Something went wrong');
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
});
Module 4 — Interview Q&A bank
What is page.route?
fulfill (fake the response), continue (let it proceed, optionally modified), abort (simulate failure), or fallback (defer to the next matching route).When would you mock an API in an E2E test?
How do you block third-party trackers in tests?
route.request().url(), abort if it includes known tracking hosts.How do you simulate a slow network response?
setTimeout before continue, or use context.routeFromHAR with delayed responses. For full-page throttling, use browser.newContext({ ... }) with a CDP session and Network.emulateNetworkConditions.Difference between waitForRequest and waitForResponse?
Why must waitForResponse be paired with Promise.all?
Promise.all registers the listener and triggers the action in the same microtask — guaranteed to capture the event.What is a HAR file?
routeFromHAR with update: true) and replay tests from it later. Useful for offline / deterministic CI runs.What is the Trace Viewer and what's inside a trace?
trace: 'on-first-retry' (or always) is set.How do you open a trace?
npx playwright show-trace path/to/trace.zip, or open the HTML report (npx playwright show-report) and click the trace icon on a failed test.What does --ui do?
What's the difference between --debug and --ui?
--debug opens the Playwright Inspector and pauses at start (or at page.pause()) for step-by-step execution. --ui is a richer interactive runner with traces and watch mode. Use --debug when you need to step through code, --ui for general dev.What does codegen do?
Can you modify a request before it's sent?
route.continue({ url, method, headers, postData }) with the overrides. Useful for injecting test-only headers (e.g. x-test-mode: 1) or rewriting URLs to a mock server.Can you modify a response after it arrives?
await route.fetch(), mutate the body, then route.fulfill({ response, body: newBody }). Useful for forcing specific edge cases on top of real data.How do you debug a failing test on CI when you can't reproduce locally?
trace: 'on-first-retry' (or 'retain-on-failure') is set, download the trace artifact from CI, then npx playwright show-trace locally. You'll see the exact DOM, network and console at the moment of failure.How do you assert that a button triggered the correct API call?
page.waitForRequest or waitForResponse: pre-register the wait, click the button, then assert on the captured request's URL/method/body.What's the difference between page.route and context.route?
page.route applies to a single page; context.route applies to every page in the context (including popups). Use context for cross-page concerns like tracker blocking.Why might a route handler "miss" a request?
** at start). Use a regex to debug: log route.request().url() for every request and confirm patterns.What's an "on-first-retry" trace and why is it the default?
How do you handle Service Workers / SW caches in tests?
use: { serviceWorkers: 'block' } to disable them entirely (cleaner for tests).