@assertrx/dino 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. package/themes/dino.json +66 -0
@@ -0,0 +1,1395 @@
1
+ # Network Mocking
2
+
3
+ > **When to use**: Isolating your frontend from external services, simulating error states, testing loading/empty/error UI, speeding up tests by avoiding real network calls, and testing against APIs that don't exist yet.
4
+ > **Prerequisites**: [core/locators.md](locators.md), [core/assertions-and-waiting.md](assertions-and-waiting.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Intercept and return fake data
10
+ await page.route('**/api/users', (route) =>
11
+ route.fulfill({ json: [{ id: 1, name: 'Jane' }] })
12
+ );
13
+
14
+ // Modify a real response before it reaches the browser
15
+ await page.route('**/api/users', async (route) => {
16
+ const response = await route.fetch();
17
+ const json = await response.json();
18
+ json.push({ id: 999, name: 'Injected' });
19
+ await route.fulfill({ response, json });
20
+ });
21
+
22
+ // Block third-party scripts
23
+ await page.route('**/*.{png,jpg,svg}', (route) => route.abort());
24
+
25
+ // Wait for a specific request/response
26
+ const responsePromise = page.waitForResponse('**/api/users');
27
+ await page.getByRole('button', { name: 'Load' }).click();
28
+ await responsePromise;
29
+
30
+ // HAR replay — serve recorded responses
31
+ await page.routeFromHAR('tests/data/api.har', { url: '**/api/**' });
32
+ ```
33
+
34
+ ## Patterns
35
+
36
+ ### Route Interception Basics
37
+
38
+ **Use when**: You need to intercept any HTTP request made by the page to fulfill, modify, or block it.
39
+ **Avoid when**: You want to test the real integration between frontend and backend (use real API calls instead).
40
+
41
+ `page.route()` registers a handler that runs for every request matching a URL pattern. Each handler must call exactly one of `route.fulfill()`, `route.continue()`, or `route.abort()`.
42
+
43
+ **TypeScript**
44
+ ```typescript
45
+ import { test, expect } from '@playwright/test';
46
+
47
+ test('route interception basics', async ({ page }) => {
48
+ // Intercept before navigating — routes must be set up first
49
+ await page.route('**/api/users', (route) => {
50
+ route.fulfill({
51
+ status: 200,
52
+ contentType: 'application/json',
53
+ body: JSON.stringify([{ id: 1, name: 'Alice' }]),
54
+ });
55
+ });
56
+
57
+ await page.goto('/dashboard');
58
+ await expect(page.getByText('Alice')).toBeVisible();
59
+
60
+ // Remove the route when done (important for cleanup)
61
+ await page.unroute('**/api/users');
62
+ });
63
+
64
+ test('context-level routes apply to all pages', async ({ context, page }) => {
65
+ // Routes on the context apply to every page in that context
66
+ await context.route('**/api/config', (route) =>
67
+ route.fulfill({ json: { theme: 'dark', locale: 'en' } })
68
+ );
69
+
70
+ await page.goto('/settings');
71
+ await expect(page.getByText('Dark')).toBeVisible();
72
+ });
73
+ ```
74
+
75
+ **JavaScript**
76
+ ```javascript
77
+ const { test, expect } = require('@playwright/test');
78
+
79
+ test('route interception basics', async ({ page }) => {
80
+ await page.route('**/api/users', (route) => {
81
+ route.fulfill({
82
+ status: 200,
83
+ contentType: 'application/json',
84
+ body: JSON.stringify([{ id: 1, name: 'Alice' }]),
85
+ });
86
+ });
87
+
88
+ await page.goto('/dashboard');
89
+ await expect(page.getByText('Alice')).toBeVisible();
90
+
91
+ await page.unroute('**/api/users');
92
+ });
93
+
94
+ test('context-level routes apply to all pages', async ({ context, page }) => {
95
+ await context.route('**/api/config', (route) =>
96
+ route.fulfill({ json: { theme: 'dark', locale: 'en' } })
97
+ );
98
+
99
+ await page.goto('/settings');
100
+ await expect(page.getByText('Dark')).toBeVisible();
101
+ });
102
+ ```
103
+
104
+ ### Mocking REST Responses
105
+
106
+ **Use when**: Your frontend depends on a REST API and you want deterministic, instant responses with controlled data.
107
+ **Avoid when**: You need to verify that your frontend sends the correct request body or headers to the real API (use `route.continue()` with `waitForRequest` instead).
108
+
109
+ **TypeScript**
110
+ ```typescript
111
+ import { test, expect } from '@playwright/test';
112
+
113
+ const mockUsers = [
114
+ { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
115
+ { id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' },
116
+ ];
117
+
118
+ test('mock a GET endpoint with JSON', async ({ page }) => {
119
+ await page.route('**/api/users', (route) =>
120
+ route.fulfill({ json: mockUsers })
121
+ );
122
+
123
+ await page.goto('/users');
124
+ await expect(page.getByRole('row')).toHaveCount(3); // header + 2 data rows
125
+ });
126
+
127
+ test('mock a POST endpoint and verify the request', async ({ page }) => {
128
+ await page.route('**/api/users', (route) => {
129
+ if (route.request().method() === 'POST') {
130
+ return route.fulfill({
131
+ status: 201,
132
+ json: { id: 3, name: 'Charlie', email: 'charlie@example.com' },
133
+ });
134
+ }
135
+ // Let other methods through
136
+ return route.continue();
137
+ });
138
+
139
+ await page.goto('/users/new');
140
+ await page.getByLabel('Name').fill('Charlie');
141
+ await page.getByLabel('Email').fill('charlie@example.com');
142
+ await page.getByRole('button', { name: 'Create' }).click();
143
+
144
+ await expect(page.getByText('Charlie')).toBeVisible();
145
+ });
146
+
147
+ test('mock with custom headers and status', async ({ page }) => {
148
+ await page.route('**/api/users', (route) =>
149
+ route.fulfill({
150
+ status: 200,
151
+ headers: {
152
+ 'content-type': 'application/json',
153
+ 'x-total-count': '42',
154
+ 'x-request-id': 'test-abc-123',
155
+ },
156
+ body: JSON.stringify(mockUsers),
157
+ })
158
+ );
159
+
160
+ await page.goto('/users');
161
+ await expect(page.getByText('42 total')).toBeVisible();
162
+ });
163
+
164
+ test('mock empty state', async ({ page }) => {
165
+ await page.route('**/api/users', (route) =>
166
+ route.fulfill({ json: [] })
167
+ );
168
+
169
+ await page.goto('/users');
170
+ await expect(page.getByText('No users found')).toBeVisible();
171
+ });
172
+ ```
173
+
174
+ **JavaScript**
175
+ ```javascript
176
+ const { test, expect } = require('@playwright/test');
177
+
178
+ const mockUsers = [
179
+ { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
180
+ { id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' },
181
+ ];
182
+
183
+ test('mock a GET endpoint with JSON', async ({ page }) => {
184
+ await page.route('**/api/users', (route) =>
185
+ route.fulfill({ json: mockUsers })
186
+ );
187
+
188
+ await page.goto('/users');
189
+ await expect(page.getByRole('row')).toHaveCount(3);
190
+ });
191
+
192
+ test('mock a POST endpoint and verify the request', async ({ page }) => {
193
+ await page.route('**/api/users', (route) => {
194
+ if (route.request().method() === 'POST') {
195
+ return route.fulfill({
196
+ status: 201,
197
+ json: { id: 3, name: 'Charlie', email: 'charlie@example.com' },
198
+ });
199
+ }
200
+ return route.continue();
201
+ });
202
+
203
+ await page.goto('/users/new');
204
+ await page.getByLabel('Name').fill('Charlie');
205
+ await page.getByLabel('Email').fill('charlie@example.com');
206
+ await page.getByRole('button', { name: 'Create' }).click();
207
+
208
+ await expect(page.getByText('Charlie')).toBeVisible();
209
+ });
210
+
211
+ test('mock empty state', async ({ page }) => {
212
+ await page.route('**/api/users', (route) =>
213
+ route.fulfill({ json: [] })
214
+ );
215
+
216
+ await page.goto('/users');
217
+ await expect(page.getByText('No users found')).toBeVisible();
218
+ });
219
+ ```
220
+
221
+ ### Mocking GraphQL
222
+
223
+ **Use when**: Your frontend uses GraphQL and you want to mock specific queries or mutations by operation name.
224
+ **Avoid when**: The GraphQL endpoint is part of your own backend and you want full integration coverage.
225
+
226
+ **TypeScript**
227
+ ```typescript
228
+ import { test, expect } from '@playwright/test';
229
+
230
+ test('mock a GraphQL query by operation name', async ({ page }) => {
231
+ await page.route('**/graphql', async (route) => {
232
+ const request = route.request();
233
+ const postData = request.postDataJSON();
234
+
235
+ if (postData.operationName === 'GetUsers') {
236
+ return route.fulfill({
237
+ json: {
238
+ data: {
239
+ users: [
240
+ { id: '1', name: 'Alice', email: 'alice@example.com' },
241
+ { id: '2', name: 'Bob', email: 'bob@example.com' },
242
+ ],
243
+ },
244
+ },
245
+ });
246
+ }
247
+
248
+ if (postData.operationName === 'GetUser') {
249
+ return route.fulfill({
250
+ json: {
251
+ data: {
252
+ user: { id: '1', name: 'Alice', email: 'alice@example.com' },
253
+ },
254
+ },
255
+ });
256
+ }
257
+
258
+ // Let unmocked operations through to the real server
259
+ return route.continue();
260
+ });
261
+
262
+ await page.goto('/users');
263
+ await expect(page.getByText('Alice')).toBeVisible();
264
+ await expect(page.getByText('Bob')).toBeVisible();
265
+ });
266
+
267
+ test('mock a GraphQL mutation', async ({ page }) => {
268
+ await page.route('**/graphql', async (route) => {
269
+ const { operationName, variables } = route.request().postDataJSON();
270
+
271
+ if (operationName === 'CreateUser') {
272
+ return route.fulfill({
273
+ json: {
274
+ data: {
275
+ createUser: {
276
+ id: '99',
277
+ name: variables.input.name,
278
+ email: variables.input.email,
279
+ },
280
+ },
281
+ },
282
+ });
283
+ }
284
+
285
+ return route.continue();
286
+ });
287
+
288
+ await page.goto('/users/new');
289
+ await page.getByLabel('Name').fill('Charlie');
290
+ await page.getByLabel('Email').fill('charlie@example.com');
291
+ await page.getByRole('button', { name: 'Create' }).click();
292
+
293
+ await expect(page.getByText('Charlie')).toBeVisible();
294
+ });
295
+
296
+ test('mock GraphQL errors', async ({ page }) => {
297
+ await page.route('**/graphql', async (route) => {
298
+ const { operationName } = route.request().postDataJSON();
299
+
300
+ if (operationName === 'GetUsers') {
301
+ return route.fulfill({
302
+ json: {
303
+ data: null,
304
+ errors: [
305
+ {
306
+ message: 'Not authorized',
307
+ extensions: { code: 'UNAUTHORIZED' },
308
+ },
309
+ ],
310
+ },
311
+ });
312
+ }
313
+
314
+ return route.continue();
315
+ });
316
+
317
+ await page.goto('/users');
318
+ await expect(page.getByText('Not authorized')).toBeVisible();
319
+ });
320
+ ```
321
+
322
+ **JavaScript**
323
+ ```javascript
324
+ const { test, expect } = require('@playwright/test');
325
+
326
+ test('mock a GraphQL query by operation name', async ({ page }) => {
327
+ await page.route('**/graphql', async (route) => {
328
+ const postData = route.request().postDataJSON();
329
+
330
+ if (postData.operationName === 'GetUsers') {
331
+ return route.fulfill({
332
+ json: {
333
+ data: {
334
+ users: [
335
+ { id: '1', name: 'Alice', email: 'alice@example.com' },
336
+ { id: '2', name: 'Bob', email: 'bob@example.com' },
337
+ ],
338
+ },
339
+ },
340
+ });
341
+ }
342
+
343
+ return route.continue();
344
+ });
345
+
346
+ await page.goto('/users');
347
+ await expect(page.getByText('Alice')).toBeVisible();
348
+ await expect(page.getByText('Bob')).toBeVisible();
349
+ });
350
+
351
+ test('mock a GraphQL mutation', async ({ page }) => {
352
+ await page.route('**/graphql', async (route) => {
353
+ const { operationName, variables } = route.request().postDataJSON();
354
+
355
+ if (operationName === 'CreateUser') {
356
+ return route.fulfill({
357
+ json: {
358
+ data: {
359
+ createUser: {
360
+ id: '99',
361
+ name: variables.input.name,
362
+ email: variables.input.email,
363
+ },
364
+ },
365
+ },
366
+ });
367
+ }
368
+
369
+ return route.continue();
370
+ });
371
+
372
+ await page.goto('/users/new');
373
+ await page.getByLabel('Name').fill('Charlie');
374
+ await page.getByLabel('Email').fill('charlie@example.com');
375
+ await page.getByRole('button', { name: 'Create' }).click();
376
+
377
+ await expect(page.getByText('Charlie')).toBeVisible();
378
+ });
379
+ ```
380
+
381
+ ### Modifying Responses
382
+
383
+ **Use when**: You need the real API response but want to tweak specific fields -- inject test data, override feature flags, simulate edge cases in real data.
384
+ **Avoid when**: You can fully mock the response. `route.fetch()` adds a real network round-trip, so it is slower.
385
+
386
+ **TypeScript**
387
+ ```typescript
388
+ import { test, expect } from '@playwright/test';
389
+
390
+ test('modify a real API response', async ({ page }) => {
391
+ await page.route('**/api/users', async (route) => {
392
+ // Fetch the real response from the server
393
+ const response = await route.fetch();
394
+ const users = await response.json();
395
+
396
+ // Inject a test user into the real data
397
+ users.push({ id: 999, name: 'Test User', email: 'test@example.com' });
398
+
399
+ await route.fulfill({ response, json: users });
400
+ });
401
+
402
+ await page.goto('/users');
403
+ await expect(page.getByText('Test User')).toBeVisible();
404
+ });
405
+
406
+ test('override feature flags from real config', async ({ page }) => {
407
+ await page.route('**/api/config', async (route) => {
408
+ const response = await route.fetch();
409
+ const config = await response.json();
410
+
411
+ // Enable a feature flag for testing
412
+ config.featureFlags = {
413
+ ...config.featureFlags,
414
+ newCheckout: true,
415
+ darkMode: true,
416
+ };
417
+
418
+ await route.fulfill({ response, json: config });
419
+ });
420
+
421
+ await page.goto('/settings');
422
+ await expect(page.getByRole('switch', { name: 'Dark mode' })).toBeVisible();
423
+ });
424
+
425
+ test('modify response headers', async ({ page }) => {
426
+ await page.route('**/api/data', async (route) => {
427
+ const response = await route.fetch();
428
+
429
+ await route.fulfill({
430
+ response,
431
+ headers: {
432
+ ...response.headers(),
433
+ 'cache-control': 'no-cache',
434
+ 'x-test-header': 'injected',
435
+ },
436
+ });
437
+ });
438
+
439
+ await page.goto('/data');
440
+ });
441
+ ```
442
+
443
+ **JavaScript**
444
+ ```javascript
445
+ const { test, expect } = require('@playwright/test');
446
+
447
+ test('modify a real API response', async ({ page }) => {
448
+ await page.route('**/api/users', async (route) => {
449
+ const response = await route.fetch();
450
+ const users = await response.json();
451
+
452
+ users.push({ id: 999, name: 'Test User', email: 'test@example.com' });
453
+
454
+ await route.fulfill({ response, json: users });
455
+ });
456
+
457
+ await page.goto('/users');
458
+ await expect(page.getByText('Test User')).toBeVisible();
459
+ });
460
+
461
+ test('override feature flags from real config', async ({ page }) => {
462
+ await page.route('**/api/config', async (route) => {
463
+ const response = await route.fetch();
464
+ const config = await response.json();
465
+
466
+ config.featureFlags = {
467
+ ...config.featureFlags,
468
+ newCheckout: true,
469
+ darkMode: true,
470
+ };
471
+
472
+ await route.fulfill({ response, json: config });
473
+ });
474
+
475
+ await page.goto('/settings');
476
+ await expect(page.getByRole('switch', { name: 'Dark mode' })).toBeVisible();
477
+ });
478
+ ```
479
+
480
+ ### Request Blocking
481
+
482
+ **Use when**: Blocking analytics, ads, third-party scripts, images, or fonts to speed up tests and eliminate flakiness from external dependencies.
483
+ **Avoid when**: The blocked resource is required for the feature under test.
484
+
485
+ **TypeScript**
486
+ ```typescript
487
+ import { test, expect } from '@playwright/test';
488
+
489
+ test('block analytics and tracking scripts', async ({ page }) => {
490
+ await page.route(/(google-analytics|segment|hotjar|mixpanel)/, (route) =>
491
+ route.abort()
492
+ );
493
+
494
+ await page.goto('/dashboard');
495
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
496
+ });
497
+
498
+ test('block images to speed up tests', async ({ page }) => {
499
+ await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', (route) =>
500
+ route.abort()
501
+ );
502
+
503
+ await page.goto('/gallery');
504
+ // Test interactions without waiting for image downloads
505
+ await page.getByRole('button', { name: 'Next' }).click();
506
+ });
507
+
508
+ test('block specific third-party domains', async ({ page }) => {
509
+ const blockedDomains = [
510
+ 'ads.example.com',
511
+ 'tracker.example.com',
512
+ 'cdn.slow-service.com',
513
+ ];
514
+
515
+ await page.route('**/*', (route) => {
516
+ const url = new URL(route.request().url());
517
+ if (blockedDomains.includes(url.hostname)) {
518
+ return route.abort();
519
+ }
520
+ return route.continue();
521
+ });
522
+
523
+ await page.goto('/home');
524
+ });
525
+
526
+ test('block by resource type', async ({ context }) => {
527
+ // Context-level blocking affects all pages
528
+ await context.route('**/*', (route) => {
529
+ const resourceType = route.request().resourceType();
530
+ if (['image', 'font', 'media'].includes(resourceType)) {
531
+ return route.abort();
532
+ }
533
+ return route.continue();
534
+ });
535
+ });
536
+ ```
537
+
538
+ **JavaScript**
539
+ ```javascript
540
+ const { test, expect } = require('@playwright/test');
541
+
542
+ test('block analytics and tracking scripts', async ({ page }) => {
543
+ await page.route(/(google-analytics|segment|hotjar|mixpanel)/, (route) =>
544
+ route.abort()
545
+ );
546
+
547
+ await page.goto('/dashboard');
548
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
549
+ });
550
+
551
+ test('block images to speed up tests', async ({ page }) => {
552
+ await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', (route) =>
553
+ route.abort()
554
+ );
555
+
556
+ await page.goto('/gallery');
557
+ await page.getByRole('button', { name: 'Next' }).click();
558
+ });
559
+
560
+ test('block by resource type', async ({ context }) => {
561
+ await context.route('**/*', (route) => {
562
+ const resourceType = route.request().resourceType();
563
+ if (['image', 'font', 'media'].includes(resourceType)) {
564
+ return route.abort();
565
+ }
566
+ return route.continue();
567
+ });
568
+ });
569
+ ```
570
+
571
+ ### HAR Recording and Replay
572
+
573
+ **Use when**: You want to capture real network traffic once and replay it in tests for speed and determinism. Great for complex APIs with many endpoints or when API access is limited.
574
+ **Avoid when**: API responses change frequently and stale recordings would cause false passes. Keep HAR files in version control and update them regularly.
575
+
576
+ **TypeScript**
577
+ ```typescript
578
+ import { test, expect } from '@playwright/test';
579
+
580
+ // Record a HAR file — run once to capture traffic, then replay
581
+ test('record HAR for later replay', async ({ page }) => {
582
+ // This records all matching network traffic to the HAR file.
583
+ // If the file already exists and responses match, it serves from HAR.
584
+ // If a request has no match in the HAR, it falls through to the network
585
+ // and the new response is appended to the HAR file.
586
+ await page.routeFromHAR('tests/data/users-api.har', {
587
+ url: '**/api/**',
588
+ update: true, // set to true to record, false (or omit) to replay
589
+ });
590
+
591
+ await page.goto('/users');
592
+ await expect(page.getByRole('row')).toHaveCount(6);
593
+ });
594
+
595
+ // Replay from a previously recorded HAR file
596
+ test('replay from HAR', async ({ page }) => {
597
+ await page.routeFromHAR('tests/data/users-api.har', {
598
+ url: '**/api/**',
599
+ // update: false is the default — serves from the HAR file
600
+ });
601
+
602
+ await page.goto('/users');
603
+ await expect(page.getByRole('row')).toHaveCount(6);
604
+ });
605
+
606
+ // HAR with notFound option — control what happens for unmatched requests
607
+ test('HAR replay with fallback behavior', async ({ page }) => {
608
+ await page.routeFromHAR('tests/data/users-api.har', {
609
+ url: '**/api/**',
610
+ notFound: 'abort', // 'abort' fails unmatched requests; 'fallback' lets them through
611
+ });
612
+
613
+ await page.goto('/users');
614
+ await expect(page.getByText('Alice')).toBeVisible();
615
+ });
616
+
617
+ // Context-level HAR replay
618
+ test('HAR replay at context level', async ({ context, page }) => {
619
+ await context.routeFromHAR('tests/data/full-app.har', {
620
+ url: '**/api/**',
621
+ });
622
+
623
+ await page.goto('/dashboard');
624
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
625
+ });
626
+ ```
627
+
628
+ **JavaScript**
629
+ ```javascript
630
+ const { test, expect } = require('@playwright/test');
631
+
632
+ test('record HAR for later replay', async ({ page }) => {
633
+ await page.routeFromHAR('tests/data/users-api.har', {
634
+ url: '**/api/**',
635
+ update: true,
636
+ });
637
+
638
+ await page.goto('/users');
639
+ await expect(page.getByRole('row')).toHaveCount(6);
640
+ });
641
+
642
+ test('replay from HAR', async ({ page }) => {
643
+ await page.routeFromHAR('tests/data/users-api.har', {
644
+ url: '**/api/**',
645
+ });
646
+
647
+ await page.goto('/users');
648
+ await expect(page.getByRole('row')).toHaveCount(6);
649
+ });
650
+
651
+ test('HAR replay with fallback behavior', async ({ page }) => {
652
+ await page.routeFromHAR('tests/data/users-api.har', {
653
+ url: '**/api/**',
654
+ notFound: 'abort',
655
+ });
656
+
657
+ await page.goto('/users');
658
+ await expect(page.getByText('Alice')).toBeVisible();
659
+ });
660
+ ```
661
+
662
+ ### On-Demand HAR Recording in Tracing (Playwright 1.60+)
663
+
664
+ **Use when**: You want to capture network traffic for a *specific slice* of a test — one user flow, one step — instead of the whole session, and you want to start/stop recording from inside the test body.
665
+ **Avoid when**: You only need replay for mocking (use `routeFromHAR`), or you want HAR for the entire context (set `recordHar` at context creation). This API is for scoped, programmatic capture.
666
+
667
+ Playwright 1.60 promotes HAR recording to a first-class tracing API: `context.tracing.startHar()` / `context.tracing.stopHar()`. It accepts the same `content`, `mode`, and `urlFilter` options as the context-level `recordHar`, and returns a disposable so `await using` cleans up automatically even if the test throws.
668
+
669
+ **TypeScript**
670
+ ```typescript
671
+ import { test, expect } from '@playwright/test';
672
+
673
+ test('capture HAR for the checkout flow only', async ({ context, page }) => {
674
+ await page.goto('/cart');
675
+
676
+ // Record only the network traffic during checkout
677
+ await context.tracing.startHar({
678
+ path: 'test-results/checkout.har',
679
+ urlFilter: '**/api/**',
680
+ content: 'embed', // 'omit' | 'embed' | 'attach'
681
+ });
682
+
683
+ await page.getByRole('button', { name: 'Checkout' }).click();
684
+ await page.getByRole('button', { name: 'Place order' }).click();
685
+ await expect(page.getByText('Order confirmed')).toBeVisible();
686
+
687
+ await context.tracing.stopHar();
688
+ });
689
+
690
+ test('scoped capture with automatic cleanup', async ({ context, page }) => {
691
+ await page.goto('/search');
692
+
693
+ // `await using` stops and flushes the HAR when the block exits — even on failure
694
+ await using _har = await context.tracing.startHar({
695
+ path: 'test-results/search.har',
696
+ urlFilter: '**/api/search**',
697
+ });
698
+
699
+ await page.getByLabel('Search').fill('playwright');
700
+ await page.getByRole('button', { name: 'Search' }).click();
701
+ await expect(page.getByRole('listitem')).not.toHaveCount(0);
702
+ });
703
+ ```
704
+
705
+ **JavaScript**
706
+ ```javascript
707
+ const { test, expect } = require('@playwright/test');
708
+
709
+ test('capture HAR for the checkout flow only', async ({ context, page }) => {
710
+ await page.goto('/cart');
711
+
712
+ await context.tracing.startHar({
713
+ path: 'test-results/checkout.har',
714
+ urlFilter: '**/api/**',
715
+ content: 'embed',
716
+ });
717
+
718
+ await page.getByRole('button', { name: 'Checkout' }).click();
719
+ await page.getByRole('button', { name: 'Place order' }).click();
720
+ await expect(page.getByText('Order confirmed')).toBeVisible();
721
+
722
+ await context.tracing.stopHar();
723
+ });
724
+ ```
725
+
726
+ > **`await using` note**: the disposable form requires TypeScript 5.2+ / a runtime with explicit resource management. If your toolchain doesn't support it, call `stopHar()` explicitly in a `finally` block instead.
727
+
728
+ ### Conditional Mocking
729
+
730
+ **Use when**: You need different responses based on the request method, body, headers, or query parameters. Common for paginated APIs, search endpoints, and role-based access.
731
+ **Avoid when**: Simple static mocking suffices. Don't over-engineer route handlers.
732
+
733
+ **TypeScript**
734
+ ```typescript
735
+ import { test, expect } from '@playwright/test';
736
+
737
+ test('respond based on request method', async ({ page }) => {
738
+ await page.route('**/api/users', (route) => {
739
+ const method = route.request().method();
740
+
741
+ switch (method) {
742
+ case 'GET':
743
+ return route.fulfill({
744
+ json: [{ id: 1, name: 'Alice' }],
745
+ });
746
+ case 'POST':
747
+ return route.fulfill({
748
+ status: 201,
749
+ json: { id: 2, name: 'Bob' },
750
+ });
751
+ case 'DELETE':
752
+ return route.fulfill({ status: 204, body: '' });
753
+ default:
754
+ return route.continue();
755
+ }
756
+ });
757
+
758
+ await page.goto('/users');
759
+ await expect(page.getByText('Alice')).toBeVisible();
760
+ });
761
+
762
+ test('respond based on query parameters', async ({ page }) => {
763
+ await page.route('**/api/users*', (route) => {
764
+ const url = new URL(route.request().url());
765
+ const page_num = parseInt(url.searchParams.get('page') || '1');
766
+ const role = url.searchParams.get('role');
767
+
768
+ const allUsers = [
769
+ { id: 1, name: 'Alice', role: 'admin' },
770
+ { id: 2, name: 'Bob', role: 'user' },
771
+ { id: 3, name: 'Charlie', role: 'user' },
772
+ { id: 4, name: 'Diana', role: 'admin' },
773
+ ];
774
+
775
+ let filtered = allUsers;
776
+ if (role) {
777
+ filtered = allUsers.filter((u) => u.role === role);
778
+ }
779
+
780
+ const perPage = 2;
781
+ const start = (page_num - 1) * perPage;
782
+ const paginated = filtered.slice(start, start + perPage);
783
+
784
+ return route.fulfill({
785
+ json: paginated,
786
+ headers: {
787
+ 'content-type': 'application/json',
788
+ 'x-total-count': String(filtered.length),
789
+ },
790
+ });
791
+ });
792
+
793
+ await page.goto('/users');
794
+ await expect(page.getByRole('row')).toHaveCount(3); // header + 2 rows
795
+ });
796
+
797
+ test('respond based on request body', async ({ page }) => {
798
+ await page.route('**/api/search', (route) => {
799
+ const body = route.request().postDataJSON();
800
+ const query = body?.query?.toLowerCase() || '';
801
+
802
+ const results = {
803
+ playwright: [{ title: 'Playwright Docs' }, { title: 'Playwright GitHub' }],
804
+ cypress: [{ title: 'Cypress Docs' }],
805
+ };
806
+
807
+ return route.fulfill({
808
+ json: results[query] || [],
809
+ });
810
+ });
811
+
812
+ await page.goto('/search');
813
+ await page.getByLabel('Search').fill('playwright');
814
+ await page.getByRole('button', { name: 'Search' }).click();
815
+ await expect(page.getByRole('listitem')).toHaveCount(2);
816
+ });
817
+
818
+ test('respond based on request headers', async ({ page }) => {
819
+ await page.route('**/api/users', (route) => {
820
+ const authHeader = route.request().headers()['authorization'];
821
+
822
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
823
+ return route.fulfill({
824
+ status: 401,
825
+ json: { error: 'Unauthorized' },
826
+ });
827
+ }
828
+
829
+ return route.fulfill({
830
+ json: [{ id: 1, name: 'Alice' }],
831
+ });
832
+ });
833
+
834
+ await page.goto('/login');
835
+ await expect(page.getByText('Unauthorized')).toBeVisible();
836
+ });
837
+ ```
838
+
839
+ **JavaScript**
840
+ ```javascript
841
+ const { test, expect } = require('@playwright/test');
842
+
843
+ test('respond based on request method', async ({ page }) => {
844
+ await page.route('**/api/users', (route) => {
845
+ const method = route.request().method();
846
+
847
+ switch (method) {
848
+ case 'GET':
849
+ return route.fulfill({ json: [{ id: 1, name: 'Alice' }] });
850
+ case 'POST':
851
+ return route.fulfill({ status: 201, json: { id: 2, name: 'Bob' } });
852
+ case 'DELETE':
853
+ return route.fulfill({ status: 204, body: '' });
854
+ default:
855
+ return route.continue();
856
+ }
857
+ });
858
+
859
+ await page.goto('/users');
860
+ await expect(page.getByText('Alice')).toBeVisible();
861
+ });
862
+
863
+ test('respond based on query parameters', async ({ page }) => {
864
+ await page.route('**/api/users*', (route) => {
865
+ const url = new URL(route.request().url());
866
+ const role = url.searchParams.get('role');
867
+
868
+ const allUsers = [
869
+ { id: 1, name: 'Alice', role: 'admin' },
870
+ { id: 2, name: 'Bob', role: 'user' },
871
+ { id: 3, name: 'Charlie', role: 'user' },
872
+ ];
873
+
874
+ const filtered = role ? allUsers.filter((u) => u.role === role) : allUsers;
875
+
876
+ return route.fulfill({ json: filtered });
877
+ });
878
+
879
+ await page.goto('/users?role=admin');
880
+ await expect(page.getByText('Alice')).toBeVisible();
881
+ });
882
+
883
+ test('respond based on request body', async ({ page }) => {
884
+ await page.route('**/api/search', (route) => {
885
+ const body = route.request().postDataJSON();
886
+ const query = body?.query?.toLowerCase() || '';
887
+
888
+ const results = {
889
+ playwright: [{ title: 'Playwright Docs' }, { title: 'Playwright GitHub' }],
890
+ cypress: [{ title: 'Cypress Docs' }],
891
+ };
892
+
893
+ return route.fulfill({ json: results[query] || [] });
894
+ });
895
+
896
+ await page.goto('/search');
897
+ await page.getByLabel('Search').fill('playwright');
898
+ await page.getByRole('button', { name: 'Search' }).click();
899
+ await expect(page.getByRole('listitem')).toHaveCount(2);
900
+ });
901
+ ```
902
+
903
+ ### Network Error Simulation
904
+
905
+ **Use when**: Testing how your UI handles server errors, timeouts, and connection failures. Essential for verifying error boundaries, retry logic, and degraded-mode UX.
906
+ **Avoid when**: The test is about the happy path. Only introduce errors when testing error handling.
907
+
908
+ **TypeScript**
909
+ ```typescript
910
+ import { test, expect } from '@playwright/test';
911
+
912
+ test('simulate a 500 server error', async ({ page }) => {
913
+ await page.route('**/api/users', (route) =>
914
+ route.fulfill({
915
+ status: 500,
916
+ json: { error: 'Internal Server Error' },
917
+ })
918
+ );
919
+
920
+ await page.goto('/users');
921
+ await expect(page.getByText('Something went wrong')).toBeVisible();
922
+ await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
923
+ });
924
+
925
+ test('simulate a 403 forbidden', async ({ page }) => {
926
+ await page.route('**/api/admin/**', (route) =>
927
+ route.fulfill({
928
+ status: 403,
929
+ json: { error: 'Forbidden', message: 'Admin access required' },
930
+ })
931
+ );
932
+
933
+ await page.goto('/admin/settings');
934
+ await expect(page.getByText('Admin access required')).toBeVisible();
935
+ });
936
+
937
+ test('simulate a 404 not found', async ({ page }) => {
938
+ await page.route('**/api/users/999', (route) =>
939
+ route.fulfill({
940
+ status: 404,
941
+ json: { error: 'User not found' },
942
+ })
943
+ );
944
+
945
+ await page.goto('/users/999');
946
+ await expect(page.getByText('User not found')).toBeVisible();
947
+ });
948
+
949
+ test('simulate a network error (connection refused)', async ({ page }) => {
950
+ await page.route('**/api/users', (route) =>
951
+ route.abort('connectionrefused')
952
+ );
953
+
954
+ await page.goto('/users');
955
+ await expect(page.getByText('Network error')).toBeVisible();
956
+ });
957
+
958
+ test('simulate a timeout', async ({ page }) => {
959
+ await page.route('**/api/users', async (route) => {
960
+ // Delay longer than the app's fetch timeout to trigger a timeout error
961
+ await new Promise((resolve) => setTimeout(resolve, 30_000));
962
+ await route.fulfill({ json: [] });
963
+ });
964
+
965
+ await page.goto('/users');
966
+ // The app should show a timeout message before the route resolves
967
+ await expect(page.getByText('Request timed out')).toBeVisible({
968
+ timeout: 15_000,
969
+ });
970
+ });
971
+
972
+ test('simulate intermittent failures then recovery', async ({ page }) => {
973
+ let requestCount = 0;
974
+
975
+ await page.route('**/api/users', (route) => {
976
+ requestCount++;
977
+ if (requestCount <= 2) {
978
+ return route.fulfill({
979
+ status: 503,
980
+ json: { error: 'Service Unavailable' },
981
+ });
982
+ }
983
+ return route.fulfill({
984
+ json: [{ id: 1, name: 'Alice' }],
985
+ });
986
+ });
987
+
988
+ await page.goto('/users');
989
+ await expect(page.getByText('Something went wrong')).toBeVisible();
990
+
991
+ // Simulate user clicking retry (which makes the 3rd request)
992
+ await page.getByRole('button', { name: 'Retry' }).click();
993
+ await expect(page.getByText('Something went wrong')).toBeVisible();
994
+
995
+ // Third attempt succeeds
996
+ await page.getByRole('button', { name: 'Retry' }).click();
997
+ await expect(page.getByText('Alice')).toBeVisible();
998
+ });
999
+ ```
1000
+
1001
+ **JavaScript**
1002
+ ```javascript
1003
+ const { test, expect } = require('@playwright/test');
1004
+
1005
+ test('simulate a 500 server error', async ({ page }) => {
1006
+ await page.route('**/api/users', (route) =>
1007
+ route.fulfill({
1008
+ status: 500,
1009
+ json: { error: 'Internal Server Error' },
1010
+ })
1011
+ );
1012
+
1013
+ await page.goto('/users');
1014
+ await expect(page.getByText('Something went wrong')).toBeVisible();
1015
+ await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
1016
+ });
1017
+
1018
+ test('simulate a network error (connection refused)', async ({ page }) => {
1019
+ await page.route('**/api/users', (route) =>
1020
+ route.abort('connectionrefused')
1021
+ );
1022
+
1023
+ await page.goto('/users');
1024
+ await expect(page.getByText('Network error')).toBeVisible();
1025
+ });
1026
+
1027
+ test('simulate intermittent failures then recovery', async ({ page }) => {
1028
+ let requestCount = 0;
1029
+
1030
+ await page.route('**/api/users', (route) => {
1031
+ requestCount++;
1032
+ if (requestCount <= 2) {
1033
+ return route.fulfill({
1034
+ status: 503,
1035
+ json: { error: 'Service Unavailable' },
1036
+ });
1037
+ }
1038
+ return route.fulfill({
1039
+ json: [{ id: 1, name: 'Alice' }],
1040
+ });
1041
+ });
1042
+
1043
+ await page.goto('/users');
1044
+ await expect(page.getByText('Something went wrong')).toBeVisible();
1045
+
1046
+ await page.getByRole('button', { name: 'Retry' }).click();
1047
+ await expect(page.getByText('Something went wrong')).toBeVisible();
1048
+
1049
+ await page.getByRole('button', { name: 'Retry' }).click();
1050
+ await expect(page.getByText('Alice')).toBeVisible();
1051
+ });
1052
+ ```
1053
+
1054
+ **Abort reasons**: `'aborted'`, `'accessdenied'`, `'addressunreachable'`, `'blockedbyclient'`, `'blockedbyresponse'`, `'connectionaborted'`, `'connectionclosed'`, `'connectionfailed'`, `'connectionrefused'`, `'connectionreset'`, `'internetdisconnected'`, `'namenotresolved'`, `'timedout'`, `'failed'`.
1055
+
1056
+ ### Request Waiting
1057
+
1058
+ **Use when**: Synchronizing your test with network activity -- waiting for a request to be sent or a response to arrive before asserting on the UI.
1059
+ **Avoid when**: A web-first assertion on a locator is sufficient. Only use request waiting when you need to inspect the request/response itself.
1060
+
1061
+ **TypeScript**
1062
+ ```typescript
1063
+ import { test, expect } from '@playwright/test';
1064
+
1065
+ test('wait for response and verify UI updates', async ({ page }) => {
1066
+ await page.goto('/users');
1067
+
1068
+ // CRITICAL: set up the wait BEFORE triggering the action
1069
+ const responsePromise = page.waitForResponse('**/api/users');
1070
+ await page.getByRole('button', { name: 'Refresh' }).click();
1071
+ const response = await responsePromise;
1072
+
1073
+ expect(response.status()).toBe(200);
1074
+ const users = await response.json();
1075
+ expect(users).toHaveLength(5);
1076
+ });
1077
+
1078
+ test('wait for request and verify payload', async ({ page }) => {
1079
+ await page.goto('/users/new');
1080
+
1081
+ const requestPromise = page.waitForRequest('**/api/users');
1082
+ await page.getByLabel('Name').fill('Alice');
1083
+ await page.getByLabel('Email').fill('alice@example.com');
1084
+ await page.getByRole('button', { name: 'Create' }).click();
1085
+ const request = await requestPromise;
1086
+
1087
+ expect(request.method()).toBe('POST');
1088
+ expect(request.postDataJSON()).toMatchObject({
1089
+ name: 'Alice',
1090
+ email: 'alice@example.com',
1091
+ });
1092
+ });
1093
+
1094
+ test('wait for response with predicate function', async ({ page }) => {
1095
+ await page.goto('/dashboard');
1096
+
1097
+ // Wait for a specific response matching custom criteria
1098
+ const responsePromise = page.waitForResponse(
1099
+ (response) =>
1100
+ response.url().includes('/api/users') &&
1101
+ response.status() === 200 &&
1102
+ response.request().method() === 'GET'
1103
+ );
1104
+ await page.getByRole('button', { name: 'Load users' }).click();
1105
+ const response = await responsePromise;
1106
+
1107
+ const data = await response.json();
1108
+ expect(data.length).toBeGreaterThan(0);
1109
+ });
1110
+
1111
+ test('wait for multiple requests in sequence', async ({ page }) => {
1112
+ await page.goto('/checkout');
1113
+
1114
+ // Wait for multiple API calls that happen during a flow
1115
+ const [validateResponse, submitResponse] = await Promise.all([
1116
+ page.waitForResponse('**/api/cart/validate'),
1117
+ page.waitForResponse('**/api/orders'),
1118
+ page.getByRole('button', { name: 'Place order' }).click(),
1119
+ ]);
1120
+
1121
+ expect(validateResponse.status()).toBe(200);
1122
+ expect(submitResponse.status()).toBe(201);
1123
+ });
1124
+ ```
1125
+
1126
+ **JavaScript**
1127
+ ```javascript
1128
+ const { test, expect } = require('@playwright/test');
1129
+
1130
+ test('wait for response and verify UI updates', async ({ page }) => {
1131
+ await page.goto('/users');
1132
+
1133
+ const responsePromise = page.waitForResponse('**/api/users');
1134
+ await page.getByRole('button', { name: 'Refresh' }).click();
1135
+ const response = await responsePromise;
1136
+
1137
+ expect(response.status()).toBe(200);
1138
+ const users = await response.json();
1139
+ expect(users).toHaveLength(5);
1140
+ });
1141
+
1142
+ test('wait for request and verify payload', async ({ page }) => {
1143
+ await page.goto('/users/new');
1144
+
1145
+ const requestPromise = page.waitForRequest('**/api/users');
1146
+ await page.getByLabel('Name').fill('Alice');
1147
+ await page.getByLabel('Email').fill('alice@example.com');
1148
+ await page.getByRole('button', { name: 'Create' }).click();
1149
+ const request = await requestPromise;
1150
+
1151
+ expect(request.method()).toBe('POST');
1152
+ expect(request.postDataJSON()).toMatchObject({
1153
+ name: 'Alice',
1154
+ email: 'alice@example.com',
1155
+ });
1156
+ });
1157
+
1158
+ test('wait for multiple requests in sequence', async ({ page }) => {
1159
+ await page.goto('/checkout');
1160
+
1161
+ const [validateResponse, submitResponse] = await Promise.all([
1162
+ page.waitForResponse('**/api/cart/validate'),
1163
+ page.waitForResponse('**/api/orders'),
1164
+ page.getByRole('button', { name: 'Place order' }).click(),
1165
+ ]);
1166
+
1167
+ expect(validateResponse.status()).toBe(200);
1168
+ expect(submitResponse.status()).toBe(201);
1169
+ });
1170
+ ```
1171
+
1172
+ ### Glob Patterns and URL Matching
1173
+
1174
+ **Use when**: You need to match URLs with wildcards, partial paths, or regex. Every `page.route()`, `waitForRequest()`, and `waitForResponse()` accepts glob patterns, strings, or regex.
1175
+ **Avoid when**: The URL is known and static. Use the exact string.
1176
+
1177
+ **TypeScript**
1178
+ ```typescript
1179
+ import { test, expect } from '@playwright/test';
1180
+
1181
+ test('glob pattern examples', async ({ page }) => {
1182
+ // ** matches any path segments (including nested)
1183
+ await page.route('**/api/users', (route) =>
1184
+ route.fulfill({ json: [] })
1185
+ );
1186
+ // Matches: https://example.com/api/users
1187
+ // Matches: https://example.com/v2/api/users
1188
+
1189
+ // * matches any single path segment
1190
+ await page.route('**/api/users/*/orders', (route) =>
1191
+ route.fulfill({ json: [] })
1192
+ );
1193
+ // Matches: /api/users/123/orders
1194
+ // Matches: /api/users/abc/orders
1195
+ // Does NOT match: /api/users/123/456/orders
1196
+
1197
+ // Match file extensions
1198
+ await page.route('**/*.{png,jpg,svg}', (route) => route.abort());
1199
+ // Matches: /images/logo.png, /assets/photo.jpg
1200
+
1201
+ // Match query strings with *
1202
+ await page.route('**/api/search?q=*', (route) =>
1203
+ route.fulfill({ json: [] })
1204
+ );
1205
+ // Matches: /api/search?q=anything
1206
+
1207
+ // Use regex for complex patterns
1208
+ await page.route(/\/api\/users\/\d+$/, (route) =>
1209
+ route.fulfill({ json: { id: 1, name: 'Alice' } })
1210
+ );
1211
+ // Matches: /api/users/123, /api/users/456
1212
+ // Does NOT match: /api/users/abc, /api/users/123/orders
1213
+
1214
+ // Regex with captured groups for dynamic responses
1215
+ await page.route(/\/api\/users\/(\d+)/, (route) => {
1216
+ const match = route.request().url().match(/\/api\/users\/(\d+)/);
1217
+ const userId = match ? match[1] : '0';
1218
+ return route.fulfill({
1219
+ json: { id: parseInt(userId), name: `User ${userId}` },
1220
+ });
1221
+ });
1222
+
1223
+ await page.goto('/users');
1224
+ });
1225
+
1226
+ test('match all requests on a domain', async ({ page }) => {
1227
+ // Block everything from a specific domain
1228
+ await page.route('https://analytics.example.com/**', (route) =>
1229
+ route.abort()
1230
+ );
1231
+
1232
+ await page.goto('/dashboard');
1233
+ });
1234
+ ```
1235
+
1236
+ **JavaScript**
1237
+ ```javascript
1238
+ const { test, expect } = require('@playwright/test');
1239
+
1240
+ test('glob pattern examples', async ({ page }) => {
1241
+ // ** matches any path segments
1242
+ await page.route('**/api/users', (route) =>
1243
+ route.fulfill({ json: [] })
1244
+ );
1245
+
1246
+ // * matches a single path segment
1247
+ await page.route('**/api/users/*/orders', (route) =>
1248
+ route.fulfill({ json: [] })
1249
+ );
1250
+
1251
+ // Match file extensions
1252
+ await page.route('**/*.{png,jpg,svg}', (route) => route.abort());
1253
+
1254
+ // Regex for complex patterns
1255
+ await page.route(/\/api\/users\/\d+$/, (route) =>
1256
+ route.fulfill({ json: { id: 1, name: 'Alice' } })
1257
+ );
1258
+
1259
+ // Regex with dynamic responses
1260
+ await page.route(/\/api\/users\/(\d+)/, (route) => {
1261
+ const match = route.request().url().match(/\/api\/users\/(\d+)/);
1262
+ const userId = match ? match[1] : '0';
1263
+ return route.fulfill({
1264
+ json: { id: parseInt(userId), name: `User ${userId}` },
1265
+ });
1266
+ });
1267
+
1268
+ await page.goto('/users');
1269
+ });
1270
+
1271
+ test('match all requests on a domain', async ({ page }) => {
1272
+ await page.route('https://analytics.example.com/**', (route) =>
1273
+ route.abort()
1274
+ );
1275
+
1276
+ await page.goto('/dashboard');
1277
+ });
1278
+ ```
1279
+
1280
+ **Pattern reference**:
1281
+
1282
+ | Pattern | Matches | Does Not Match |
1283
+ |---|---|---|
1284
+ | `**/api/users` | `/api/users`, `/v2/api/users` | `/api/users/1` |
1285
+ | `**/api/users*` | `/api/users`, `/api/users?page=1` | `/api/users/1` |
1286
+ | `**/api/users/**` | `/api/users/1`, `/api/users/1/orders` | `/api/users` |
1287
+ | `**/api/users/*/orders` | `/api/users/1/orders` | `/api/users/1/2/orders` |
1288
+ | `**/*.{png,jpg}` | `/logo.png`, `/deep/path/img.jpg` | `/file.svg` |
1289
+ | `/\/api\/users\/\d+$/` (regex) | `/api/users/123` | `/api/users/abc` |
1290
+
1291
+ ## Decision Guide
1292
+
1293
+ | Scenario | Use | Why |
1294
+ |---|---|---|
1295
+ | Frontend depends on an external API | `route.fulfill()` | Deterministic data, no external dependency, fast |
1296
+ | Need to test real API but tweak one field | `route.fetch()` + modify + `route.fulfill()` | Uses real data as baseline, only overrides what you need |
1297
+ | Testing happy path with real backend | `route.continue()` (or no route) | Full integration coverage |
1298
+ | Block analytics/ads/third-party noise | `route.abort()` | Faster tests, no flakiness from external services |
1299
+ | Complex API with many endpoints | `page.routeFromHAR()` with `update: true` | Record once, replay forever; minimal test code |
1300
+ | Testing error handling (500, timeout) | `route.fulfill({ status: 500 })` or `route.abort('timedout')` | Simulate errors deterministically |
1301
+ | Verify request payload sent by frontend | `page.waitForRequest()` + assertions | Confirms frontend sends correct data |
1302
+ | Verify response data before UI check | `page.waitForResponse()` + assertions | Confirms data arrives before asserting on DOM |
1303
+ | Multiple tests need the same mock | `context.route()` or fixture | Share routes across tests without repetition |
1304
+ | Testing loading spinners / skeleton UI | `route.fulfill()` with a delay (via `setTimeout`) | Control exact timing of response |
1305
+ | Paginated or search-based API | Conditional mock (check query params / body) | Dynamic responses based on request content |
1306
+
1307
+ ## Anti-Patterns
1308
+
1309
+ | Don't Do This | Problem | Do This Instead |
1310
+ |---|---|---|
1311
+ | Mocking your own app's pages and static assets | You end up testing a fake app, not your real one | Only mock API/data endpoints, never HTML/JS/CSS served by your app |
1312
+ | Hardcoding mock data inline in every test | Duplicated data, hard to update when API changes | Extract mock data into shared fixtures (`tests/data/users.json`) |
1313
+ | Never updating mocks when the API changes | Tests pass against stale data; real app breaks | Use HAR recording with periodic `update: true` runs, or validate mock shapes against OpenAPI schemas |
1314
+ | Mocking in production-like E2E tests | You lose integration confidence | Keep a separate test suite with real backends for smoke/integration tests; mock only in component and isolated UI tests |
1315
+ | Forgetting to call `route.fulfill()`, `route.continue()`, or `route.abort()` | Request hangs, test times out with a confusing error | Every route handler must call exactly one of the three |
1316
+ | Setting up routes after `page.goto()` | Requests fire during navigation before the route is registered | Always call `page.route()` before `page.goto()` |
1317
+ | Using `waitForResponse` after the triggering action | Race condition: response may arrive before the wait is registered | Always set up the promise before the action: `const p = page.waitForResponse(...); await click(); await p;` |
1318
+ | Mocking with `route.continue()` and thinking it mocks | `route.continue()` passes the request to the real server | Use `route.fulfill()` to return fake data |
1319
+ | Over-broad glob patterns (`**/*`) without filtering | Catches all requests including HTML, JS, CSS; breaks the app | Be specific: `**/api/**` or filter by `resourceType()` |
1320
+ | Forgetting `await` on route setup or fulfillment | Route may not be active when navigation starts | Always `await page.route(...)` and `await route.fulfill(...)` |
1321
+ | Not calling `page.unroute()` when swapping mocks mid-test | Old route handler still fires, new one is ignored or both fire | Call `page.unroute()` before registering a new handler for the same pattern |
1322
+ | Using `page.on('request')` for mocking | Event listeners are read-only; they cannot modify or fulfill requests | Use `page.route()` for interception; `page.on('request')` only for logging |
1323
+
1324
+ ## Troubleshooting
1325
+
1326
+ ### Route handler never fires
1327
+
1328
+ **Cause**: The URL pattern does not match the actual request URL. Common when the base URL includes a port number, path prefix, or the request uses a different protocol.
1329
+
1330
+ **Fix**:
1331
+ - Log all requests to find the exact URL:
1332
+ ```typescript
1333
+ page.on('request', (req) => console.log(req.method(), req.url()));
1334
+ ```
1335
+ - Ensure glob pattern matches. `**/api/users` does not match `http://localhost:3000/api/users?page=1` -- use `**/api/users*` to include query strings.
1336
+ - Check that `page.route()` is called before `page.goto()`.
1337
+
1338
+ ### Route handler fires but test still times out
1339
+
1340
+ **Cause**: The handler throws an error or never calls `fulfill`/`continue`/`abort`.
1341
+
1342
+ **Fix**:
1343
+ - Add error handling inside the route handler.
1344
+ - Ensure every code path in the handler ends with one of the three resolution methods.
1345
+ ```typescript
1346
+ await page.route('**/api/users', async (route) => {
1347
+ try {
1348
+ const data = getTestData(); // this might throw
1349
+ await route.fulfill({ json: data });
1350
+ } catch (error) {
1351
+ console.error('Route handler error:', error);
1352
+ await route.abort();
1353
+ }
1354
+ });
1355
+ ```
1356
+
1357
+ ### Mocked response is ignored -- app shows real data
1358
+
1359
+ **Cause**: The route is registered at the page level but the request is made by a service worker or a different browser context.
1360
+
1361
+ **Fix**:
1362
+ - Use `context.route()` instead of `page.route()` to cover all pages and service workers.
1363
+ - Disable service workers in the config if they interfere:
1364
+ ```typescript
1365
+ // playwright.config.ts
1366
+ export default defineConfig({
1367
+ use: {
1368
+ serviceWorkers: 'block',
1369
+ },
1370
+ });
1371
+ ```
1372
+
1373
+ ### `route.fetch()` causes infinite loop
1374
+
1375
+ **Cause**: `route.fetch()` re-issues the request, which can re-trigger the same route handler if the URL pattern matches.
1376
+
1377
+ **Fix**: Playwright handles this correctly for the same route handler -- `route.fetch()` will not re-enter the handler that called it. But if you have multiple overlapping route handlers, they can interfere. Simplify to a single handler per URL pattern, or use `route.fetch({ url: 'different-url' })` to redirect.
1378
+
1379
+ ### HAR replay returns wrong responses
1380
+
1381
+ **Cause**: HAR files match requests by URL and sometimes by POST body. If the request body changes (e.g., timestamps, CSRF tokens), the match fails.
1382
+
1383
+ **Fix**:
1384
+ - Re-record the HAR file with `update: true`.
1385
+ - Use `notFound: 'fallback'` to let unmatched requests hit the real server.
1386
+ - For POST requests with dynamic bodies, consider using `page.route()` with manual matching instead of HAR.
1387
+
1388
+ ## Related
1389
+
1390
+ - [core/when-to-mock.md](when-to-mock.md) -- decision framework for when to mock vs use real services
1391
+ - [core/api-testing.md](api-testing.md) -- testing REST and GraphQL APIs directly (without a browser)
1392
+ - [core/assertions-and-waiting.md](assertions-and-waiting.md) -- web-first assertions and `waitForResponse` patterns
1393
+ - [core/authentication.md](authentication.md) -- mocking auth tokens and session state
1394
+ - [core/error-and-edge-cases.md](error-and-edge-cases.md) -- error state testing patterns beyond network errors
1395
+ - [core/service-workers-and-pwa.md](service-workers-and-pwa.md) -- handling service worker caching that interferes with mocks