level 05 / mocking-and-chaining

API Mocking & Chaining

Mock network responses in browser tests and chain API calls to build dependent test data.

Two Types of Mocking

page.route()Mock server (MSW, Nock)
ScopeBrowser network layerNode.js / process level
UseUI tests — intercept XHR/fetchAPI unit/integration tests
PersistencePer test / pageAcross test suite
HTTPS✅ (Playwright handles certs)Needs extra config

page.route() — Browser Intercept

test('cart shows error when checkout API fails', async ({ page }) => {
  await page.route('**/api/checkout', (route) =>
    route.fulfill({
      status: 503,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Service unavailable' }),
    })
  );

  await page.goto('/cart');
  await page.getByRole('button', { name: 'Checkout' }).click();
  await expect(page.getByText('Service unavailable')).toBeVisible();
});

Partial Mocking — Modify Real Response

test('inject extra product into real API response', async ({ page }) => {
  await page.route('**/api/products', async (route) => {
    const res = await route.fetch();
    const body = await res.json();
    body.push({ id: 999, name: 'Test Product', price: 0 });
    await route.fulfill({ response: res, json: body });
  });

  await page.goto('/products');
  await expect(page.getByText('Test Product')).toBeVisible();
});

API Chaining

Create dependent resources by chaining API calls — each response feeds the next:

test('order flow: create user → create product → place order → verify', async ({ request }) => {
  // Step 1: Create user
  const userRes = await request.post('/users', {
    data: { name: 'Test User', email: 'test@example.com' },
  });
  const { id: userId } = await userRes.json();

  // Step 2: Create product
  const productRes = await request.post('/products', {
    data: { name: 'Widget', price: 9.99, stock: 10 },
  });
  const { id: productId } = await productRes.json();

  // Step 3: Place order (uses both IDs)
  const orderRes = await request.post('/orders', {
    data: { userId, productId, quantity: 2 },
  });
  expect(orderRes.status()).toBe(201);
  const { id: orderId } = await orderRes.json();

  // Step 4: Verify order
  const verify = await request.get(`/orders/${orderId}`);
  const order = await verify.json();
  expect(order.userId).toBe(userId);
  expect(order.total).toBe(19.98);
});

Teardown with test.afterEach

Clean up created resources:

let createdIds: string[] = [];

test.afterEach(async ({ request }) => {
  for (const id of createdIds.reverse()) {
    await request.delete(`/bookings/${id}`);
  }
  createdIds = [];
});

test('create and verify booking', async ({ request }) => {
  const res = await request.post('/bookings', { data: { /* ... */ } });
  const { id } = await res.json();
  createdIds.push(id);
  // ... test assertions
});

Route Abort and Delay

// Abort — simulate network failure
await page.route('**/api/analytics', (route) => route.abort());

// Delay — simulate slow API
await page.route('**/api/feed', async (route) => {
  await new Promise((r) => setTimeout(r, 3000));
  await route.continue();
});