|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- # Intercept Network Call Utility
-
- ## Principle
-
- Intercept network requests with a single declarative call that returns a Promise. Automatically parse JSON responses, support both spy (observe) and stub (mock) patterns, and use powerful glob pattern matching for URL filtering.
-
- ## Rationale
-
- Vanilla Playwright's network interception requires multiple steps:
-
- - `page.route()` to setup, `page.waitForResponse()` to capture
- - Manual JSON parsing
- - Verbose syntax for conditional handling
- - Complex filter predicates
-
- The `interceptNetworkCall` utility provides:
-
- - **Single declarative call**: Setup and wait in one statement
- - **Automatic JSON parsing**: Response pre-parsed, strongly typed
- - **Flexible URL patterns**: Glob matching with picomatch
- - **Spy or stub modes**: Observe real traffic or mock responses
- - **Concise API**: Reduces boilerplate by 60-70%
-
- ## Pattern Examples
-
- ### Example 1: Spy on Network (Observe Real Traffic)
-
- **Context**: Capture and inspect real API responses for validation.
-
- **Implementation**:
-
- ```typescript
- import { test } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
-
- test('should spy on users API', async ({ page, interceptNetworkCall }) => {
- // Setup interception BEFORE navigation
- const usersCall = interceptNetworkCall({
- url: '**/api/users', // Glob pattern
- });
-
- await page.goto('/dashboard');
-
- // Wait for response and access parsed data
- const { responseJson, status } = await usersCall;
-
- expect(status).toBe(200);
- expect(responseJson).toHaveLength(10);
- expect(responseJson[0]).toHaveProperty('name');
- });
- ```
-
- **Key Points**:
-
- - Intercept before navigation (critical for race-free tests)
- - Returns Promise with `{ responseJson, status, requestBody }`
- - Glob patterns (`**` matches any path segment)
- - JSON automatically parsed
-
- ### Example 2: Stub Network (Mock Response)
-
- **Context**: Mock API responses for testing UI behavior without backend.
-
- **Implementation**:
-
- ```typescript
- test('should stub users API', async ({ page, interceptNetworkCall }) => {
- const mockUsers = [
- { id: 1, name: 'Test User 1' },
- { id: 2, name: 'Test User 2' },
- ];
-
- const usersCall = interceptNetworkCall({
- url: '**/api/users',
- fulfillResponse: {
- status: 200,
- body: mockUsers,
- },
- });
-
- await page.goto('/dashboard');
- await usersCall;
-
- // UI shows mocked data
- await expect(page.getByText('Test User 1')).toBeVisible();
- await expect(page.getByText('Test User 2')).toBeVisible();
- });
- ```
-
- **Key Points**:
-
- - `fulfillResponse` mocks the API
- - No backend needed
- - Test UI logic in isolation
- - Status code and body fully controllable
-
- ### Example 3: Conditional Response Handling
-
- **Context**: Different responses based on request method or parameters.
-
- **Implementation**:
-
- ```typescript
- test('conditional mocking', async ({ page, interceptNetworkCall }) => {
- await interceptNetworkCall({
- url: '**/api/data',
- handler: async (route, request) => {
- if (request.method() === 'POST') {
- // Mock POST success
- await route.fulfill({
- status: 201,
- body: JSON.stringify({ id: 'new-id', success: true }),
- });
- } else if (request.method() === 'GET') {
- // Mock GET with data
- await route.fulfill({
- status: 200,
- body: JSON.stringify([{ id: 1, name: 'Item' }]),
- });
- } else {
- // Let other methods through
- await route.continue();
- }
- },
- });
-
- await page.goto('/data-page');
- });
- ```
-
- **Key Points**:
-
- - `handler` function for complex logic
- - Access full `route` and `request` objects
- - Can mock, continue, or abort
- - Flexible for advanced scenarios
-
- ### Example 4: Error Simulation
-
- **Context**: Testing error handling in UI when API fails.
-
- **Implementation**:
-
- ```typescript
- test('should handle API errors gracefully', async ({ page, interceptNetworkCall }) => {
- // Simulate 500 error
- const errorCall = interceptNetworkCall({
- url: '**/api/users',
- fulfillResponse: {
- status: 500,
- body: { error: 'Internal Server Error' },
- },
- });
-
- await page.goto('/dashboard');
- await errorCall;
-
- // Verify UI shows error state
- await expect(page.getByText('Failed to load users')).toBeVisible();
- await expect(page.getByTestId('retry-button')).toBeVisible();
- });
-
- // Simulate network timeout
- test('should handle timeout', async ({ page, interceptNetworkCall }) => {
- await interceptNetworkCall({
- url: '**/api/slow',
- handler: async (route) => {
- // Never respond - simulates timeout
- await new Promise(() => {});
- },
- });
-
- await page.goto('/slow-page');
-
- // UI should show timeout error
- await expect(page.getByText('Request timed out')).toBeVisible({ timeout: 10000 });
- });
- ```
-
- **Key Points**:
-
- - Mock error statuses (4xx, 5xx)
- - Test timeout scenarios
- - Validate error UI states
- - No real failures needed
-
- ### Example 5: Order Matters - Intercept Before Navigate
-
- **Context**: The interceptor must be set up before the network request occurs.
-
- **Implementation**:
-
- ```typescript
- // INCORRECT - interceptor set up too late
- await page.goto('https://example.com'); // Request already happened
- const networkCall = interceptNetworkCall({ url: '**/api/data' });
- await networkCall; // Will hang indefinitely!
-
- // CORRECT - Set up interception first
- const networkCall = interceptNetworkCall({ url: '**/api/data' });
- await page.goto('https://example.com');
- const result = await networkCall;
- ```
-
- This pattern follows the classic test spy/stub pattern:
-
- 1. Define the spy/stub (set up interception)
- 2. Perform the action (trigger the network request)
- 3. Assert on the spy/stub (await and verify the response)
-
- ### Example 6: Multiple Intercepts
-
- **Context**: Intercepting different endpoints in same test - setup order is critical.
-
- **Implementation**:
-
- ```typescript
- test('multiple intercepts', async ({ page, interceptNetworkCall }) => {
- // Setup all intercepts BEFORE navigation
- const usersCall = interceptNetworkCall({ url: '**/api/users' });
- const productsCall = interceptNetworkCall({ url: '**/api/products' });
- const ordersCall = interceptNetworkCall({ url: '**/api/orders' });
-
- // THEN navigate
- await page.goto('/dashboard');
-
- // Wait for all (or specific ones)
- const [users, products] = await Promise.all([usersCall, productsCall]);
-
- expect(users.responseJson).toHaveLength(10);
- expect(products.responseJson).toHaveLength(50);
- });
- ```
-
- **Key Points**:
-
- - Setup all intercepts before triggering actions
- - Use `Promise.all()` to wait for multiple calls
- - Order: intercept -> navigate -> await
- - Prevents race conditions
-
- ### Example 7: Capturing Multiple Requests to the Same Endpoint
-
- **Context**: Each `interceptNetworkCall` captures only the first matching request.
-
- **Implementation**:
-
- ```typescript
- // Capturing a known number of requests
- const firstRequest = interceptNetworkCall({ url: '/api/data' });
- const secondRequest = interceptNetworkCall({ url: '/api/data' });
-
- await page.click('#load-data-button');
-
- const firstResponse = await firstRequest;
- const secondResponse = await secondRequest;
-
- expect(firstResponse.status).toBe(200);
- expect(secondResponse.status).toBe(200);
-
- // Handling an unknown number of requests
- const getDataRequestInterceptor = () =>
- interceptNetworkCall({
- url: '/api/data',
- timeout: 1000, // Short timeout to detect when no more requests are coming
- });
-
- let currentInterceptor = getDataRequestInterceptor();
- const allResponses = [];
-
- await page.click('#load-multiple-data-button');
-
- while (true) {
- try {
- const response = await currentInterceptor;
- allResponses.push(response);
- currentInterceptor = getDataRequestInterceptor();
- } catch (error) {
- // No more requests (timeout)
- break;
- }
- }
-
- console.log(`Captured ${allResponses.length} requests to /api/data`);
- ```
-
- ### Example 8: Using Timeout
-
- **Context**: Set a timeout for waiting on a network request.
-
- **Implementation**:
-
- ```typescript
- const dataCall = interceptNetworkCall({
- method: 'GET',
- url: '/api/data-that-might-be-slow',
- timeout: 5000, // 5 seconds timeout
- });
-
- await page.goto('/data-page');
-
- try {
- const { responseJson } = await dataCall;
- console.log('Data loaded successfully:', responseJson);
- } catch (error) {
- if (error.message.includes('timeout')) {
- console.log('Request timed out as expected');
- } else {
- throw error;
- }
- }
- ```
-
- ## URL Pattern Matching
-
- The utility uses [picomatch](https://github.com/micromatch/picomatch) for powerful glob pattern matching, dramatically simplifying URL targeting:
-
- **Supported glob patterns:**
-
- ```typescript
- '**/api/users'; // Any path ending with /api/users
- '/api/users'; // Exact match
- '**/users/*'; // Any users sub-path
- '**/api/{users,products}'; // Either users or products
- '**/api/users?id=*'; // With query params
- ```
-
- **Comparison with vanilla Playwright:**
-
- ```typescript
- // Vanilla Playwright - complex predicate
- const predicate = (response) => {
- const url = response.url();
- return url.endsWith('/api/users') || url.match(/\/api\/users\/\d+/) || (url.includes('/api/users/') && url.includes('/profile'));
- };
- page.waitForResponse(predicate);
-
- // With interceptNetworkCall - simple glob patterns
- interceptNetworkCall({ url: '/api/users' }); // Exact endpoint
- interceptNetworkCall({ url: '/api/users/*' }); // User by ID pattern
- interceptNetworkCall({ url: '/api/users/*/profile' }); // Specific sub-paths
- interceptNetworkCall({ url: '/api/users/**' }); // Match all
- ```
-
- ## API Reference
-
- ### `interceptNetworkCall(options)`
-
- | Parameter | Type | Description |
- | ----------------- | ---------- | --------------------------------------------------------------------- |
- | `page` | `Page` | Required when using direct import (not needed with fixture) |
- | `method` | `string` | Optional: HTTP method to match (e.g., 'GET', 'POST') |
- | `url` | `string` | Optional: URL pattern to match (supports glob patterns via picomatch) |
- | `fulfillResponse` | `object` | Optional: Response to use when mocking |
- | `handler` | `function` | Optional: Custom handler function for the route |
- | `timeout` | `number` | Optional: Timeout in milliseconds for the network request |
-
- ### `fulfillResponse` Object
-
- | Property | Type | Description |
- | --------- | ------------------------ | ----------------------------------------------------- |
- | `status` | `number` | HTTP status code (default: 200) |
- | `headers` | `Record<string, string>` | Response headers |
- | `body` | `any` | Response body (will be JSON.stringified if an object) |
-
- ### Return Value
-
- Returns a `Promise<NetworkCallResult>` with:
-
- | Property | Type | Description |
- | -------------- | ---------- | --------------------------------------- |
- | `request` | `Request` | The intercepted request |
- | `response` | `Response` | The response (null if mocked) |
- | `responseJson` | `any` | Parsed JSON response (if available) |
- | `status` | `number` | HTTP status code |
- | `requestJson` | `any` | Parsed JSON request body (if available) |
-
- ## Comparison with Vanilla Playwright
-
- | Vanilla Playwright | intercept-network-call |
- | ----------------------------------------------------------- | ------------------------------------------------------------ |
- | `await page.route('/api/users', route => route.continue())` | `const call = interceptNetworkCall({ url: '**/api/users' })` |
- | `const resp = await page.waitForResponse('/api/users')` | (Combined in single statement) |
- | `const json = await resp.json()` | `const { responseJson } = await call` |
- | `const status = resp.status()` | `const { status } = await call` |
- | Complex filter predicates | Simple glob patterns |
-
- **Reduction:** ~5-7 lines -> ~2-3 lines per interception
-
- ## Related Fragments
-
- - `network-first.md` - Core pattern: intercept before navigate
- - `network-recorder.md` - HAR-based offline testing
- - `overview.md` - Fixture composition basics
-
- ## Anti-Patterns
-
- **DON'T intercept after navigation:**
-
- ```typescript
- await page.goto('/dashboard'); // Navigation starts
- const usersCall = interceptNetworkCall({ url: '**/api/users' }); // Too late!
- ```
-
- **DO intercept before navigate:**
-
- ```typescript
- const usersCall = interceptNetworkCall({ url: '**/api/users' }); // First
- await page.goto('/dashboard'); // Then navigate
- const { responseJson } = await usersCall; // Then await
- ```
-
- **DON'T ignore the returned Promise:**
-
- ```typescript
- interceptNetworkCall({ url: '**/api/users' }); // Not awaited!
- await page.goto('/dashboard');
- // No deterministic wait - race condition
- ```
-
- **DO always await the intercept:**
-
- ```typescript
- const usersCall = interceptNetworkCall({ url: '**/api/users' });
- await page.goto('/dashboard');
- await usersCall; // Deterministic wait
- ```
|