← Blog

"Network Mock Studio — Intercept and Stub HTTP Requests in Playwright Tests"

Stub API responses, simulate network errors, and test edge cases that are impossible to trigger in a real environment — all from inside the Playwright Studio DevTools panel.

reading now
views
comments

Series Navigation

Assertion Builder Guide

Self-Healing Locators


Why Mock Networks in Tests?

Real APIs make tests slow, flaky, and hard to control:

Problem 1: Speed
  Real API call → 200-800ms per request
  10 API calls per test × 50 tests = minutes of waiting

Problem 2: Test data
  Your API returns real data that changes
  Test that checks "3 items in cart" fails when data changes

Problem 3: Edge cases
  How do you test "what happens when the API returns 500"?
  You can't trigger that on a real server reliably

Problem 4: External dependencies
  Third-party APIs go down, rate-limit, or change responses
  Your tests should not depend on Stripe/Twilio/etc. being up

Solution: Mock the network

Opening the Network Mock Studio

  1. Open Chrome DevTools → Playwright Studio panel
  2. Click "Network Mock Studio" tab
  3. Click "+ Add Mock Rule"

Creating a Mock Rule

Each rule has three parts:

URL Pattern    → which requests to intercept
Method         → GET, POST, PUT, DELETE, or *
Mock Response  → status code + headers + body

Example — mock a product listing API:

URL Pattern:  **/api/products**
Method:       GET
Status:       200
Body:
{
  "products": [
    { "id": 1, "name": "Widget Pro", "price": 49.99 },
    { "id": 2, "name": "Gadget Plus", "price": 29.99 }
  ],
  "total": 2
}

The exported Playwright code:

await page.route('**/api/products', async route => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({
      products: [
        { id: 1, name: 'Widget Pro', price: 49.99 },
        { id: 2, name: 'Gadget Plus', price: 29.99 }
      ],
      total: 2
    })
  });
});

await page.goto('/products');
await expect(page.locator('.product-card')).toHaveCount(2);

URL Pattern Matching

// Exact URL
await page.route('https://api.example.com/users', handler);

// Wildcard — any path under /api/
await page.route('**/api/**', handler);

// Specific endpoint regardless of domain
await page.route('**/products*', handler);

// Regex pattern
await page.route(/api\/users\/\d+/, handler);

// Query parameters — match the path, ignore params
await page.route('**/search**', handler);
// Matches: /search?q=test&page=2

Common Mock Scenarios

Simulate API Error (500)

await page.route('**/api/checkout', async route => {
  await route.fulfill({
    status: 500,
    contentType: 'application/json',
    body: JSON.stringify({ error: 'Internal server error' })
  });
});

await page.click('#checkout-btn');
await expect(page.locator('.error-banner')).toBeVisible();
await expect(page.locator('.error-banner')).toContainText('Something went wrong');

Simulate 401 Unauthorized

await page.route('**/api/profile', async route => {
  await route.fulfill({
    status: 401,
    contentType: 'application/json',
    body: JSON.stringify({ error: 'Unauthorized' })
  });
});

await page.goto('/profile');
// App should redirect to login
await expect(page).toHaveURL(/login/);

Simulate Slow Network

await page.route('**/api/reports/generate', async route => {
  // Delay 3 seconds
  await new Promise(r => setTimeout(r, 3000));
  await route.fulfill({
    status: 200,
    body: JSON.stringify({ status: 'complete', url: '/reports/123' })
  });
});

// Test that loading state appears
await page.click('#generate-report');
await expect(page.locator('.loading-spinner')).toBeVisible();
await expect(page.locator('.report-link')).toBeVisible({ timeout: 10000 });

Simulate Network Failure (no response)

await page.route('**/api/save', async route => {
  await route.abort('failed');  // simulates network failure
});

await page.click('#save-btn');
await expect(page.locator('.offline-warning')).toBeVisible();

Partial Mock — Let Some Requests Through

// Only mock POST to /api/login, let everything else through
await page.route('**/api/login', async route => {
  if (route.request().method() === 'POST') {
    await route.fulfill({
      status: 200,
      body: JSON.stringify({ token: 'fake-jwt-token', userId: 42 })
    });
  } else {
    await route.continue();  // pass through to real server
  }
});

Inspecting Captured Requests

The Network Mock Studio also shows you all network requests made by the page — useful for understanding what your app actually calls before writing mocks:

  1. Navigate to the page you want to test
  2. Open Playwright Studio → Network Mock Studio
  3. Click "Capture Mode"
  4. Interact with the page
  5. See every request: URL, method, status, response body

This tells you exactly what to mock without guessing.


Best Practices

// ✅ Mock in beforeEach for consistent test state
test.beforeEach(async ({ page }) => {
  await page.route('**/api/user', async route => {
    await route.fulfill({
      status: 200,
      body: JSON.stringify({ id: 1, name: 'Test User', role: 'admin' })
    });
  });
});

// ✅ Use meaningful test data that matches what your UI expects
// ❌ Don't use generic placeholder data like { id: 1, name: 'test' }
// ✅ Use realistic data: { id: 42, name: 'Alice Chen', email: 'alice@acme.com' }

// ✅ Assert on the mocked data — verify your UI renders it correctly
await expect(page.locator('.user-name')).toHaveText('Alice Chen');

// ✅ Clean up routes after tests that need real network
test.afterEach(async ({ page }) => {
  await page.unrouteAll();
});

What's Next

Discussion

Loading...

Leave a Comment

All comments are reviewed before appearing. No links please.

0 / 1000