📖 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:

  1. route.fulfill(): Sends a custom response back to the browser. The request never hits the server.
  2. route.continue(): Allows the request to go to the server, but lets you modify headers, body, or URL before it goes.
  3. route.abort(): Cancels the request entirely. The browser receives a network error.

📊 The Interception Flow

Without Mocking vs With Mocking

Without Mocking
🌐 Browser Request
🗄️ Real Server (500ms latency)
🌐 Browser Response
With Mocking
🌐 Browser Request
🎭 Playwright Intercept (5ms)
🌐 Fake Response

✅ route.fulfill() — The Mocker

Use fulfill to return a fake response. This is the most common mocking technique.

typescript
// 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)

typescript
// 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).

typescript
// 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.

typescript
// 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

typescript · ❌ Too late
// ❌ 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

typescript · ❌ Broken parsing
// ❌ 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

  1. Always call page.route() before page.goto(). Interception must be set up before the request is made.
  2. Use specific URL patterns. Don't mock **/* or you'll break CSS/JS assets.
  3. Store large mock payloads in separate JSON files using the path option to keep test code clean.
  4. Only mock what you need. If you're testing the Users API, let the CSS and image requests go to the server.
  5. Reset mocks if they are applied globally to avoid leaking into other tests (Playwright handles this automatically if set on page).
💡 Pro Tip: Request Context

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

Beginner
How do you mock an API response in Playwright?
Show Answer
Use page.route(url, handler) to intercept the request, and inside the handler, call route.fulfill({ status: 200, body: '...' }) to return a fake response.
Intermediate
What is the difference between 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.
Advanced
If you want to test how your UI handles a 500 Internal Server Error, how would you achieve this?
Show Answer
I would use 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.
Scenario
Your test suite is slow because the app loads dozens of high-resolution images. How can you speed it up?
Show Answer
Use 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

🎯 Hands-On Practice Medium

Mock a 500 Error

  1. Go to a site that makes an API call on load (e.g., a weather app).
  2. Write a Playwright test that intercepts the API call.
  3. Use route.fulfill to return a 500 status code.
  4. 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.

  1. Write a test that mocks GET /api/stats to return { "users": 100, "revenue": 5000 }.
  2. Navigate to /dashboard (can be a blank HTML page).
  3. Since the UI doesn't exist, use page.evaluate to fetch the data and write it to the DOM, or just assert the network request was made and mocked properly.
  4. 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.

1. Which method is used to intercept network requests in Playwright?
A) page.mock()
B) page.intercept()
C) page.route()
D) page.on('request')
Answer: C — page.route(url, handler) is used to intercept, fulfill, continue, or abort network requests.
2. What does route.fulfill() do?
A) Lets the request go to the server
B) Cancels the request
C) Returns a fake response to the browser
D) Retries the request
Answer: C — route.fulfill() sends a custom response back to the browser, preventing the request from reaching the server.
3. When should you call page.route()?
A) After page.goto()
B> Before page.goto()
C) Inside afterEach
D) It doesn't matter
Answer: B — You must set up the route interceptor before the browser makes the request, otherwise the real request will go through.
4. How can you block all images from loading during a test?
A) page.route('**/*', route => route.abort())
B) page.route('**/*.{png,jpg}', route => route.abort())
C) page.disableImages()
D) page.route('**/*.{png,jpg}', route => route.fulfill())
Answer: B — Use a glob pattern matching image extensions and call route.abort() to cancel them.
5. If you want to add a custom header to a request but still let it hit the server, which method do you use?
A) route.fulfill()
B) route.abort()
C) route.continue()
D) route.modify()
Answer: C — route.continue() allows the request to proceed to the server, with options to override headers, body, or URL.
6. What is a major benefit of mocking network requests?
A) It tests the backend database logic
B) It eliminates network flakiness and latency
C) It makes the browser run faster overall
D) It bypasses CORS restrictions
Answer: B — By serving fake responses locally, tests are isolated from backend downtime, rate limits, and network latency.
7. What happens if you use route.fulfill() but forget to set the contentType?
A) Playwright throws an error
B) The request automatically goes to the server
C) The browser might not parse the body correctly (e.g., JSON.parse fails)
D) The response is treated as plain text
Answer: C — If the frontend expects JSON but doesn't receive the application/json content-type header, it may fail to parse the response.
8. What does the glob pattern **/api/users match?
A) Only https://example.com/api/users
B> Any URL ending in /api/users regardless of domain
C) Only local API calls
D) Any URL containing the word "users"
Answer: B — The ** matches any protocol and domain, so it intercepts the path /api/users on any host.
9. Can you mock a response using a static JSON file stored on disk?
A) Yes, using route.fulfill({ path: 'mocks/data.json' })
B) No, you must inline the JSON
C) Yes, but only if the file is less than 1MB
D) Yes, using route.continue({ file: '...' })
Answer: A — route.fulfill supports a path option to read the response body from a local file.
10. How does Playwright intercept the network at a technical level?
A) By monkey-patching window.fetch
B) By using Chrome DevTools Protocol (CDP) network commands
C) By running a local proxy server automatically
D) By blocking the OS network stack
Answer: B — Playwright uses CDP (or browser-specific equivalents) to pause and modify requests at the browser's network layer before they are sent.

❓ 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 contentType when fulfilling JSON responses.