Network & Advanced

8Network & Advanced

What you will master here

  • page.route — abort, continue, fulfill, fallback
  • Mocking JSON responses, errors and slow networks
  • waitForResponse / waitForRequest and 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

MethodWhat 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 });
});
Pattern matchingRoute patterns use glob (**/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
When HAR shinesSnapshot a complex API conversation once, then run tests against the snapshot offline. Eliminates "the staging API is down" failures.

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

Playwright Trace Viewer DOM snapshot — BEFORE click Email input Pass input Sign in action timeline + network log goto /login fill email + password click "Sign in" ← failed POST /api/login (401)
Figure 6 — Trace Viewer: scrub the timeline, see DOM before/after each action, network beside it.

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?
A handler that intercepts network requests matching a glob or regex. Inside the handler you decide: 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?
When the test's purpose is the UI's reaction to specific server states (errors, edge cases, rare conditions) rather than full end-to-end validation. Mocking removes the dependency on a live backend and produces fast, deterministic tests. Use real backend for happy-path coverage.
How do you block third-party trackers in tests?
A context-level route handler: match all URLs, inspect route.request().url(), abort if it includes known tracking hosts.
How do you simulate a slow network response?
Either route the request and add a 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?
waitForRequest fires when the browser sends a matching request — useful for asserting payload/headers. waitForResponse fires when the response arrives — useful for asserting status/body. They are independent: a request may fire without a response yet.
Why must waitForResponse be paired with Promise.all?
Because the response may arrive before your listener is attached. Promise.all registers the listener and triggers the action in the same microtask — guaranteed to capture the event.
What is a HAR file?
HTTP Archive — JSON capture of all HTTP request/response pairs for a session. Playwright can record one (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?
A desktop-app-like UI that shows everything that happened during a test: action timeline, DOM snapshot before+after each action, screenshots, network log, console, source. The trace.zip is generated by Playwright when 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?
Opens UI mode — an interactive runner: pick tests, watch them step by step, hover the timeline to see DOM at each step, re-run on file save. The best local dev workflow.
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?
Opens a browser and records your clicks/typing as Playwright test code in real time. Good for scaffolding new tests or learning the API — but always clean up the generated locators (prefer getByRole over the CSS it sometimes picks).
Can you modify a request before it's sent?
Yes — in a route handler, call 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?
Yes — fetch the real response via 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?
Ensure 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?
Promise.all with 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?
Same API; scope differs. 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?
Typically the handler was registered after the request fired (race), or the URL pattern doesn't match (e.g. forgetting ** 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?
Generate a trace only when a test fails and is being retried. Cheap (most tests pass first time, no trace cost) but you still get a trace for every actual failure on CI.
How do you handle Service Workers / SW caches in tests?
By default Playwright bypasses Service Worker caching. Configure with use: { serviceWorkers: 'block' } to disable them entirely (cleaner for tests).