Three strategies: return fake JSON directly, fetch the real response and patch it, or record the whole session to a HAR file and replay. Each has its place. I use fake JSON for happy-path isolation, patching when I need 90% real data, and HAR when the interaction is too complex to hand-craft.
Return fake JSON — intercept and fulfill
route.fulfill() short-circuits the request — the server is never called and the page always gets the exact data you specify
The most common case: intercept a URL pattern and return custom JSON. No real request goes to the server. I use this when I need the test to always see exactly two orders — regardless of what's in the database, regardless of environment.
Set up page.route() BEFORE navigating to the page. The pattern is a glob — */**/api/orders matches any origin.
Sometimes I need real data from the server — maybe 20 orders that are already seeded — but I want to force one of them into a specific state. Instead of fully mocking, I fetch the actual response and modify it before sending it to the page.
The key: route.fetch() makes the actual request, then you modify json and call route.fulfill({ response, json }). Passing the original response preserves headers, status, and cookies.
ts
test('overdue order shows warning badge', async ({ page }) => {
await page.route('*/**/api/orders', async route => {
// Виконуємо реальний запитconst response = await route.fetch()
const orders = await response.json()
// Модифікуємо перше замовлення — додаємо поле
orders[0].overdue = true
orders[0].dueDaysAgo = 5// Повертаємо оригінальну відповідь + патчawait route.fulfill({ response, json: orders })
})
await page.goto('/orders')
// Badge з'являється тільки для overdue замовленьawaitexpect(page.getByTestId('overdue-badge')).toBeVisible()
})
HAR files — record once, replay forever
A HAR file captures every network request the page made: URL, method, headers, request body, response status, response body. Record the whole checkout flow once against a real backend, commit the HAR file, replay it in CI without touching the server.
Step 1 — record: run with update: true. Playwright makes real requests and saves them to the HAR file. Step 2 — commit the .har file. Step 3 — replay: switch to update: false. All requests are served from the file.
HAR replay matches URL and HTTP method strictly. For POST requests it also matches the payload. If multiple recorded entries match, the one with the most matching headers wins.
// Крок 3: update: false — відтворює з HAR без реального сервераtest('checkout flow (replay mode)', async ({ page }) => {
await page.routeFromHAR('./hars/checkout.har', {
url: '*/**/api/**',
update: false, // обслуговує з файлу, не йде до сервера
})
await page.goto('/checkout')
await page.getByLabel('Card number').fill('4242424242424242')
await page.getByRole('button', { name: 'Pay' }).click()
await page.waitForURL('/orders/*/confirmation')
awaitexpect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible()
})
Mock WebSockets
The orders dashboard uses WebSocket for real-time status updates. In tests I don't want to depend on a real WebSocket server — I mock the connection and send controlled messages.
Two modes: fully mock the connection (no real server), or proxy to the real server and intercept specific messages. The proxy approach lets you test edge cases without breaking the full integration.