Files
magnus919_agent-skills/playwright/references/03-network-interception-and-mocking.md
T
Magnus HedemarkGitHubfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
ac1beb117d feat(skill): add Playwright skill (E2E testing + scraping + headless browsing) (#264)
Add ONE tool skill for Playwright: SKILL.md covering E2E test authoring,
selector robustness, network interception/mocking, parallel workers, CI
integration, scraping/headless patterns, accessibility snapshot checks, and
headed debugging; scripts/pwrun (agent-first smoke harness with --json,
fixture-tested); templates/ test-suite scaffold; eight dated references; a
schema-valid evals/evals.json (6 cases); a human-facing README; reverse
routing from qa-methodology and frontend-engineering; top-level README index
entry; and regenerated catalogs (llms.txt, marketplace, codex).

Closes #244.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-03 17:59:38 -04:00

3.1 KiB

Network Interception and Mocking

Last Updated: 2026-08-03

Intercept browser traffic with page.route() to make E2E tests hermetic, deterministic, and fast — and to assert on what the app actually sent.

The route API

// Fulfill: answer an intercepted request with canned data.
await page.route('**/api/payments/tokenize', (route) =>
  route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ token: 'tok_test_123' }),
  }),
);

// Abort: block a request entirely (trackers, analytics, heavy media).
await page.route('**/analytics/*', (route) => route.abort());

// Continue: let the real request through (with optional overrides).
await page.route('**/api/feature-flags', (route) =>
  route.continue({ headers: { ...route.request().headers(), 'x-experiment': 'on' } }),
);

Guidance:

  • Mock at the boundary. The app under test is real; the world outside it (payment providers, third-party APIs, message queues) is faked. Never patch in-page code (window.fetch = ...) — that tests a tampered app.
  • Realistic bodies win. Mock payloads that match the real schema and content type; a mock that differs from production can pass a test the app would fail.
  • Glob ** patterns, not exact URLs — hosts, query strings, and CDN prefixes change; **/api/orders covers https://api.example.com/api/orders.
  • Register routes before navigation — routes apply to requests made after registration, so set them up in test.beforeEach before page.goto.

What NOT to mock

  • The server under test: if the suite verifies the app + its backend integration, intercepting that API makes the test meaningless. Mock only third-party boundaries, or test the real API via a controlled environment.
  • page.goto() navigation that the test depends on — routing over the app's own document requests breaks the flow the test is verifying.
  • Responses whose timing you are testing (e.g., loading states): use route.fulfill with a small artificial delay or the API's page.clock/routing delay, never a fixed waitForTimeout.

Asserting on traffic

Capture what the app sent, then assert on it:

let tokenizeBody: string | undefined;
page.on('request', (request) => {
  if (request.url().includes('/api/payments/tokenize')) tokenizeBody = request.postData();
});
// ... drive the flow ...
expect(JSON.parse(tokenizeBody!)).toMatchObject({ amount: 4990, currency: 'usd' });

Use page.on('response') to wait for a specific status instead of guessing when the network settled:

await page.waitForResponse((response) => response.url().includes('/api/orders') && response.status() === 201);

Emulating network conditions

await page.route('**/*', (route) =>
  route.continue().catch(() => {})
);
await page.context().setOffline(true); // test offline/error states
// or throttle via context options: offline, latency, downloadThroughput...
  • Locators that survive the mocked world: 02-selectors.md.
  • Running these tests in parallel without interference: 04-parallel-workers-and-sharding.md.