page.route() is your interception tool. You tell Playwright: "When the browser tries to call this URL, do not send it to the server. I will handle it." That is it. One line to intercept, one line to respond.
import { test, expect } from '@playwright/test';
test('intercept and fulfill a request', async ({ page }) => {
// Set up the route BEFORE navigating
await page.route('**/api/products', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Chai Masala', price: 199 },
{ id: 2, name: 'Filter Coffee', price: 249 },
]),
});
});
await page.goto('/shopping');
// UI now shows our mock data -- not whatever the real API returns
await expect(page.getByText('Chai Masala')).toBeVisible();
await expect(page.getByText('Filter Coffee')).toBeVisible();
});Always set up page.route() BEFORE the action that triggers the request. If you set it up after page.goto(), the request has already been sent and your mock will not catch it.
The first argument to page.route() is a URL pattern. Playwright supports glob patterns, regex, and predicate functions.
| Pattern | Matches | Use Case |
|---|---|---|
| **/api/products | Any URL ending with /api/products | Exact endpoint match |
| **/api/products* | Includes query params like ?page=1 | Endpoint with query strings |
| **/api/** | Any API route | Mock all API calls |
| **/*.{png,jpg} | All PNG and JPG images | Block or replace images |
| https://api.example.com/** | Only this specific host | Third-party API mocking |
// Glob pattern -- most common
await page.route('**/api/products', handler);
// Regex -- when you need more precision
await page.route(/\/api\/products\/\d+/, handler);
// Predicate function -- full control
await page.route(
(url) => url.pathname.startsWith('/api/') && url.searchParams.has('category'),
handler
);route.fulfill() -- Return a custom response. The request never reaches the server.
route.abort() -- Kill the request. The browser gets a network error. Use this to block analytics, ads, images.
route.continue() -- Let the request go to the real server, optionally modifying headers or URL on the way out.
route.fetch() -- Actually send the request to the server, get the real response, then decide what to do with it.
// Abort -- block analytics
await page.route('**/*analytics*', (route) => route.abort());
// Continue -- add a custom header
await page.route('**/api/**', (route) => {
route.continue({
headers: {
...route.request().headers(),
'X-Test-Mode': 'true',
},
});
});
// Fulfill -- return custom data (covered in next lesson)Q: What is the difference between route.abort() and route.fulfill() with a 500 status?
A: route.abort() simulates a network failure -- the request never completes, the browser gets a connection error. route.fulfill({ status: 500 }) simulates the server responding with an error -- the request completes, but the response is an error. Use abort() to test "what if the network is down" and fulfill(500) to test "what if the server returns an error."
Key Point: page.route() intercepts requests before they leave the browser. fulfill() returns fake data, abort() kills the request, continue() lets it pass through.