Now let us get serious. You know how to intercept. Time to learn how to craft proper mock responses -- custom JSON, status codes, headers, delays. This is where mocking becomes powerful.
import { test, expect } from '@playwright/test';
test('mock user profile API', async ({ page }) => {
await page.route('**/api/profile', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 'user-001',
name: 'Priya Sharma',
email: 'priya@test.com',
plan: 'premium',
xp: 4500,
}),
});
});
await page.goto('/profile');
await expect(page.getByText('Priya Sharma')).toBeVisible();
await expect(page.getByText('Premium')).toBeVisible();
});| Status Code | Meaning | UI Should Show |
|---|---|---|
| 200 | Success | Normal data |
| 201 | Created | Success confirmation |
| 400 | Bad Request | Validation error message |
| 401 | Unauthorized | Redirect to login or "Session expired" |
| 403 | Forbidden | "Access denied" message |
| 404 | Not Found | "Not found" or empty state |
| 429 | Rate Limited | "Too many requests, try later" |
| 500 | Server Error | "Something went wrong" |
| 503 | Service Unavailable | "Service is down, try later" |
test('mock 401 -- session expired', async ({ page }) => {
await page.route('**/api/profile', async (route) => {
await route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ error: 'Token expired' }),
});
});
await page.goto('/profile');
// App should redirect to login or show expiry message
await expect(page.getByText(/session expired|please login/i)).toBeVisible();
});
test('mock 403 -- access denied', async ({ page }) => {
await page.route('**/api/admin/users', async (route) => {
await route.fulfill({
status: 403,
contentType: 'application/json',
body: JSON.stringify({ error: 'Insufficient permissions' }),
});
});
await page.goto('/admin/users');
await expect(page.getByText(/access denied|forbidden/i)).toBeVisible();
});test('mock response with custom headers', async ({ page }) => {
await page.route('**/api/products', async (route) => {
await route.fulfill({
status: 200,
headers: {
'Content-Type': 'application/json',
'X-Total-Count': '150',
'X-Page': '1',
'X-Per-Page': '10',
'Cache-Control': 'no-cache',
},
body: JSON.stringify({
products: [{ id: 1, name: 'Widget' }],
total: 150,
}),
});
});
await page.goto('/shopping');
// Now you can test pagination UI that reads X-Total-Count header
});Want to test loading spinners? Add a delay before fulfilling. This simulates a slow API.
test('show loading spinner while API is slow', async ({ page }) => {
await page.route('**/api/products', async (route) => {
// Wait 3 seconds before responding
await new Promise((resolve) => setTimeout(resolve, 3000));
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});
await page.goto('/shopping');
// Spinner should be visible during the delay
await expect(page.getByTestId('loading-spinner')).toBeVisible();
// After 3 seconds, spinner should disappear
await expect(page.getByTestId('loading-spinner')).toBeHidden({ timeout: 5000 });
});Use JSON fixture files for large mock data. Create a folder like tests/fixtures/ and read from it: body: JSON.stringify(require("./fixtures/products.json")). Keeps your test files clean.
Key Point: route.fulfill() lets you return any status code, headers, and body. This is how you test every possible API outcome without touching the backend.
Key Point: route.fulfill() lets you return any status code, headers, and body -- test every possible API outcome without touching the backend.