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.

Senior engineer insight

The moment that changed how I think about network interception was discovering that our most unreliable tests — the ones that failed in CI once a week but never locally — were all hitting a real third-party payment gateway. Switching those to route.fulfill() with scripted responses eliminated the flakiness entirely. The tests weren't testing our code anymore; they were testing Stripe's sandbox uptime. Separating those concerns was the breakthrough.

The most common mistake teams make is treating network mocking as "cheating" and insisting on real API calls throughout the suite — then wondering why CI is unreliable and slow. Real API calls belong in integration tests with explicit contracts; everything else should be intercepted.

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 Revenue NZ API response.

test('handles Revenue NZ validation error gracefully', async ({ page }) => {
  // Intercept the Revenue NZ 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 Revenue NZ number provided is not valid',
      }),
    });
  });

  await page.goto('/enrolment/step-2');
  await page.getByLabel('Revenue NZ 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.

From the field

A NZ local government team building a rates-calculator portal assumed their suite was solid — 90 tests, all green on their machines, CI passing most of the time. When they audited the suite they found that 40 tests were hitting the real council backend to seed fixture data, and another 20 were logging in fresh on every run. Every push to the main branch triggered a 22-minute pipeline. They introduced Playwright fixtures to share authenticated state and replaced all council API calls with route.fulfill() using captured response snapshots. Pipeline time dropped to six minutes. More importantly, the tests stopped failing whenever the council staging environment had its weekly maintenance window — because the tests no longer depended on it at all. The lesson: a test suite that relies on external infrastructure isn't a safety net; it's a liability.

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: Revenue NZ, 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 Revenue NZ 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.

Why teams fail here

  • Regenerating auth.json manually in CI instead of via globalSetup — the file goes stale when tokens expire and the whole suite fails with cryptic auth errors rather than a clear login failure.
  • Using wildcard routes like '**' in one test and forgetting to unregister — the intercept bleeds into subsequent tests, causing unrelated tests to receive mocked data and masking real defects.
  • Building Page Object Models (POMs) that embed network mocking logic inside page actions — now the abstraction layer owns both UI interaction and API fakery, making tests impossible to run against a real environment when you need to.
  • Mocking every API call in integration tests — the suite passes confidently while a breaking contract change between the front end and back end ships undetected, because no test ever exercised the real wire.

8 Self-Check

Click each question to reveal the answer.

Interview Questions

What NZ hiring managers ask about advanced Playwright — network interception and authentication.

Q1. How do you intercept and mock network requests in Playwright?

Strong answer: Use page.route() matching a URL pattern to provide mock responses: page.route('**/api/users', route => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{id:1}]) })). This enables testing edge cases (payment failures, rate limits, concurrent updates) without real backend dependencies. Use route.continue() to pass through with optional header/body modifications.

Q2. How do you handle authentication efficiently across a large Playwright suite?

Strong answer: Authenticate once and save browser storage state: context.storageState({ path: 'auth.json' }). Configure test.use({ storageState: 'auth.json' }) for tests requiring authentication. This avoids logging in at the start of every test. For multi-role suites (admin, standard user), create separate state files per role. For OAuth flows, use page.route() to intercept redirects and skip the real provider in test environments.

Q3. What is the Playwright Trace Viewer and how do you use it to debug CI failures?

Strong answer: The Trace Viewer records a full test run timeline: screenshots at each step, network requests, console logs, and DOM snapshots. Enable with trace: "on-first-retry" in playwright.config.ts. When a test fails and retries, the trace is saved. Run npx playwright show-trace trace.zip for an interactive timeline. For NZ teams investigating CI failures asynchronously across time zones, traces eliminate the "works on my machine" conversation by providing complete reproduction context.

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.

Key takeaway

Authenticate once, intercept everything you don't own, and your Playwright suite stops being a test of third-party uptime and starts being a test of your own code.

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.