Answer
Scenario: Testing Error Handling via Mocked API Failures
One of the most valuable uses of Playwright's network interception is testing how your app handles API errors — without needing a broken backend.
Mock a 500 Internal Server Error
TypeScript
import { test, expect } from '@playwright/test';
test('shows error toast when product API fails', async ({ page }) => {
// Return 500 error for the products endpoint
await page.route('/api/products', route =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: 'Internal Server Error',
message: 'Database connection failed',
}),
})
);
await page.goto('/products');
// Verify error state in UI
await expect(page.locator('.error-toast')).toBeVisible();
await expect(page.locator('.error-toast')).toContainText('Something went wrong');
await expect(page.locator('.product-list')).not.toBeVisible();
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
});
Mock a 404 Not Found
TypeScript
test('shows not found page for invalid product', async ({ page }) => {
await page.route('/api/products/99999', route =>
route.fulfill({
status: 404,
json: { error: 'Product not found' },
})
);
await page.goto('/products/99999');
await expect(page.getByRole('heading')).toContainText('Product Not Found');
await expect(page.getByRole('link', { name: 'Back to Products' })).toBeVisible();
});
Mock a 401 Unauthorized
TypeScript
test('redirects to login on 401 response', async ({ page }) => {
// Simulate expired session
await page.route('/api/user/profile', route =>
route.fulfill({
status: 401,
json: { error: 'Session expired. Please log in again.' },
})
);
await page.goto('/profile');
// App should redirect to login
await expect(page).toHaveURL('/login');
await expect(page.locator('.session-message')).toContainText('Session expired');
});
Mock Network Failure (No Connection)
TypeScript
test('shows offline message when network fails', async ({ page }) => {
// Simulate complete network failure
await page.route('/api/**', route => route.abort('failed'));
await page.goto('/dashboard');
await expect(page.locator('.offline-banner')).toBeVisible();
await expect(page.locator('.offline-banner')).toContainText('Check your connection');
});
Mock Slow Response (Loading State)
TypeScript
test('shows loading skeleton during slow API', async ({ page }) => {
let resolveDelay: () => void;
await page.route('/api/products', route =>
new Promise(resolve => {
resolveDelay = resolve;
// Keep the route "pending" for 3 seconds
setTimeout(() => {
resolve(undefined);
route.fulfill({ json: { products: [] } });
}, 3000);
})
);
await page.goto('/products');
// Verify loading skeleton shows during the delay
await expect(page.locator('.skeleton-loader')).toBeVisible();
await expect(page.locator('.product-list')).not.toBeVisible();
// After 3s, data loads
await expect(page.locator('.product-list')).toBeVisible({ timeout: 5000 });
await expect(page.locator('.skeleton-loader')).not.toBeVisible();
});
Test Retry Mechanism
TypeScript
test('retry button fetches data again after error', async ({ page }) => {
let callCount = 0;
await page.route('/api/products', async route => {
callCount++;
if (callCount === 1) {
// First call fails
await route.fulfill({ status: 500, json: { error: 'Error' } });
} else {
// Second call succeeds (after user clicks retry)
await route.fulfill({ status: 200, json: [{ id: 1, name: 'Laptop' }] });
}
});
await page.goto('/products');
await expect(page.locator('.error-message')).toBeVisible();
// Click retry
await page.getByRole('button', { name: 'Retry' }).click();
// Data loads successfully on second attempt
await expect(page.locator('.product-card')).toBeVisible();
expect(callCount).toBe(2);
});
