This recipe shows the smallest useful Playwright page.route test against QA Shop.
It intercepts the product detail page's reviews "Load more" fetch, returns a deterministic fixture, and asserts the mocked review renders into the DOM. Because the first page of reviews is server-rendered, only the load-more click triggers a real browser request — making it the canonical interception target on this site.
Network mocking with page.route is the right tool when you want a fast, hermetic browser test that does not depend on the database state, a third-party API, or even a running backend for the mocked endpoint.
§ 01 · PREREQUISITES
Prerequisites
You need a Playwright project pointing at QA Shop and a product slug whose PDP has at least one page of reviews.
Bootstrap with npm init playwright@latest if you don't already have a project.
Set to http://localhost:3002 for a local checkout, or https://automationtestingplatform.com for the hosted demo.
The default seed for wooden-train-set-100-piece ships with enough reviews to show the load-more button. If your local seed does not, see the FAQ.
§ 02 · THE TEST
The full spec
Save this file as tests/recipes/playwright/network-intercept-mock.spec.ts and run it with npx playwright test --grep network-intercept-mock.
// tests/recipes/playwright/network-intercept-mock.spec.ts
import { test, expect } from "@playwright/test";
const PRODUCT_SLUG = "wooden-train-set-100-piece";
test("page.route intercepts the PDP reviews fetch with a fixture payload", async ({
page,
}) => {
// Install the route handler BEFORE the action that triggers the fetch.
// The PDP's first reviews page is server-rendered; the load-more button
// triggers a client-side GET /api/products/{slug}/reviews?page=2&perPage=N.
await page.route("**/api/products/*/reviews*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
reviews: [
{
// Matches the PdpReview shape exported by
// src/components/features/pdp/pdp-review-list.tsx.
reviewerName: "Mocked Reviewer",
rating: 5,
title: "Intercepted",
body: "This review came from page.route, not the database.",
verifiedPurchase: true,
helpfulCount: 0,
createdAt: new Date().toISOString(),
},
],
total: 1,
hasMore: false,
}),
});
});
await page.goto(`/products/${PRODUCT_SLUG}`);
await expect(page.getByTestId("page-pdp")).toBeVisible();
// Trigger the client-side fetch.
await page.getByTestId("pdp-reviews-load-more").click();
// The mocked review renders into the next available pdp-review-body slot.
// Assert by text rather than by stable index because the SSR-paged reviews
// already occupy indices 0..N-1 and the mocked one is appended.
await expect(
page.getByText("This review came from page.route, not the database."),
).toBeVisible();
});Three details are worth calling out.
The route handler glob is **/api/products/*/reviews*. The leading ** matches any origin prefix so the same spec works against localhost:3002 and the hosted demo. The trailing * matches the ?page=2&perPage=N query string — Playwright's URL matcher treats the query as part of the path for glob purposes1.
The handler is installed before page.goto. This matters because handlers only intercept requests made after registration. Installing after the navigation would let the real network request escape — a classic source of "my mock doesn't work" debugging.
The assertion uses page.getByText instead of page.getByTestId('pdp-review-body-N'). The SSR'd first page already occupies pdp-review-body-0..N-1, and the mocked review appears at whatever the next index turns out to be. Asserting by text is decoupled from that count.
§ 03 · FAQ
FAQ
Common questions when extending this pattern to other endpoints.
route.fulfill returns a complete fake response (this recipe). route.continue lets the real request proceed but with modified headers or body — useful for adding auth headers or rewriting the URL without inventing the response body. route.fallback defers to the next registered handler, which lets you stack interceptors.
For a one-off recipe like this, register inside the test. For a fixture you reuse across many specs (mocked auth, mocked search), put page.route in a test.beforeEach or in a custom fixture so every test in the file gets the same interception.
page.route registers a handler — it does not wait for any request. The await you see in this recipe is on the call to page.route itself (which returns a promise that resolves when the handler is registered), not on the handler firing. The handler fires lazily when the matching request happens.
Related
For the cart-and-checkout starter test that exercises real network traffic end-to-end, see the add-to-cart Playwright recipe. For the broader Playwright vs Selenium vs Cypress picture, see the QA Shop testing guide.
The Selenium, Cypress, and WebdriverIO network-mock counterparts are coming soon — placeholder slugs are /recipes/selenium/network-intercept-mock, /recipes/cypress/network-intercept-mock, and /recipes/webdriverio/network-intercept-mock.
Footnotes
-
Playwright page.route — https://playwright.dev/docs/api/class-page#page-route (verified 2026-05-03, Playwright 1.58) ↩
Frequently asked questions
Last verified: