Learn Playwright in 10 DaysDay 6: Network Control and Mocking

Day 6: Network Control and Mocking

What You Will Learn Today

  • Monitoring network requests with page.on('request') and page.on('response')
  • Waiting for network events: waitForRequest(), waitForResponse()
  • Route interception with page.route()
  • Mocking API responses with route.fulfill()
  • Modifying requests with route.continue()
  • Aborting requests with route.abort()
  • HAR recording and replay with page.routeFromHAR()
  • Emulating network conditions (offline, slow 3G)
  • API testing with request context (request.get(), request.post())
  • Hands-on: testing with a mocked backend

The Network Control Landscape

Playwright provides a rich set of APIs for complete control over browser network communication.

flowchart TB
    subgraph Monitor["Monitoring"]
        OnReq["page.on('request')"]
        OnRes["page.on('response')"]
    end

    subgraph Wait["Waiting"]
        WaitReq["waitForRequest()"]
        WaitRes["waitForResponse()"]
    end

    subgraph Intercept["Interception"]
        Route["page.route()"]
        Fulfill["route.fulfill()"]
        Continue["route.continue()"]
        Abort["route.abort()"]
    end

    subgraph Advanced["Advanced"]
        HAR["routeFromHAR()"]
        Offline["Offline\nEmulation"]
        API["API Testing\nrequest context"]
    end

    Monitor --> Wait --> Intercept --> Advanced

    style Monitor fill:#3b82f6,color:#fff
    style Wait fill:#8b5cf6,color:#fff
    style Intercept fill:#f59e0b,color:#fff
    style Advanced fill:#22c55e,color:#fff

Monitoring Network Requests

page.on('request') and page.on('response')

You can observe all network requests and responses in real time as they occur on the page.

import { test, expect } from '@playwright/test';

test('monitor network requests', async ({ page }) => {
  const requests: string[] = [];

  // All requests
  page.on('request', (request) => {
    console.log(`>> ${request.method()} ${request.url()}`);
    requests.push(request.url());
  });

  // All responses
  page.on('response', (response) => {
    console.log(`<< ${response.status()} ${response.url()}`);
  });

  await page.goto('https://example.com');
  expect(requests.length).toBeGreaterThan(0);
});

Detecting Failed Requests

page.on('requestfailed', (request) => {
  console.log(`FAILED: ${request.url()} - ${request.failure()?.errorText}`);
});

Waiting for Requests

waitForRequest() and waitForResponse()

You can wait for specific requests or responses to occur before proceeding.

test('wait for specific API call', async ({ page }) => {
  // Wait by URL string
  const responsePromise = page.waitForResponse('**/api/users');

  await page.goto('/users');

  const response = await responsePromise;
  expect(response.status()).toBe(200);

  const data = await response.json();
  expect(data).toHaveLength(3);
});

Conditional Waiting

test('wait with predicate', async ({ page }) => {
  // Wait with a predicate function
  const responsePromise = page.waitForResponse(
    (response) =>
      response.url().includes('/api/users') &&
      response.status() === 200
  );

  await page.click('#load-users');

  const response = await responsePromise;
  const users = await response.json();
  expect(users.length).toBeGreaterThan(0);
});

Waiting for POST Requests

test('wait for POST request', async ({ page }) => {
  const requestPromise = page.waitForRequest(
    (request) =>
      request.url().includes('/api/users') &&
      request.method() === 'POST'
  );

  await page.fill('#name', 'John Smith');
  await page.click('#submit');

  const request = await requestPromise;
  const postData = request.postDataJSON();
  expect(postData.name).toBe('John Smith');
});

Route Interception with page.route()

Basic Usage

page.route() is the central API for intercepting requests and choosing to mock, modify, or abort them.

flowchart LR
    Browser["Browser"] --> |"Request"| Route["page.route()"]
    Route --> |"fulfill()"| Mock["Mock Response"]
    Route --> |"continue()"| Server["Server\n(modified)"]
    Route --> |"abort()"| Block["Blocked"]

    style Browser fill:#3b82f6,color:#fff
    style Route fill:#f59e0b,color:#fff
    style Mock fill:#8b5cf6,color:#fff
    style Server fill:#22c55e,color:#fff
    style Block fill:#ef4444,color:#fff

URL Pattern Matching

// Glob patterns
await page.route('**/api/users', handler);
await page.route('**/api/users/*', handler);

// Regular expressions
await page.route(/\/api\/users\/\d+/, handler);

// Predicate function
await page.route(
  (url) => url.pathname.startsWith('/api/'),
  handler
);

route.fulfill() - Mocking Responses

Returning JSON Responses

test('mock API response', async ({ page }) => {
  await page.route('**/api/users', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'John Smith', email: 'john@example.com' },
        { id: 2, name: 'Jane Doe', email: 'jane@example.com' },
      ]),
    });
  });

  await page.goto('/users');
  await expect(page.locator('.user-card')).toHaveCount(2);
  await expect(page.locator('.user-card').first()).toContainText('John Smith');
});

Simulating Error Responses

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

  await page.goto('/users');
  await expect(page.locator('.error-message')).toBeVisible();
});

Returning Responses from Files

test('mock with file', async ({ page }) => {
  await page.route('**/api/users', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      path: 'tests/fixtures/users.json',
    });
  });

  await page.goto('/users');
});

route.continue() - Modifying Requests

You can forward requests to the server while modifying headers, URLs, or other properties.

test('modify request headers', async ({ page }) => {
  await page.route('**/api/**', async (route) => {
    await route.continue({
      headers: {
        ...route.request().headers(),
        'X-Custom-Header': 'test-value',
        'Authorization': 'Bearer mock-token',
      },
    });
  });

  await page.goto('/dashboard');
});

URL Rewriting

test('rewrite API URL', async ({ page }) => {
  // Redirect staging API to local API
  await page.route('**/api.staging.example.com/**', async (route) => {
    const url = route.request().url().replace(
      'api.staging.example.com',
      'localhost:3001'
    );
    await route.continue({ url });
  });
});

Modifying Responses

test('modify response data', async ({ page }) => {
  await page.route('**/api/users', async (route) => {
    const response = await route.fetch();
    const json = await response.json();

    // Add a test user to the response
    json.push({ id: 999, name: 'Test User', email: 'test@example.com' });

    await route.fulfill({
      response,
      body: JSON.stringify(json),
    });
  });

  await page.goto('/users');
});

route.abort() - Blocking Requests

Block unnecessary requests (images, analytics, etc.) to speed up tests.

test('block images and analytics', async ({ page }) => {
  await page.route('**/*.{png,jpg,jpeg,gif,svg}', (route) => route.abort());
  await page.route('**/analytics/**', (route) => route.abort());
  await page.route('**/ads/**', (route) => route.abort());

  await page.goto('/');
  await expect(page.locator('h1')).toBeVisible();
});

Filtering by Resource Type

await page.route('**/*', async (route) => {
  const resourceType = route.request().resourceType();
  if (['image', 'font', 'stylesheet'].includes(resourceType)) {
    await route.abort();
  } else {
    await route.continue();
  }
});
Use Case for abort() Description
Speed up tests Skip loading unnecessary resources
Remove external dependencies Block third-party scripts
Error testing Simulate network errors

HAR Recording and Replay

What is HAR?

HAR (HTTP Archive) is a format for recording browser network traffic. You can capture real API responses and replay them during tests.

flowchart LR
    subgraph Record["Recording Phase"]
        R1["Real API traffic"] --> R2["Save to HAR file"]
    end

    subgraph Replay["Replay Phase"]
        P1["Load HAR file"] --> P2["Return recorded responses"]
    end

    Record --> Replay

    style Record fill:#3b82f6,color:#fff
    style Replay fill:#22c55e,color:#fff

Recording HAR

test('record HAR', async ({ page }) => {
  // Start recording
  await page.routeFromHAR('tests/fixtures/api.har', {
    update: true,  // Record mode
    url: '**/api/**',
  });

  await page.goto('/dashboard');
  await page.click('#load-data');
  await page.waitForResponse('**/api/data');
});

Replaying HAR

test('replay from HAR', async ({ page }) => {
  // Replay mode (default)
  await page.routeFromHAR('tests/fixtures/api.har', {
    url: '**/api/**',
  });

  await page.goto('/dashboard');
  await expect(page.locator('.data-table')).toBeVisible();
});

Emulating Network Conditions

Offline Mode

test('offline mode', async ({ page, context }) => {
  await page.goto('/');

  // Go offline
  await context.setOffline(true);

  await page.click('#load-data');
  await expect(page.locator('.offline-message')).toBeVisible();

  // Go back online
  await context.setOffline(false);

  await page.click('#retry');
  await expect(page.locator('.data-loaded')).toBeVisible();
});

Slow Network Simulation

You can emulate slow connections using CDP (Chrome DevTools Protocol) with Chromium-based browsers.

test('slow 3G simulation', async ({ page }) => {
  const cdpSession = await page.context().newCDPSession(page);

  await cdpSession.send('Network.emulateNetworkConditions', {
    offline: false,
    downloadThroughput: (500 * 1024) / 8,  // 500kb/s
    uploadThroughput: (500 * 1024) / 8,
    latency: 400,  // 400ms RTT
  });

  await page.goto('/');
  // Slow loading behavior can be verified here
});

API Testing with Request Context

Playwright can test APIs directly without a browser, using the built-in request context.

Basic API Tests

import { test, expect } from '@playwright/test';

test.describe('API Tests', () => {
  const BASE_URL = 'https://jsonplaceholder.typicode.com';

  test('GET /users', async ({ request }) => {
    const response = await request.get(`${BASE_URL}/users`);

    expect(response.ok()).toBeTruthy();
    expect(response.status()).toBe(200);

    const users = await response.json();
    expect(users).toHaveLength(10);
    expect(users[0]).toHaveProperty('name');
  });

  test('POST /posts', async ({ request }) => {
    const response = await request.post(`${BASE_URL}/posts`, {
      data: {
        title: 'Test Post',
        body: 'This is a test.',
        userId: 1,
      },
    });

    expect(response.status()).toBe(201);
    const post = await response.json();
    expect(post.title).toBe('Test Post');
  });

  test('PUT /posts/1', async ({ request }) => {
    const response = await request.put(`${BASE_URL}/posts/1`, {
      data: {
        title: 'Updated Title',
        body: 'Updated body.',
        userId: 1,
      },
    });

    expect(response.ok()).toBeTruthy();
    const post = await response.json();
    expect(post.title).toBe('Updated Title');
  });

  test('DELETE /posts/1', async ({ request }) => {
    const response = await request.delete(`${BASE_URL}/posts/1`);
    expect(response.ok()).toBeTruthy();
  });
});

Authenticated API Tests

test('authenticated API request', async ({ request }) => {
  // Login to get token
  const loginResponse = await request.post('/api/login', {
    data: { email: 'user@example.com', password: 'password123' },
  });
  const { token } = await loginResponse.json();

  // Use token for subsequent requests
  const response = await request.get('/api/profile', {
    headers: { Authorization: `Bearer ${token}` },
  });

  expect(response.ok()).toBeTruthy();
  const profile = await response.json();
  expect(profile.email).toBe('user@example.com');
});

Hands-On: Testing with a Mocked Backend

Complete TODO App Test Suite

import { test, expect, Page } from '@playwright/test';

const mockTodos = [
  { id: 1, title: 'Go shopping', completed: false },
  { id: 2, title: 'Clean the house', completed: true },
  { id: 3, title: 'Cook dinner', completed: false },
];

async function setupMockAPI(page: Page) {
  // GET /api/todos
  await page.route('**/api/todos', async (route) => {
    if (route.request().method() === 'GET') {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify(mockTodos),
      });
    } else if (route.request().method() === 'POST') {
      const body = route.request().postDataJSON();
      await route.fulfill({
        status: 201,
        contentType: 'application/json',
        body: JSON.stringify({ id: 4, ...body }),
      });
    }
  });

  // PUT or DELETE /api/todos/:id
  await page.route('**/api/todos/*', async (route) => {
    if (route.request().method() === 'PUT') {
      const body = route.request().postDataJSON();
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify(body),
      });
    } else if (route.request().method() === 'DELETE') {
      await route.fulfill({ status: 204, body: '' });
    }
  });
}

test.describe('TODO App with Mocked Backend', () => {
  test.beforeEach(async ({ page }) => {
    await setupMockAPI(page);
    await page.goto('/todos');
  });

  test('display todo list', async ({ page }) => {
    await expect(page.locator('.todo-item')).toHaveCount(3);
    await expect(page.locator('.todo-item').first()).toContainText('Go shopping');
  });

  test('add new todo', async ({ page }) => {
    await page.fill('#new-todo', 'Study');
    await page.click('button.add');

    const response = await page.waitForResponse('**/api/todos');
    expect(response.status()).toBe(201);
  });

  test('show error on server failure', async ({ page }) => {
    // Override the POST handler with an error
    await page.route('**/api/todos', async (route) => {
      if (route.request().method() === 'POST') {
        await route.fulfill({
          status: 500,
          contentType: 'application/json',
          body: JSON.stringify({ error: 'Server Error' }),
        });
      } else {
        await route.continue();
      }
    });

    await page.fill('#new-todo', 'Test');
    await page.click('button.add');
    await expect(page.locator('.error-message')).toBeVisible();
  });
});

Summary

Category API Purpose
Monitor page.on('request') Observe outgoing requests
Monitor page.on('response') Observe incoming responses
Wait waitForRequest() Wait for a specific request
Wait waitForResponse() Wait for a specific response
Mock route.fulfill() Return a mocked response
Modify route.continue() Forward with modifications
Block route.abort() Block a request entirely
HAR routeFromHAR() Record and replay traffic
Offline context.setOffline() Simulate offline mode
API Test request.get() etc. Test APIs without a browser

Key Takeaways

  1. page.route() is the core - Playwright's network control revolves around page.route() with three actions: fulfill, continue, and abort
  2. Stabilize tests with mocks - Avoid backend dependencies by using deterministic mock responses for reliable tests
  3. Speed up with route.abort() - Block unnecessary resources like images and analytics to improve test execution time
  4. Use HAR for real data - Record and replay actual API responses to maintain realistic test data without manual fixture creation
  5. Combine browser and API tests - Use the request context for fast API-level checks alongside browser-based E2E tests

Exercises

Basics

  1. Use page.route() and route.fulfill() to return a mock response for a GET request
  2. Use waitForResponse() to wait for a specific API response and verify its status code and body
  3. Use route.abort() to block image requests and compare the difference in test speed

Intermediate

  1. Use route.continue() to add a custom authentication token header to requests
  2. Mock error responses (404, 500) and test the error handling behavior of the UI
  3. Use context.setOffline(true) to test offline mode and verify recovery after reconnection

Challenge

  1. Implement HAR file recording and replay using routeFromHAR()
  2. Build a CRUD API test suite using the request context and combine it with browser-based tests

References


Next Up

In Day 7, you will learn about Fixtures and Page Object Model. You will master Playwright's fixture system and the Page Object Model pattern to dramatically improve the reusability and maintainability of your test code.