Network interception and mocking
Fulfil, modify and abort requests, record a HAR for replay, and assert on what the page actually sent.
Fulfilling and aborting
// mock the response before the page navigates
await page.route("**/api/orders", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ orders: [{ id: "1042", total: 4200 }] }),
});
});
await page.goto("/orders");
await expect(page.getByRole("row")).toHaveCount(1);
// remove the route when you are done with it
await page.unroute("**/api/orders");// let the real request happen, but reshape the response
await page.route("**/api/user", async (route) => {
const response = await route.fetch();
const json = await response.json();
await route.fulfill({
response,
json: { ...json, plan: "enterprise" },
});
});
// simulate an outage
await page.route("**/api/recommendations", (route) => route.abort("failed"));
// force a 500 to test the error state
await page.route("**/api/checkout", (route) =>
route.fulfill({ status: 500, json: { error: { code: "UPSTREAM" } } })
);
// pass through to the next matching handler or the network
await page.route("**/api/**", (route) => route.fallback());| Method | Effect |
|---|---|
route.fulfill() | Return a synthetic response |
route.continue() | Send the request on, optionally modified |
route.abort() | Fail the request as if the network dropped |
route.fallback() | Defer to the next matching handler |
route.fetch() | Perform the request yourself and inspect it |
routeFromHAR() | Serve responses from a recorded archive |
⚠️
unroute takes the same pattern you registered. If you register with a regular expression and unroute with a string, the handler stays installed and every later test inherits your mock. Prefer a per-test fixture that routes and cleans up automatically.Record once, replay always
// record: run with the real network, write the archive
await page.routeFromHAR("hars/orders.har", {
url: "**/api/**",
update: true,
});
// replay: no network needed for the recorded endpoints
await page.routeFromHAR("hars/orders.har", {
url: "**/api/**",
notFound: "abort", // or "fallback" to hit the real network
});- A HAR makes a test fast, deterministic and independent of a backend - at the cost of drifting from the real API.
- Use
update: truedeliberately and review the archive diff; a re-recorded HAR is a change to the test's fixtures and deserves the same review as the test itself. - HAR files can contain credentials and personal data. Redact or generate them from a seeded environment before committing.
- Keep HAR-based tests for UI behaviour, and keep a smaller number of tests that hit the real service to detect contract drift.
Asserting on what was sent
// capture the request the page makes
const [request] = await Promise.all([
page.waitForRequest((r) => r.url().includes("/api/orders") && r.method() === "POST"),
page.getByRole("button", { name: "Place order" }).click(),
]);
expect(request.postDataJSON()).toMatchObject({
items: [{ sku: "A-1", quantity: 2 }],
currency: "GBP",
});
// and the response it got
const [response] = await Promise.all([
page.waitForResponse((r) => r.url().endsWith("/api/orders") && r.status() === 201),
page.getByRole("button", { name: "Place order" }).click(),
]);
const body = await response.json();
expect(body.orderId).toMatch(/^ORD-/);- Start the wait before the action that triggers the request, or you will wait for something that already happened.
- Prefer
waitForResponseover a hard wait; it is the condition, expressed exactly. - Assert on the request body when the payload is the contract you care about - a UI assertion cannot check what was sent.
- Log the status in the assertion message so a failure tells you whether the call failed or the payload was wrong.
// a fixture that fails the test on any unexpected 5xx
export const test = base.extend({
page: async ({ page }, use) => {
const failures = [];
page.on("response", (r) => {
if (r.status() >= 500) failures.push(r.status() + " " + r.url());
});
await use(page);
expect(failures, "unexpected server errors").toEqual([]);
},
});FAQ
Should I mock the API or use the real one?
Mock for UI behaviour - loading states, error states, empty states, unusual payloads - because those are hard to produce from a real backend. Use the real API for the happy-path flows that must keep working against the deployed service, so contract changes are caught.
Why is my route handler not being called?
Most often the route was registered after the request was already made. Register routes before the navigation or action that triggers the request. Also check the pattern: glob patterns are matched against the full URL, so
**/api/** is usually what you mean.Related
API testing with request contexts Auto-waiting, timeouts and flakiness
Last refreshed 2026-09-18.