JS Sketchbooksee JavaScript think ✏️
← back to phase 11

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.

watch it happen
// 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 envelope’s road — with a customs booththe pageasks /api/productscustomsboothpage.route(…)the realserverthe dependency problem, browser-sized: E2E tests lean on the BACKEND — slow,shared, and its data changes under you
under the hood

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.

your turn

⌨️ 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. Create routes: an empty array, and addRoute(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 to server(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:

Mug ₹250 (mocked)
Ada (from server)

✏️ 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.

teach it back

🗣️ 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.

a few sentences, minimum — you’ve got this
to remember
page.route(pattern, handler) = a customs booth: the page’s request stopped MID-FLIGHT. Verdicts: fulfill (a STUB — 10.6, network edition), abort (fake outage), continue (observe). Register BEFORE navigating.
The superpower: sad paths on demand — fulfill({ status: 500 }) tests error UI in one deterministic line. Tradeoff: mocked = fast/exact but backend unproven; keep a few unmocked journeys (the pyramid is fractal).
request fixture = 9.7 delivered: pure API tests (no page) in the same runner/report. 4.13 + 6.8 + 10.4 do all the JSON work — those lessons were this, in advance.