skip to content

How do you make a Playwright route return the real forecast response with one field changed?

level: seniorimportance: should knowfreq 36%

answer

  1. Get the real one, then edit it
  2. Fetching does not settle the route
  3. Keep the metadata, replace the payload
  4. Small edit beats a large fixture
  5. The test now depends on the service

basics

~20 s

Call route.fetch() to perform the real request, parse the returned APIResponse, edit the parsed body, then settle the route with route.fulfill({ response, json }). Passing the response keeps the real status and headers while the json option replaces the payload.

solid answer

~40 s

`route.fetch()` performs the routed request from the test process and returns an `APIResponse`, but it does **not** settle the route — the browser is still waiting. So the handler reads the body with `await response.json()`, mutates the parsed object, and then calls `await route.fulfill({ response, json: body })`. Passing `response` preserves the real status and headers; `json` overrides only the payload. This beats a hand-written fixture when the payload is large or the edit is small — forcing a 45°C reading into an otherwise genuine forecast response, or truncating a list to prove an empty state. The cost is real: this test now contacts the third-party service, so it is slower and can fail for reasons unrelated to your change. Assert on the field you forced, not on data the service happened to send.

code

typescript · 13 lines
typescript
import { test, expect } from '@playwright/test';

test('heat warning renders for an extreme temperature', async ({ page }) => {
  await page.route('**/api/forecast**', async route => {
    const response = await route.fetch();
    const forecast = await response.json();
    forecast.current.tempC = 45;
    await route.fulfill({ response, json: forecast });
  });

  await page.goto('/dashboard?city=oslo');
  await expect(page.getByTestId('heat-warning')).toBeVisible();
});

go deeper

for a junior

Recall that a handler can fetch the real response first and reply with a modified copy, and that fetching alone leaves the request waiting until you fulfil.

for a middle

Explain the four steps and why the response object is passed to fulfil alongside the edited body, plus what fetch does with redirects and headers.

for a senior

Show that you know what this costs. The test now depends on a third-party service, so justify the trade, keep the mutation minimal, guard the parse, and assert only on what you forced.

for a principal

Own the boundary between the hermetic suite and the thin layer allowed to touch real services, including where it runs, who reacts when it fails, and how contract drift is meant to surface.

## The problem with hand-written stubs Fulfilling from a literal object is perfect until the payload is large or the interesting part is one field inside it. A third-party forecast response with hourly arrays, station metadata and units is painful to type, goes stale silently, and a hand-written copy that drifts from the real shape produces a green test against a payload nobody serves. When you only want to say "same response, but make the temperature 45", you want the real body with one edit. ## route.fetch, then fulfil with the result `route.fetch()` performs the routed request from the test process and returns an `APIResponse`. It does **not** settle the route — the browser is still waiting — so the handler must go on to fulfil (or abort). The standard four lines: 1. `const response = await route.fetch();` — the real request goes out, carrying the routed request's method, URL, headers and body, plus the context's cookies. 2. `const body = await response.json();` — read and parse what came back. 3. Mutate the parsed object in place. 4. `await route.fulfill({ response, json: body });` — reply with the original status and headers, but the edited body. Passing `response` is what preserves the real status and headers; `json` (or `body`) overrides just the payload. Without `response` you would be back to inventing the metadata as well. ## What route.fetch changes about the request - Overrides are accepted — `url`, `method`, `headers`, `postData` — and `headers` applies to the fetched request **and to any redirects it initiates**. If you want headers on the original request only, that is `route.continue()` territory. - Redirects are followed for you, bounded by `maxRedirects`; there is a `timeout` too. - The call happens outside the browser's own network stack, so the page's cache and service worker are not involved. | approach | server contacted | body authored by | breaks when | |---|---|---|---| | `route.fulfill({ json })` | no | the test | the real contract changes | | `route.fetch()` then `fulfill` | yes | the service, edited | the service is slow or down | | `route.continue()` | yes | the service | the data is not what the test needs | ## Where it earns its keep - Forcing a value the real API will not produce on demand: a 45°C reading, a negative wind speed, a station the dashboard has never seen. - Truncating a long list to prove pagination or an empty state without a fixture. - Adding a field the backend has not shipped yet, to build the UI against tomorrow's contract today. ## The cost you are accepting This test now depends on the third-party service. It is slower, it can fail for reasons unrelated to your change, and it may need credentials in CI. That is a deliberate trade, not a free upgrade — a handful of these are a useful early warning that the upstream shape moved, while a whole suite of them is a flaky suite. Keep the mutation minimal and assert on the field you changed, so a failure points at your code rather than at whatever the upstream service did overnight.

  • Why pass response to route.fulfill() when you already have the edited body?
    Because `response` supplies the real status line and headers, so you are not inventing the metadata as well. The `json` or `body` option then overrides just the payload. Fulfilling without `response` means hand-writing the status and any headers the app relies on, which is exactly the drift the approach is meant to avoid.
  • How do header overrides on route.fetch() behave when the request redirects?
    Header overrides apply to the fetched request and to any redirects it initiates, which is usually what you want for an auth header. If you need headers on the original request only, use `route.continue()` instead. Redirects are followed automatically, bounded by the `maxRedirects` option, and there is a `timeout` option too.
  • When would you prefer a checked-in fixture over fetching and mutating?
    Whenever the test runs on every commit. Fixtures are fast, hermetic and offline; fetch-and-mutate makes the test only as reliable as the third-party service and may need credentials in CI. Keep the live variant for a small layer whose purpose is noticing that the upstream contract moved.

saying these in an interview costs you the question

  • Thinks route.fetch settles the route by itself
  • Fulfils the edited body without passing the response
  • Believes fetch and mutate keeps the test hermetic
  • Expects continue to hand back the response for editing
  • Asserts on live fields the service changes hourly
  • Parses the body as JSON without guarding against an HTML error page