Sometimes you do not want fully fake data. You want the real API response, but with one small change. Maybe you want to add an extra product. Or change a price. Or inject an error field. This is partial mocking -- and route.fetch() makes it dead simple.
Intercept the request with page.route()
Use route.fetch() to actually send the request to the real server
Get the real response back
Modify the response data (add items, change fields, inject errors)
Use route.fulfill() to send the modified response to the browser
import { test, expect } from '@playwright/test';
test('add a premium badge to the first product', async ({ page }) => {
await page.route('**/api/products', async (route) => {
// Step 1: Get the real response
const response = await route.fetch();
const products = await response.json();
// Step 2: Modify it
if (products.length > 0) {
products[0].badge = 'Premium';
products[0].discount = 20;
}
// Step 3: Send modified data to browser
await route.fulfill({
response, // keeps original status, headers
body: JSON.stringify(products),
});
});
await page.goto('/shopping');
await expect(page.getByText('Premium')).toBeVisible();
});test('add a test product to real product list', async ({ page }) => {
await page.route('**/api/products', async (route) => {
const response = await route.fetch();
const products = await response.json();
// Inject a test product at the beginning
products.unshift({
id: 9999,
name: 'QA Test Product -- DO NOT BUY',
price: 0.01,
category: 'testing',
});
await route.fulfill({
response,
body: JSON.stringify(products),
});
});
await page.goto('/shopping');
await expect(page.getByText('QA Test Product')).toBeVisible();
});test('override cache headers from real response', async ({ page }) => {
await page.route('**/api/**', async (route) => {
const response = await route.fetch();
// Get original headers and override caching
const headers = response.headers();
headers['cache-control'] = 'no-store';
headers['x-test-modified'] = 'true';
await route.fulfill({
status: response.status(),
headers,
body: await response.body(),
});
});
await page.goto('/shopping');
});Be careful with route.fetch() in CI environments. It makes a real network call. If the backend is down, your "partial mock" test will still fail. Consider full mocking for CI and partial mocking for local development only.
Q: When would you use partial mocking instead of full mocking?
A: When I need real data as a baseline but want to test how the UI handles modifications. For example, I fetch the real product list but inject one product with a very long name to test text overflow. Or I fetch the real response and change one field to null to test missing data handling. It is also useful when you want to keep your mock data aligned with the actual API shape without maintaining large fixture files.
Key Point: route.fetch() gets the real response. You modify it. route.fulfill() sends the modified version. Best of both worlds.