Core Route Interception Setup #
Playwright intercepts outbound requests via page.route(). The matcher accepts strings, globs, or regex. Always register routes before navigation to prevent race conditions. Use route.fulfill() for static JSON or route.continue() for unmatched traffic. Proper timing guarantees seamless test lifecycle integration.
// tests/api-mocking.spec.js
const { test, expect } = require('@playwright/test');
test('loads user list from mock', async ({ page }) => {
// Register the route BEFORE page.goto() to ensure no requests escape.
await page.route('**/api/v1/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test User' }])
});
});
await page.goto('/users');
await expect(page.getByRole('listitem')).toHaveCount(1);
});
Intercepts the exact endpoint and returns a deterministic JSON payload.
Dynamic Payload Generation & Contract Validation #
Static mocks degrade quickly as backend contracts evolve. Use callback functions to inspect route.request() and generate context-aware responses. Validate outgoing payloads against OpenAPI schemas before returning stubbed data. This prevents false-positive passes and decouples UI assertions from volatile backends.
test('loads order detail from dynamic mock', async ({ page }) => {
await page.route(/\/api\/v1\/orders\/.*/, async route => {
const orderId = route.request().url().split('/').pop();
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: orderId, status: 'fulfilled' })
});
});
await page.goto('/orders/42');
await expect(page.getByTestId('order-status')).toHaveText('fulfilled');
});
Extracts path parameters to generate context-aware mock responses.
Handling CORS & Pre-flight OPTIONS Requests #
Playwright fulfills routes at the protocol level, which bypasses browser CORS enforcement. However, if you use route.continue() to forward to a real backend from a cross-origin test page, the browser’s CORS rules still apply. When fulfilling responses directly via route.fulfill(), set the appropriate Access-Control-Allow-* headers to ensure the application code processes them correctly.
await page.route('**/api/**', async route => {
if (route.request().method() === 'OPTIONS') {
// Fulfill preflight requests explicitly to prevent browser-level CORS blocks.
await route.fulfill({
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
} else if (route.request().method() === 'GET') {
await route.fulfill({ status: 200, body: '{}', contentType: 'application/json' });
} else {
await route.continue();
}
});
Handles preflight requests explicitly while preserving write operations for integration testing.
Troubleshooting Route Registration Timing #
If mocks fail to trigger, verify page.route() executes before page.goto() or the user action that triggers the request. Use page.waitForResponse() to assert interception synchronously. For flaky matches, replace glob patterns with regex containing explicit URL parameter boundaries.
test('route registered before navigation', async ({ page }) => {
// Correct: route registered first, then navigation.
await page.route('**/api/data', route => route.fulfill({
status: 200,
body: '{"items": []}',
contentType: 'application/json'
}));
const [response] = await Promise.all([
page.waitForResponse('**/api/data'),
page.goto('/dashboard')
]);
expect(response.status()).toBe(200);
});
Common Pitfalls #
- Registering routes after page navigation causes missed interceptions and test flakiness.
- Using overly broad globs (
**/api/**) masks unrelated network failures and inflates false-positive pass rates. - Forgetting to handle
OPTIONSpreflight requests when testing cross-origin interactions. - Hardcoding timestamps or UUIDs in mock payloads breaks assertion logic for dynamic fields.
FAQ #
Why do my Playwright API mocks fail intermittently in CI?
Intermittent failures usually stem from race conditions between route registration and network dispatch. Always call page.route() before triggering navigation or user interactions, and use page.waitForResponse() to synchronize assertions.
Can I mock GraphQL queries using page.route()?
Yes. GraphQL typically uses a single POST endpoint. Intercept the URL and inspect await route.request().postData() to match the operationName or query string, then return a structured JSON response.
How do I verify that the mock was actually triggered?
Use Playwright’s built-in page.waitForResponse() with a predicate function that checks response.url() and response.status(). This guarantees the interception occurred before proceeding with DOM assertions.
Reliability Metrics #
| Metric | Impact |
|---|---|
| Flakiness Reduction | 60–85% reduction in network-dependent test failures |
| Execution Time | 40–70% faster suite execution by eliminating external latency |
| Maintenance Overhead | Low to Medium (requires periodic sync with OpenAPI contracts) |
| CI Stability Score | High (deterministic payloads eliminate rate-limit and timeout variables) |