Playwright interview question
How do you mock or modify network responses in Playwright, and when should you?
Short answer
Use page.route or context.route with a URL pattern, then route.fulfill to return a mock, route.continue to modify the request, or route.fetch to patch the real response. Mock error states, edge cases and unstable third parties, but keep some unmocked end-to-end tests to catch contract drift.
Answer
Playwright intercepts traffic with page.route() (one page) or context.route() (all pages in the context). The handler receives a Route and decides what to do: route.fulfill() returns a response you define without hitting the server, route.continue() sends the request on, optionally modified, route.fetch() performs the real request so you can patch the real response, and route.abort() simulates a network failure. I mock deliberately: to make hard-to-reproduce states testable and to isolate the UI from unstable dependencies, while keeping a set of unmocked tests that prove the real integration works.
Return a mocked response
import { test, expect } from '@playwright/test';
test('shows empty state when there are no orders', async ({ page }) => {
await page.route('**/api/orders*', (route) =>
route.fulfill({ status: 200, json: { items: [], total: 0 } }),
);
await page.goto('/orders');
await expect(page.getByText('You have no orders yet')).toBeVisible();
});
The route must be registered before the request is made, so set it up before goto or before the click that triggers it.
Error and slow paths
test('shows a retry banner when the API fails', async ({ page }) => {
await page.route('**/api/orders*', (route) =>
route.fulfill({ status: 503, json: { message: 'Service unavailable' } }),
);
await page.goto('/orders');
await expect(page.getByRole('alert')).toContainText('Try again');
});
test('handles network failure', async ({ page }) => {
await page.route('**/api/recommendations', (route) => route.abort('failed'));
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Recommended' })).toBeHidden();
});
These states are nearly impossible to produce reliably against a real backend, which is exactly why mocking them is valuable.
Patch the real response
await page.route('**/api/profile', async (route) => {
const response = await route.fetch();
const body = await response.json();
await route.fulfill({ response, json: { ...body, plan: 'enterprise' } });
});
This keeps the real data but forces one field, for example a feature flag or a long name to test truncation.
Modify the outgoing request
await page.route('**/api/**', (route) =>
route.continue({ headers: { ...route.request().headers(), 'x-test-run': 'nightly' } }),
);
Block noise
await context.route(/(google-analytics|doubleclick|hotjar)\./, (route) => route.abort());
Blocking analytics and ads speeds up tests and removes a source of flakiness.
Recording with HAR
page.routeFromHAR('fixtures/orders.har', { url: '**/api/**', update: false }) replays responses recorded from a real session. Run once with update: true to record. Useful for complex payloads, but recordings go stale, so refresh them deliberately.
Gotchas
- Service workers can serve requests before routing sees them. Set
serviceWorkers: 'block'inusewhen mocking apps with an active service worker. - When multiple routes match, the most recently registered handler runs first; use
route.fallback()to pass to the next handler. - Route patterns are globs by default;
**/api/ordersdoes not match a URL with a query string, hence the*in the examples, or use a regular expression or predicate. - To assert the app sent the right request, combine with
page.waitForRequestor inspectroute.request().postDataJSON().
When not to mock
If every UI test mocks the API, the suite can be green while the frontend and backend disagree about the contract. I keep a thin layer of true end-to-end tests on critical journeys, and cover the contract itself with API or contract tests. Mocks need maintenance too: shape them from shared types or schemas so drift becomes a compile or validation error.
What interviewers look for
- Correct API:
route,fulfill,continue,fetch,abort, registered before the request. - Good reasons to mock: error states, edge cases, third parties, speed.
- Awareness of over-mocking and contract drift.
- Practical details: HAR replay, service workers, handler order.
A strong answer covers
- Intercepts with page.route or context.route and a URL pattern
- Fulfils with mocked data via route.fulfill
- Modifies real traffic with route.continue or route.fetch
- Explains when to mock: errors, edge cases, third parties, determinism
- Warns about over-mocking and contract drift; mentions HAR or waitForResponse