Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

intercept-network-call.md 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. # Intercept Network Call Utility
  2. ## Principle
  3. 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.
  4. ## Rationale
  5. Vanilla Playwright's network interception requires multiple steps:
  6. - `page.route()` to setup, `page.waitForResponse()` to capture
  7. - Manual JSON parsing
  8. - Verbose syntax for conditional handling
  9. - Complex filter predicates
  10. The `interceptNetworkCall` utility provides:
  11. - **Single declarative call**: Setup and wait in one statement
  12. - **Automatic JSON parsing**: Response pre-parsed, strongly typed
  13. - **Flexible URL patterns**: Glob matching with picomatch
  14. - **Spy or stub modes**: Observe real traffic or mock responses
  15. - **Concise API**: Reduces boilerplate by 60-70%
  16. ## Pattern Examples
  17. ### Example 1: Spy on Network (Observe Real Traffic)
  18. **Context**: Capture and inspect real API responses for validation.
  19. **Implementation**:
  20. ```typescript
  21. import { test } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
  22. test('should spy on users API', async ({ page, interceptNetworkCall }) => {
  23. // Setup interception BEFORE navigation
  24. const usersCall = interceptNetworkCall({
  25. url: '**/api/users', // Glob pattern
  26. });
  27. await page.goto('/dashboard');
  28. // Wait for response and access parsed data
  29. const { responseJson, status } = await usersCall;
  30. expect(status).toBe(200);
  31. expect(responseJson).toHaveLength(10);
  32. expect(responseJson[0]).toHaveProperty('name');
  33. });
  34. ```
  35. **Key Points**:
  36. - Intercept before navigation (critical for race-free tests)
  37. - Returns Promise with `{ responseJson, status, requestBody }`
  38. - Glob patterns (`**` matches any path segment)
  39. - JSON automatically parsed
  40. ### Example 2: Stub Network (Mock Response)
  41. **Context**: Mock API responses for testing UI behavior without backend.
  42. **Implementation**:
  43. ```typescript
  44. test('should stub users API', async ({ page, interceptNetworkCall }) => {
  45. const mockUsers = [
  46. { id: 1, name: 'Test User 1' },
  47. { id: 2, name: 'Test User 2' },
  48. ];
  49. const usersCall = interceptNetworkCall({
  50. url: '**/api/users',
  51. fulfillResponse: {
  52. status: 200,
  53. body: mockUsers,
  54. },
  55. });
  56. await page.goto('/dashboard');
  57. await usersCall;
  58. // UI shows mocked data
  59. await expect(page.getByText('Test User 1')).toBeVisible();
  60. await expect(page.getByText('Test User 2')).toBeVisible();
  61. });
  62. ```
  63. **Key Points**:
  64. - `fulfillResponse` mocks the API
  65. - No backend needed
  66. - Test UI logic in isolation
  67. - Status code and body fully controllable
  68. ### Example 3: Conditional Response Handling
  69. **Context**: Different responses based on request method or parameters.
  70. **Implementation**:
  71. ```typescript
  72. test('conditional mocking', async ({ page, interceptNetworkCall }) => {
  73. await interceptNetworkCall({
  74. url: '**/api/data',
  75. handler: async (route, request) => {
  76. if (request.method() === 'POST') {
  77. // Mock POST success
  78. await route.fulfill({
  79. status: 201,
  80. body: JSON.stringify({ id: 'new-id', success: true }),
  81. });
  82. } else if (request.method() === 'GET') {
  83. // Mock GET with data
  84. await route.fulfill({
  85. status: 200,
  86. body: JSON.stringify([{ id: 1, name: 'Item' }]),
  87. });
  88. } else {
  89. // Let other methods through
  90. await route.continue();
  91. }
  92. },
  93. });
  94. await page.goto('/data-page');
  95. });
  96. ```
  97. **Key Points**:
  98. - `handler` function for complex logic
  99. - Access full `route` and `request` objects
  100. - Can mock, continue, or abort
  101. - Flexible for advanced scenarios
  102. ### Example 4: Error Simulation
  103. **Context**: Testing error handling in UI when API fails.
  104. **Implementation**:
  105. ```typescript
  106. test('should handle API errors gracefully', async ({ page, interceptNetworkCall }) => {
  107. // Simulate 500 error
  108. const errorCall = interceptNetworkCall({
  109. url: '**/api/users',
  110. fulfillResponse: {
  111. status: 500,
  112. body: { error: 'Internal Server Error' },
  113. },
  114. });
  115. await page.goto('/dashboard');
  116. await errorCall;
  117. // Verify UI shows error state
  118. await expect(page.getByText('Failed to load users')).toBeVisible();
  119. await expect(page.getByTestId('retry-button')).toBeVisible();
  120. });
  121. // Simulate network timeout
  122. test('should handle timeout', async ({ page, interceptNetworkCall }) => {
  123. await interceptNetworkCall({
  124. url: '**/api/slow',
  125. handler: async (route) => {
  126. // Never respond - simulates timeout
  127. await new Promise(() => {});
  128. },
  129. });
  130. await page.goto('/slow-page');
  131. // UI should show timeout error
  132. await expect(page.getByText('Request timed out')).toBeVisible({ timeout: 10000 });
  133. });
  134. ```
  135. **Key Points**:
  136. - Mock error statuses (4xx, 5xx)
  137. - Test timeout scenarios
  138. - Validate error UI states
  139. - No real failures needed
  140. ### Example 5: Order Matters - Intercept Before Navigate
  141. **Context**: The interceptor must be set up before the network request occurs.
  142. **Implementation**:
  143. ```typescript
  144. // INCORRECT - interceptor set up too late
  145. await page.goto('https://example.com'); // Request already happened
  146. const networkCall = interceptNetworkCall({ url: '**/api/data' });
  147. await networkCall; // Will hang indefinitely!
  148. // CORRECT - Set up interception first
  149. const networkCall = interceptNetworkCall({ url: '**/api/data' });
  150. await page.goto('https://example.com');
  151. const result = await networkCall;
  152. ```
  153. This pattern follows the classic test spy/stub pattern:
  154. 1. Define the spy/stub (set up interception)
  155. 2. Perform the action (trigger the network request)
  156. 3. Assert on the spy/stub (await and verify the response)
  157. ### Example 6: Multiple Intercepts
  158. **Context**: Intercepting different endpoints in same test - setup order is critical.
  159. **Implementation**:
  160. ```typescript
  161. test('multiple intercepts', async ({ page, interceptNetworkCall }) => {
  162. // Setup all intercepts BEFORE navigation
  163. const usersCall = interceptNetworkCall({ url: '**/api/users' });
  164. const productsCall = interceptNetworkCall({ url: '**/api/products' });
  165. const ordersCall = interceptNetworkCall({ url: '**/api/orders' });
  166. // THEN navigate
  167. await page.goto('/dashboard');
  168. // Wait for all (or specific ones)
  169. const [users, products] = await Promise.all([usersCall, productsCall]);
  170. expect(users.responseJson).toHaveLength(10);
  171. expect(products.responseJson).toHaveLength(50);
  172. });
  173. ```
  174. **Key Points**:
  175. - Setup all intercepts before triggering actions
  176. - Use `Promise.all()` to wait for multiple calls
  177. - Order: intercept -> navigate -> await
  178. - Prevents race conditions
  179. ### Example 7: Capturing Multiple Requests to the Same Endpoint
  180. **Context**: Each `interceptNetworkCall` captures only the first matching request.
  181. **Implementation**:
  182. ```typescript
  183. // Capturing a known number of requests
  184. const firstRequest = interceptNetworkCall({ url: '/api/data' });
  185. const secondRequest = interceptNetworkCall({ url: '/api/data' });
  186. await page.click('#load-data-button');
  187. const firstResponse = await firstRequest;
  188. const secondResponse = await secondRequest;
  189. expect(firstResponse.status).toBe(200);
  190. expect(secondResponse.status).toBe(200);
  191. // Handling an unknown number of requests
  192. const getDataRequestInterceptor = () =>
  193. interceptNetworkCall({
  194. url: '/api/data',
  195. timeout: 1000, // Short timeout to detect when no more requests are coming
  196. });
  197. let currentInterceptor = getDataRequestInterceptor();
  198. const allResponses = [];
  199. await page.click('#load-multiple-data-button');
  200. while (true) {
  201. try {
  202. const response = await currentInterceptor;
  203. allResponses.push(response);
  204. currentInterceptor = getDataRequestInterceptor();
  205. } catch (error) {
  206. // No more requests (timeout)
  207. break;
  208. }
  209. }
  210. console.log(`Captured ${allResponses.length} requests to /api/data`);
  211. ```
  212. ### Example 8: Using Timeout
  213. **Context**: Set a timeout for waiting on a network request.
  214. **Implementation**:
  215. ```typescript
  216. const dataCall = interceptNetworkCall({
  217. method: 'GET',
  218. url: '/api/data-that-might-be-slow',
  219. timeout: 5000, // 5 seconds timeout
  220. });
  221. await page.goto('/data-page');
  222. try {
  223. const { responseJson } = await dataCall;
  224. console.log('Data loaded successfully:', responseJson);
  225. } catch (error) {
  226. if (error.message.includes('timeout')) {
  227. console.log('Request timed out as expected');
  228. } else {
  229. throw error;
  230. }
  231. }
  232. ```
  233. ## URL Pattern Matching
  234. The utility uses [picomatch](https://github.com/micromatch/picomatch) for powerful glob pattern matching, dramatically simplifying URL targeting:
  235. **Supported glob patterns:**
  236. ```typescript
  237. '**/api/users'; // Any path ending with /api/users
  238. '/api/users'; // Exact match
  239. '**/users/*'; // Any users sub-path
  240. '**/api/{users,products}'; // Either users or products
  241. '**/api/users?id=*'; // With query params
  242. ```
  243. **Comparison with vanilla Playwright:**
  244. ```typescript
  245. // Vanilla Playwright - complex predicate
  246. const predicate = (response) => {
  247. const url = response.url();
  248. return url.endsWith('/api/users') || url.match(/\/api\/users\/\d+/) || (url.includes('/api/users/') && url.includes('/profile'));
  249. };
  250. page.waitForResponse(predicate);
  251. // With interceptNetworkCall - simple glob patterns
  252. interceptNetworkCall({ url: '/api/users' }); // Exact endpoint
  253. interceptNetworkCall({ url: '/api/users/*' }); // User by ID pattern
  254. interceptNetworkCall({ url: '/api/users/*/profile' }); // Specific sub-paths
  255. interceptNetworkCall({ url: '/api/users/**' }); // Match all
  256. ```
  257. ## API Reference
  258. ### `interceptNetworkCall(options)`
  259. | Parameter | Type | Description |
  260. | ----------------- | ---------- | --------------------------------------------------------------------- |
  261. | `page` | `Page` | Required when using direct import (not needed with fixture) |
  262. | `method` | `string` | Optional: HTTP method to match (e.g., 'GET', 'POST') |
  263. | `url` | `string` | Optional: URL pattern to match (supports glob patterns via picomatch) |
  264. | `fulfillResponse` | `object` | Optional: Response to use when mocking |
  265. | `handler` | `function` | Optional: Custom handler function for the route |
  266. | `timeout` | `number` | Optional: Timeout in milliseconds for the network request |
  267. ### `fulfillResponse` Object
  268. | Property | Type | Description |
  269. | --------- | ------------------------ | ----------------------------------------------------- |
  270. | `status` | `number` | HTTP status code (default: 200) |
  271. | `headers` | `Record<string, string>` | Response headers |
  272. | `body` | `any` | Response body (will be JSON.stringified if an object) |
  273. ### Return Value
  274. Returns a `Promise<NetworkCallResult>` with:
  275. | Property | Type | Description |
  276. | -------------- | ---------- | --------------------------------------- |
  277. | `request` | `Request` | The intercepted request |
  278. | `response` | `Response` | The response (null if mocked) |
  279. | `responseJson` | `any` | Parsed JSON response (if available) |
  280. | `status` | `number` | HTTP status code |
  281. | `requestJson` | `any` | Parsed JSON request body (if available) |
  282. ## Comparison with Vanilla Playwright
  283. | Vanilla Playwright | intercept-network-call |
  284. | ----------------------------------------------------------- | ------------------------------------------------------------ |
  285. | `await page.route('/api/users', route => route.continue())` | `const call = interceptNetworkCall({ url: '**/api/users' })` |
  286. | `const resp = await page.waitForResponse('/api/users')` | (Combined in single statement) |
  287. | `const json = await resp.json()` | `const { responseJson } = await call` |
  288. | `const status = resp.status()` | `const { status } = await call` |
  289. | Complex filter predicates | Simple glob patterns |
  290. **Reduction:** ~5-7 lines -> ~2-3 lines per interception
  291. ## Related Fragments
  292. - `network-first.md` - Core pattern: intercept before navigate
  293. - `network-recorder.md` - HAR-based offline testing
  294. - `overview.md` - Fixture composition basics
  295. ## Anti-Patterns
  296. **DON'T intercept after navigation:**
  297. ```typescript
  298. await page.goto('/dashboard'); // Navigation starts
  299. const usersCall = interceptNetworkCall({ url: '**/api/users' }); // Too late!
  300. ```
  301. **DO intercept before navigate:**
  302. ```typescript
  303. const usersCall = interceptNetworkCall({ url: '**/api/users' }); // First
  304. await page.goto('/dashboard'); // Then navigate
  305. const { responseJson } = await usersCall; // Then await
  306. ```
  307. **DON'T ignore the returned Promise:**
  308. ```typescript
  309. interceptNetworkCall({ url: '**/api/users' }); // Not awaited!
  310. await page.goto('/dashboard');
  311. // No deterministic wait - race condition
  312. ```
  313. **DO always await the intercept:**
  314. ```typescript
  315. const usersCall = interceptNetworkCall({ url: '**/api/users' });
  316. await page.goto('/dashboard');
  317. await usersCall; // Deterministic wait
  318. ```