11.10 — Network interception & API testing
Your E2E tests have a dependency problem: the BACKEND — slow, shared with the whole team, and its data changes under you. 10.6 taught the cure at function scale; today it scales to the browser: page.route intercepts the page’s network requests MID-FLIGHT and answers them with your canned data — a stub at the network boundary. Plus 9.7’s promised payoff: pure API tests in the same suite.
// intercept: when the page asks for products,
// WE answer — the server is never consulted
await page.route("**/api/products", (route) =>
route.fulfill({
json: [{ name: "Mug", price: 250 }],
})
);
await page.goto("/shop");
await expect(page.getByText("Mug")).toBeVisible();
// sad paths, on demand:
await page.route("**/api/products",
(route) => route.fulfill({ status: 500 }));
await page.route("**/api/ads",
(route) => route.abort());
// pure API test — no page at all (9.7 delivered):
const res = await request.get("/api/products");
expect(res.status()).toBe(200);The problem, honestly stated: an unmocked E2E test leans on the real backend. It’s slow (every request a real round trip — 6.7), shared (a teammate deletes “Mug” and your test dies innocently), and unschedulable (how do you test the error banner without breaking the server?). 10.6’s boundary problem, browser-sized.
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
Handlers can be surgical: route.fulfill({ … }) can also start from the REAL response — fetch it (route.fetch()), tweak one field of the JSON, and fulfill with the modified body: perfect for “what if the price were negative” questions against otherwise-real data.
Routes are scoped: page.route affects one page; context.route covers every page in the context (11.7). Register routes BEFORE the navigation that triggers the requests — a booth built after the trucks left catches nothing (a classic first-week bug).
For whole recorded backends there’s HAR replay (routeFromHAR) — record a real session once, replay it as a complete mock. Heavier machinery, same stub idea. And the request fixture can share auth with your pages (11.11’s storage state) — API-create, UI-verify flows come cheap.
Job note: interviewers love “how would you test the error state of the products page?” The professional answer is one sentence now: intercept the products call with route.fulfill status 500 and assert the banner — deterministic, no server harmed. Bonus sentence: and a few unmocked journeys stay in the suite so the real wiring is still proven. That pairing is the whole judgment.
⌨️ build the customs booth
The starter is the “real server.” Build route registration and an interception-aware fetch: registered patterns get YOUR answer; everything else passes through to the server.
requirements:
- Keep the starter’s
server. Createroutes: an empty array, andaddRoute(pattern, handler)that pushes{ pattern, handler }. - Write
fetchWithRoutes(url): find the first route whose pattern the url includes (4.10’s find + includes); if found, return its handler’s answer; otherwise fall through toserver(url). - Register a mock: pattern
"/api/products"answering"Mug ₹250 (mocked)". Then print the result of fetching"https://shop.com/api/products"and of fetching"https://shop.com/api/user"— one mocked, one real.
when you press RUN, the console must show exactly:
✏️ Quick check 1
route.fulfill({ json: [...] }) answers the page’s request. In 10.6’s family, what is this — a stub, spy, or fake?
✏️ Quick check 2
How do you test the “server down” error banner without breaking any real server? (name the call)
✏️ Quick check 3
Should EVERY E2E test mock the backend? Type yes or no.
🗣️ Now teach it back
Explain interception to a friend: the backend-dependency problem, what page.route does to the envelope (name the 10.6 family member), why sad paths are the superpower, the mocked-vs-real tradeoff, and what the request fixture adds.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.