Skip to content

Chapter 8 · Network Interception and Mock ​

Chapter goals: Learn to intercept any network request with page.route, master the three handling strategies — fulfill / continue_ / abort — and precisely wait for specific API responses with expect_response.

8.1 Why E2E Tests Need Network Mocking ​

Real backends are the main source of E2E instability: data changes daily, third-party services time out occasionally, and edge cases (insufficient balance, payment failure) are impossible to trigger on demand. Playwright's network interception lets you take over requests at the browser level — the page code is unaware, and the test gets fully controllable data.

python
from playwright.sync_api import sync_playwright, expect

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()

    def handle(route):
        # Never send a real request; respond with mock data directly
        route.fulfill(json=[{"name": "Strawberry", "id": 21}])

    page.route("**/api/v1/fruits", handle)   # Intercept matching requests
    page.goto("https://demo.playwright.dev/api-mocking")

    expect(page.get_by_text("Strawberry")).to_be_visible()
    browser.close()

URL matching supports three forms: glob string ("**/api/v1/**"), regex object (re.compile(r".*\/api\/v1\/fruits")), and predicate function (receives route.request, returns bool — can filter by method, headers, etc.).

8.2 Three Handling Strategies: fulfill / continue_ / abort ​

After receiving a route, you have three paths covering all mock test needs:

python
def handle(route):
    req = route.request

    if req.method == "GET" and "banner" in req.url:
        # ① Forge response directly: request never reaches the server
        route.fulfill(status=200, json={"img": "/fake.png"})
    elif req.method == "POST":
        # ② Forward with modification: inject a custom header and continue
        headers = {**req.headers, "x-test-mark": "e2e"}
        route.continue_(headers=headers)
    else:
        # ③ Simulate network failure: for testing frontend error-handling UI
        route.abort("failed")

route.continue_() can also mutate URL, method, and postData; route.abort() takes an optional error code (e.g. "connectionrefused"). Pick exactly one per route — calling both fulfill and continue_ on the same route raises an exception.

Every handler must have an exit path

Every intercepted request must eventually be handled by fulfill, continue_, or abort. If a condition branch in your handler misses a case, that request hangs forever and the page appears to "keep spinning" — the most common beginner pitfall.

8.3 Modifying Real Responses: fetch + fulfill ​

More often you don't want to fabricate data from scratch — you want the real API to run, then patch the result (e.g., append an edge-case item to the list):

python
def handle(route):
    response = route.fetch()            # Execute the real request
    data = response.json()
    data.append({"name": "Loquat", "id": 100})   # Patch onto real data
    route.fulfill(response=response, json=data)  # Respond with patched content

page.route("**/api/v1/fruits", handle)
page.goto("https://demo.playwright.dev/api-mocking")
expect(page.get_by_text("Loquat", exact=True)).to_be_visible()

This pattern is ideal for testing scenarios like "last page of pagination", "empty list", or "超长文本截断" that depend on backend data shapes.

8.4 Waiting and Call Count Control ​

Waiting for a specific response: when a button click triggers an XHR and your assertion depends on its result, wrap the action with expect_response:

python
# Glob-match the target endpoint
with page.expect_response("**/api/fetch_data") as response_info:
    page.get_by_text("Update").click()
response = response_info.value
assert response.status == 200
assert response.json()["ok"] is True

Limiting route hit count: page.route supports a times parameter since v1.15, making the mock fire only for the first N hits, then letting subsequent requests pass through normally:

python
# Only the first request fails; retries go to the real logic
page.route("**/api/payment", lambda r: r.abort(), times=1)
page.goto("https://myshop.example/checkout")

Combined with page.unroute("**/api/payment") you can revoke interception mid-test; together they express most time-sensitive network scenarios.

8.5 Real-World: Mocking a Third-Party Payment Gateway ​

Stringing all techniques together — test the checkout flow without ever touching the real payment service:

python
import re

def test_checkout_with_mocked_payment(page):
    # Block the payment SDK script to avoid loading external JS
    page.route(re.compile(r"https://pay-gateway\.example\.com/.*"),
               lambda r: r.fulfill(status=200, body="window.PayStubbed=true"))
    # Mock the payment status polling: first "pending", then "paid"
    states = iter([{"state": "pending"}, {"state": "paid"}])
    def poll(route):
        route.fulfill(json=next(states))
    page.route("**/api/pay/status", poll)

    page.goto("https://myshop.example/checkout")
    page.get_by_role("button", name="Pay").click()
    expect(page.get_by_text("Payment successful")).to_be_visible(timeout=10_000)

8.6 Chapter Summary ​

  • page.route(url_pattern, handler) intercepts requests at the browser level;
  • Three strategies: fulfill (fake response), continue_ (forward, optionally modified), abort (simulate failure);
  • Every handler must have an unconditional exit path — missing a branch hangs the page;
  • route.fetch() + fulfill patches real API responses;
  • expect_response waits for specific API calls; times=N limits mock hit count.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. What happens if a route handler does not call fulfill/continue_/abort?

2. To modify headers on a real request before it reaches the server, which method do you use?

3. What does route.fetch() do?

4. How do you make a mock route only fire for the first 3 requests?

🛠️ Hands-On Exercises ​

  1. Intercept a real API call on any public site with page.route; log the original response status and body before fulfilling with modified data.
  2. Write a test that uses route.abort("connectionrefused") on a specific endpoint and asserts the frontend shows an error banner.
  3. Use expect_response to capture a XHR and assert both its status code and response body shape in one test.

Proceed to Chapter 9: Auth State Reuse with storage_state.