Network Requests & Mocking — Isolate Your Frontend
Backend APIs break, rate-limit, and lag. Why should your frontend tests suffer? Learn how to intercept network requests and serve fake responses to test your UI in perfect isolation.
📖 The Story: The Flaky Payment Gateway
Sarah's team was building an e-commerce checkout. Their test suite included 50 tests that verified the UI behavior when a payment succeeded, failed, or was declined. To test this, they made real API calls to Stripe's sandbox environment.
Every week, 10% of their CI runs would fail. The reason? Stripe's sandbox would occasionally rate-limit them, or the office WiFi would drop for a split second. The tests weren't broken; the network was.
Worse, testing the "Payment Declined" scenario was nearly impossible because the sandbox API made it tedious to trigger specific decline codes. The QA team spent hours maintaining test credit cards.
Then Sarah discovered page.route(). She intercepted the /api/charge endpoint and fed the browser fake responses. "Succeed in 200ms." "Fail with a 500 error." "Decline with a specific message."
The 50 tests went from taking 2 minutes to taking 4 seconds. The CI failure rate dropped to 0%. The backend was gone. The frontend was finally in control.
🎯 Why Mock Networks?
Blazing Speed
Avoid network latency. Mocked responses return in milliseconds instead of seconds.
Zero Flakiness
No network drops, no rate limits, no backend downtime. Tests run identically every time.
Edge Cases
Easily test UI behavior for 500 errors, 404s, and slow loading that are hard to trigger on a real server.
Cost Savings
Avoid hitting paid third-party APIs (like SMS or payment gateways) during test runs.
🌍 Real World Example
A weather dashboard fetches data from a third-party API. If the API is down, the dashboard shows a "Weather data unavailable" error message. To test this error message, you would have to wait for the API to actually break.
With mocking, you intercept the request and force it to return a 500 status code. The UI instantly displays the error message. You've tested your error handling without depending on the third party.
🧒 Explain Like I'm 10
Imagine you're testing a walkie-talkie. You want to see if the speaker works when someone says "Hello." Instead of giving a walkie-talkie to a friend miles away and waiting for them to say "Hello" (which might take forever, or they might be busy), you hook up a tape recorder to the walkie-talkie's internal wires.
You press play on the tape recorder, and it feeds the word "Hello" directly into the circuits. The speaker plays it. You've tested the speaker perfectly, without needing your friend at all.
Network mocking is the tape recorder. It feeds fake responses directly to the browser without needing the real server.
🎓 Professional Explanation
Playwright provides the page.route(url, handler) API to intercept network requests at the browser level. When the browser attempts to make a request matching the URL pattern, Playwright pauses it and passes it to your handler function.
Your handler receives a route object with three primary methods:
route.fulfill(): Sends a custom response back to the browser. The request never hits the server.route.continue(): Allows the request to go to the server, but lets you modify headers, body, or URL before it goes.route.abort(): Cancels the request entirely. The browser receives a network error.
📊 The Interception Flow
Without Mocking vs With Mocking
Without Mocking
With Mocking
✅ route.fulfill() — The Mocker
Use fulfill to return a fake response. This is the most common mocking technique.
// Intercept all users API calls and return a fake user array await page.route('**/api/users', async route => { const mockUsers = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockUsers) }); }); await page.goto('/users'); // Browser receives mockUsers instantly
URL Matching: The ** is a glob wildcard. **/api/users matches https://app.com/api/users and http://localhost:3000/api/users.
Mocking from a File (Har)
// Fulfill a request using a static JSON file await page.route('**/api/dashboard', route => route.fulfill({ path: 'mocks/dashboard-data.json' }));
➡️ route.continue() — The Modifier
Sometimes you want the request to hit the server, but you want to add a header (like an Auth token) or change the URL (point it to a staging server).
// Add an authorization header before letting the request go to the server await page.route('**/api/*', async route => { await route.continue({ headers: { ...route.request().headers(), 'X-Test-Auth': 'mock-token-123' } }); });
🛑 route.abort() — The Network Breaker
Use abort to simulate network failures. Perfect for testing offline modes or error handling.
// Block all images from loading (saves test time) await page.route('**/*.{png,jpg,jpeg,webp}', route => route.abort()); // Simulate a total internet disconnect for a specific API await page.route('**/api/payments', route => route.abort('internetDisconnected'));
⚠️ Common Mistakes
Mistake 1: Mocking After Navigation
// ❌ BAD: Page loads and fetches data before route is intercepted await page.goto('/dashboard'); await page.route('**/api/dashboard', route => route.fulfill({ body: 'mock' })); // ✅ GOOD: Intercept BEFORE the browser requests it await page.route('**/api/dashboard', route => route.fulfill({ body: 'mock' })); await page.goto('/dashboard');
Mistake 2: Missing the Content-Type
// ❌ BAD: Browser doesn't know it's JSON, frontend JSON.parse fails await route.fulfill({ body: JSON.stringify({ id: 1 }) }); // ✅ GOOD: Always include content type await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 1 }) });
✅ Best Practices
- Always call
page.route()beforepage.goto(). Interception must be set up before the request is made. - Use specific URL patterns. Don't mock
**/*or you'll break CSS/JS assets. - Store large mock payloads in separate JSON files using the
pathoption to keep test code clean. - Only mock what you need. If you're testing the Users API, let the CSS and image requests go to the server.
- Reset mocks if they are applied globally to avoid leaking into other tests (Playwright handles this automatically if set on
page).
You can access the original request details inside the handler using route.request(). This lets you conditionally mock responses based on the request body or headers.
🔒 Security Notes
- Mask sensitive data in mocks. Don't store real user PII (like real social security numbers) in your mock JSON files. Generate fake data instead.
- Be careful with
route.continue()modifying headers. Don't accidentally strip auth headers that the backend requires to process the request. - Mocking prevents accidental data mutations. By mocking POST/PUT requests, you ensure your tests never accidentally write bad data to a real production database.
🏗️ Senior Engineer Deep Dive
How CDP Handles Interception
Playwright implements page.route using the Chrome DevTools Protocol (CDP) Fetch.enable command. It tells the browser to pause any network request matching a pattern before sending it over the wire. Because this happens at the browser network layer, it is completely transparent to the JavaScript running in the page (e.g., fetch or axios cannot tell they are being mocked).
Performance Impact of Mocking
While route.fulfill is fast, route.abort on heavy assets (like videos or large images) provides the most significant performance boost. By aborting these requests, you save browser rendering time, memory, and network bandwidth, drastically lowering test execution time in CI.
Mocking vs. HAR Recording
Instead of manually writing mocks, Playwright supports HAR (HTTP Archive) recording. You can record a real session once (recordHar in config), and then replay it later using routeFromHar. This is excellent for complex applications with dozens of microservices, but manual mocks are still better for testing specific edge cases.
💼 Interview Questions
Show Answer
page.route(url, handler) to intercept the request, and inside the handler, call route.fulfill({ status: 200, body: '...' }) to return a fake response.route.fulfill and route.continue?Show Answer
route.fulfill sends a fake response back to the browser without hitting the server. route.continue allows the request to proceed to the server, optionally letting you modify the request headers, body, or URL before it goes.Show Answer
page.route to intercept the specific API endpoint and call route.fulfill({ status: 500, body: 'Internal Server Error' }). This forces the browser to receive a 500 response, allowing me to assert that the UI displays the correct error message.Show Answer
page.route('**/*.{png,jpg,jpeg,webp,svg}', route => route.abort()) to block all image requests. This prevents the browser from downloading and rendering images, significantly speeding up the test run without affecting DOM interaction tests.🏋️ Practical Exercise
Mock a 500 Error
- Go to a site that makes an API call on load (e.g., a weather app).
- Write a Playwright test that intercepts the API call.
- Use
route.fulfillto return a500status code. - Assert that the UI displays its generic "Something went wrong" error message.
Success criteria: The test passes instantly without hitting the real server, and the error UI is verified.
🚀 Mini Project
🏗️ The Offline Dashboard
Build a test suite for a dashboard that doesn't exist yet.
- Write a test that mocks
GET /api/statsto return{ "users": 100, "revenue": 5000 }. - Navigate to
/dashboard(can be a blank HTML page). - Since the UI doesn't exist, use
page.evaluateto fetch the data and write it to the DOM, or just assert the network request was made and mocked properly. - Add a second test that mocks the same endpoint but aborts the request. Assert that the browser console logs a network error.
📝 Quiz
Test your understanding. Click an option to check your answer.
page.mock()page.intercept()page.route()page.on('request')page.route(url, handler) is used to intercept, fulfill, continue, or abort network requests.route.fulfill() do?route.fulfill() sends a custom response back to the browser, preventing the request from reaching the server.page.route()?page.goto()page.goto()afterEachpage.route('**/*', route => route.abort())page.route('**/*.{png,jpg}', route => route.abort())page.disableImages()page.route('**/*.{png,jpg}', route => route.fulfill())route.abort() to cancel them.route.fulfill()route.abort()route.continue()route.modify()route.continue() allows the request to proceed to the server, with options to override headers, body, or URL.route.fulfill() but forget to set the contentType?application/json content-type header, it may fail to parse the response.**/api/users match?https://example.com/api/users/api/users regardless of domain** matches any protocol and domain, so it intercepts the path /api/users on any host.route.fulfill({ path: 'mocks/data.json' })route.continue({ file: '...' })route.fulfill supports a path option to read the response body from a local file.window.fetch❓ FAQ
Q: Does mocking work for WebSockets?
A: Playwright's page.route primarily targets HTTP/HTTPS requests. WebSocket mocking is not directly supported via page.route in the same way, though you can intercept the initial HTTP upgrade request.
Q: Can I intercept requests made from a Service Worker?
A: Yes, Playwright's network interception happens at the browser level, so it intercepts requests made by Service Workers as well.
Q: How do I unroute a mock?
A: Use page.unroute(url) to remove the interceptor. However, in most tests, you simply set up routes per-test, and Playwright cleans them up automatically when the page closes.
📦 Summary
🎯 Key Takeaways
page.route()intercepts network requests before they hit the server.route.fulfill()returns a fake response (mocking).route.continue()modifies a request (headers/body) and lets it proceed.route.abort()cancels the request (great for blocking images).- Always call
page.route()before navigation. - Always include
contentTypewhen fulfilling JSON responses.
