Lesson 2 of 4 · Playwright Deep-Dive

Advanced Playwright — Network Interception & Auth

Network interception lets you mock API responses, simulate errors, and test edge cases that are impossible to trigger against a real backend. Auth state lets you skip login in every test. These two patterns together make your suite 10× faster and more reliable.

Playwright Deep-Dive CTAL-TAE — Lesson 2 of 4 ~25 min read · ~55 min with exercises

1 The Hook

A test suite for a NZ logistics platform has 150 tests. Every test starts with a login. Login takes 2.5 seconds: the auth service has to round-trip, issue a token, redirect, and load the dashboard. Total time spent on auth alone: 375 seconds per CI run, before a single assertion.

The SDET implements auth state storage — log in once during global setup, save the browser storage state to a JSON file, and load it in every test instead of logging in again. New auth time across 150 tests: 8 seconds.

Time saved per CI run: over 6 minutes. On a team doing 20 PRs a day, that is 2 hours of engineering time recovered daily from one afternoon’s work.

2 The Rule

Never log in inside a test if you can authenticate once and share the session. Never hit a real API in a test if you can intercept and mock the response. Isolate what you’re testing.

3 The Analogy

Analogy

Network interception is like a film studio set.

The actors work in a room that looks exactly like a real office. The phone doesn’t connect to a real phone line. The computer isn’t on the internet. The coffee in the mug is cold. Everything is controlled and reproducible. You can run the same scene 50 times and get the same result.

That’s your test environment with API mocking. The UI behaves as if it’s talking to a real backend. The “backend” is actually Playwright intercepting the request and returning exactly the response you scripted. Controlled. Reproducible. Fast.

4 Watch Me Do It

Auth state: log in once, reuse everywhere.

// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('http://localhost:3000/login');
  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
  await page.getByRole('button', { name: 'Log in' }).click();
  await page.waitForURL('**/dashboard');
  // Save cookies + localStorage to a file
  await page.context().storageState({ path: 'auth.json' });
  await browser.close();
}

export default globalSetup;
// playwright.config.ts — add these two lines:
globalSetup: './global-setup.ts',
use: { storageState: 'auth.json' },

Every test now starts with an already-authenticated browser context. The login page is never loaded during the test run itself.

Pro tip: Add auth.json to .gitignore. It contains session tokens. Regenerate it in CI via globalSetup using secrets.

Network interception: mock an IRD API response.

test('handles IRD validation error gracefully', async ({ page }) => {
  // Intercept the IRD number validation API and return a 422
  await page.route('**/api/ird/validate**', async route => {
    await route.fulfill({
      status: 422,
      contentType: 'application/json',
      body: JSON.stringify({
        error: 'IRD_INVALID',
        message: 'The IRD number provided is not valid',
      }),
    });
  });

  await page.goto('/enrolment/step-2');
  await page.getByLabel('IRD Number').fill('000-000-000');
  await page.getByRole('button', { name: 'Validate' }).click();

  await expect(page.getByRole('alert')).toContainText('not valid');
  await expect(page.getByRole('button', { name: 'Next' })).toBeDisabled();
});

Route abort: simulate a network failure.

test('shows offline message when API is unreachable', async ({ page }) => {
  await page.route('**/api/**', route => route.abort('failed'));
  await page.goto('/dashboard');
  await expect(page.getByText('Unable to connect')).toBeVisible();
});

route.abort() drops the connection entirely. Use it to test how the UI handles a total network failure, not just a server error response.

5 When to Use It

Auth state — always, for any project where tests require authentication. No exceptions.

Network mocking — for testing error states, edge cases, rate limiting, and third-party API dependencies you cannot control: IRD, RealMe, Stripe, Open Banking APIs. If a test relies on a third-party returning a specific error code, mock it. You cannot control when that third party returns that code in a real environment.

Do not mock your own API when testing integration between front end and back end. Use mocking to isolate what you are testing; use real API calls when integration is what you are testing.

6 Common Mistakes

✗ I used to think: mocking an API response is cheating.

Actually: you are testing your front end’s handling of a response. The backend is tested separately with its own tests. Mocking isolates what you are testing, makes tests 10× more reliable, and lets you trigger error states that are impossible to reproduce against a real backend — like a 503 from IRD at exactly the wrong moment.

✗ I used to think: auth.json can be committed to the repository.

Actually: auth.json contains live session tokens. Committing it exposes those credentials to anyone with repository access, including future access if the repo ever becomes public. Add it to .gitignore and generate it fresh in CI using secrets for the test account credentials.

✗ I used to think: page.route() matches exact URLs.

Actually: it matches glob patterns. '**/api/ird/**' matches any URL containing /api/ird/, regardless of protocol, host, or path prefix. Be specific enough to only intercept the calls you intend to mock. An overly broad pattern can accidentally intercept requests you needed to reach the real server.

7 Now You Try

✎ Prompt Lab — AI Exercise

A NZ banking app calls a real-time exchange rate API. Write a Playwright test that: (1) intercepts the exchange rate API call, (2) returns a mocked USD/NZD rate of 0.61, (3) verifies the UI displays the converted amount correctly, and (4) verifies the test still works when the mock returns a 503 Service Unavailable.

8 Self-Check

Click each question to reveal the answer.

Why shouldn’t auth.json be committed to source control?

auth.json contains live session tokens for your test account. If the repository is ever made public, or if an attacker gains read access, those tokens can be used to impersonate the test account. Generate auth.json fresh in CI using encrypted secrets, and add it to .gitignore so it is never committed.

What glob pattern would intercept all calls to /api/v2/payments?

**/api/v2/payments** matches any URL containing that path regardless of host, protocol, or query string. If you want to be more specific (e.g. only your staging host), use https://staging.myapp.co.nz/api/v2/payments**. More specific patterns are safer — they won’t accidentally intercept requests you needed to reach the real endpoint.

When should you use route.abort() vs route.fulfill()?

Use route.abort() to simulate a network-level failure — the request never reaches a server, the connection drops. Use it to test how your UI handles complete connectivity loss. Use route.fulfill() when you want the request to succeed at the network level but return a specific HTTP response body and status code. Abort tests resilience to network failure; fulfill tests UI behaviour for specific server responses.

9 ISTQB Mapping

CTAL-TAE Section 5.2 — Test doubles: mocking and stubbing in automation. Network interception in Playwright is a practical implementation of the stub pattern — replacing a dependency (the real API) with a controllable substitute that returns predetermined responses. Auth state storage is a form of test fixture management, reducing setup time while maintaining test isolation.