@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,1181 @@
1
+ # Error States and Edge Cases
2
+
3
+ > **When to use**: Testing how your application handles errors, failures, boundary conditions, and unusual user behavior. These tests catch bugs that happy-path tests miss.
4
+ > **Prerequisites**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/network-mocking.md](network-mocking.md) for route interception
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Mock a 500 server error
10
+ await page.route('**/api/data', (route) => route.fulfill({ status: 500 }));
11
+
12
+ // Simulate offline mode
13
+ await page.context().setOffline(true);
14
+
15
+ // Test empty state
16
+ await page.route('**/api/items', (route) =>
17
+ route.fulfill({ status: 200, json: [] })
18
+ );
19
+
20
+ // Browser back/forward
21
+ await page.goBack();
22
+ await page.goForward();
23
+
24
+ // Abort a network request (simulate network failure)
25
+ await page.route('**/api/save', (route) => route.abort('connectionfailed'));
26
+ ```
27
+
28
+ ## Patterns
29
+
30
+ ### HTTP Error Status Codes
31
+
32
+ **Use when**: Testing that your application displays appropriate error pages or messages for 4xx and 5xx responses.
33
+ **Avoid when**: The error is handled silently (no user-facing feedback). Test via API or logs instead.
34
+
35
+ **TypeScript**
36
+ ```typescript
37
+ import { test, expect } from '@playwright/test';
38
+
39
+ test('displays 404 page for missing resources', async ({ page }) => {
40
+ // Navigate directly to a non-existent URL
41
+ await page.goto('/this-page-does-not-exist');
42
+
43
+ await expect(page.getByRole('heading', { name: /not found/i })).toBeVisible();
44
+ await expect(page.getByRole('link', { name: 'Go home' })).toBeVisible();
45
+ });
46
+
47
+ test('handles 500 server error gracefully', async ({ page }) => {
48
+ // Intercept the API call and return a 500
49
+ await page.route('**/api/dashboard', (route) =>
50
+ route.fulfill({
51
+ status: 500,
52
+ contentType: 'application/json',
53
+ body: JSON.stringify({ error: 'Internal server error' }),
54
+ })
55
+ );
56
+
57
+ await page.goto('/dashboard');
58
+
59
+ await expect(page.getByText('Something went wrong')).toBeVisible();
60
+ await expect(page.getByRole('button', { name: 'Try again' })).toBeVisible();
61
+ });
62
+
63
+ test('handles 403 forbidden with redirect to login', async ({ page }) => {
64
+ await page.route('**/api/admin/**', (route) =>
65
+ route.fulfill({ status: 403 })
66
+ );
67
+
68
+ await page.goto('/admin/settings');
69
+
70
+ // Should redirect to login or show access denied
71
+ await expect(page.getByText(/access denied|not authorized/i)).toBeVisible();
72
+ });
73
+
74
+ test('handles 429 rate limiting', async ({ page }) => {
75
+ await page.route('**/api/search*', (route) =>
76
+ route.fulfill({
77
+ status: 429,
78
+ headers: { 'Retry-After': '30' },
79
+ body: JSON.stringify({ error: 'Too many requests' }),
80
+ })
81
+ );
82
+
83
+ await page.goto('/search');
84
+ await page.getByLabel('Search').fill('test');
85
+ await page.getByRole('button', { name: 'Search' }).click();
86
+
87
+ await expect(page.getByText(/too many requests|try again later/i)).toBeVisible();
88
+ });
89
+ ```
90
+
91
+ **JavaScript**
92
+ ```javascript
93
+ const { test, expect } = require('@playwright/test');
94
+
95
+ test('displays 404 page for missing resources', async ({ page }) => {
96
+ await page.goto('/this-page-does-not-exist');
97
+
98
+ await expect(page.getByRole('heading', { name: /not found/i })).toBeVisible();
99
+ await expect(page.getByRole('link', { name: 'Go home' })).toBeVisible();
100
+ });
101
+
102
+ test('handles 500 server error gracefully', async ({ page }) => {
103
+ await page.route('**/api/dashboard', (route) =>
104
+ route.fulfill({
105
+ status: 500,
106
+ contentType: 'application/json',
107
+ body: JSON.stringify({ error: 'Internal server error' }),
108
+ })
109
+ );
110
+
111
+ await page.goto('/dashboard');
112
+
113
+ await expect(page.getByText('Something went wrong')).toBeVisible();
114
+ await expect(page.getByRole('button', { name: 'Try again' })).toBeVisible();
115
+ });
116
+
117
+ test('handles 403 forbidden with redirect to login', async ({ page }) => {
118
+ await page.route('**/api/admin/**', (route) =>
119
+ route.fulfill({ status: 403 })
120
+ );
121
+
122
+ await page.goto('/admin/settings');
123
+ await expect(page.getByText(/access denied|not authorized/i)).toBeVisible();
124
+ });
125
+
126
+ test('handles 429 rate limiting', async ({ page }) => {
127
+ await page.route('**/api/search*', (route) =>
128
+ route.fulfill({
129
+ status: 429,
130
+ headers: { 'Retry-After': '30' },
131
+ body: JSON.stringify({ error: 'Too many requests' }),
132
+ })
133
+ );
134
+
135
+ await page.goto('/search');
136
+ await page.getByLabel('Search').fill('test');
137
+ await page.getByRole('button', { name: 'Search' }).click();
138
+
139
+ await expect(page.getByText(/too many requests|try again later/i)).toBeVisible();
140
+ });
141
+ ```
142
+
143
+ ### Network Failure and Offline Mode
144
+
145
+ **Use when**: Testing how the app behaves when the network is down, requests fail, or the connection is intermittent.
146
+ **Avoid when**: The app has no offline or error handling behavior to test.
147
+
148
+ **TypeScript**
149
+ ```typescript
150
+ import { test, expect } from '@playwright/test';
151
+
152
+ test('offline mode shows offline banner', async ({ page }) => {
153
+ await page.goto('/dashboard');
154
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
155
+
156
+ // Go offline
157
+ await page.context().setOffline(true);
158
+
159
+ // Trigger a network-dependent action
160
+ await page.getByRole('button', { name: 'Refresh' }).click();
161
+
162
+ await expect(page.getByText(/offline|no connection/i)).toBeVisible();
163
+
164
+ // Go back online
165
+ await page.context().setOffline(false);
166
+
167
+ await page.getByRole('button', { name: 'Refresh' }).click();
168
+ await expect(page.getByText(/offline|no connection/i)).not.toBeVisible();
169
+ });
170
+
171
+ test('network request failure shows error state', async ({ page }) => {
172
+ // Abort specific requests to simulate network failure
173
+ await page.route('**/api/user/profile', (route) =>
174
+ route.abort('connectionfailed')
175
+ );
176
+
177
+ await page.goto('/profile');
178
+
179
+ await expect(page.getByText('Failed to load profile')).toBeVisible();
180
+ await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
181
+ });
182
+
183
+ test('request timeout shows timeout message', async ({ page }) => {
184
+ // Delay the response beyond the app's timeout threshold
185
+ await page.route('**/api/reports', async (route) => {
186
+ await new Promise((resolve) => setTimeout(resolve, 30_000));
187
+ await route.fulfill({ status: 200, json: { data: [] } });
188
+ });
189
+
190
+ await page.goto('/reports');
191
+
192
+ // App should show timeout message before Playwright's own timeout
193
+ await expect(page.getByText(/timed out|taking too long/i)).toBeVisible({
194
+ timeout: 20_000,
195
+ });
196
+ });
197
+
198
+ test('intermittent connectivity — request fails then succeeds', async ({ page }) => {
199
+ let requestCount = 0;
200
+
201
+ await page.route('**/api/data', (route) => {
202
+ requestCount++;
203
+ if (requestCount <= 2) {
204
+ return route.abort('connectionfailed');
205
+ }
206
+ return route.fulfill({ status: 200, json: { items: ['a', 'b', 'c'] } });
207
+ });
208
+
209
+ await page.goto('/data');
210
+
211
+ // First load fails
212
+ await expect(page.getByText(/failed|error/i)).toBeVisible();
213
+
214
+ // User retries — still fails
215
+ await page.getByRole('button', { name: 'Retry' }).click();
216
+ await expect(page.getByText(/failed|error/i)).toBeVisible();
217
+
218
+ // Third attempt succeeds
219
+ await page.getByRole('button', { name: 'Retry' }).click();
220
+ await expect(page.getByRole('listitem')).toHaveCount(3);
221
+ });
222
+ ```
223
+
224
+ **JavaScript**
225
+ ```javascript
226
+ const { test, expect } = require('@playwright/test');
227
+
228
+ test('offline mode shows offline banner', async ({ page }) => {
229
+ await page.goto('/dashboard');
230
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
231
+
232
+ await page.context().setOffline(true);
233
+
234
+ await page.getByRole('button', { name: 'Refresh' }).click();
235
+ await expect(page.getByText(/offline|no connection/i)).toBeVisible();
236
+
237
+ await page.context().setOffline(false);
238
+
239
+ await page.getByRole('button', { name: 'Refresh' }).click();
240
+ await expect(page.getByText(/offline|no connection/i)).not.toBeVisible();
241
+ });
242
+
243
+ test('network request failure shows error state', async ({ page }) => {
244
+ await page.route('**/api/user/profile', (route) =>
245
+ route.abort('connectionfailed')
246
+ );
247
+
248
+ await page.goto('/profile');
249
+
250
+ await expect(page.getByText('Failed to load profile')).toBeVisible();
251
+ await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
252
+ });
253
+
254
+ test('intermittent connectivity — request fails then succeeds', async ({ page }) => {
255
+ let requestCount = 0;
256
+
257
+ await page.route('**/api/data', (route) => {
258
+ requestCount++;
259
+ if (requestCount <= 2) {
260
+ return route.abort('connectionfailed');
261
+ }
262
+ return route.fulfill({ status: 200, json: { items: ['a', 'b', 'c'] } });
263
+ });
264
+
265
+ await page.goto('/data');
266
+
267
+ await expect(page.getByText(/failed|error/i)).toBeVisible();
268
+
269
+ await page.getByRole('button', { name: 'Retry' }).click();
270
+ await expect(page.getByText(/failed|error/i)).toBeVisible();
271
+
272
+ await page.getByRole('button', { name: 'Retry' }).click();
273
+ await expect(page.getByRole('listitem')).toHaveCount(3);
274
+ });
275
+ ```
276
+
277
+ ### Aborting on Unexpected Requests (`test.abort()`, Playwright 1.60+)
278
+
279
+ **Use when**: A route handler sees a request that should *never* happen (a call to a forbidden endpoint, a leaked third-party request, a missing mock), and you'd rather fail loudly at the source than chase a confusing downstream assertion.
280
+ **Avoid when**: You can assert the condition in the test body — use `expect()` there. `test.abort()` shines in route handlers and fixtures, which run outside the test body.
281
+
282
+ Playwright 1.60 adds `test.abort(message?)` to stop the running test immediately with a clear message, from inside a route handler or fixture.
283
+
284
+ **TypeScript**
285
+ ```typescript
286
+ import { test, expect } from '@playwright/test';
287
+
288
+ test('fails fast if the app calls a forbidden endpoint', async ({ page }) => {
289
+ await page.route('**/api/**', (route) => {
290
+ const url = route.request().url();
291
+ if (url.includes('/api/admin/delete-all')) {
292
+ // Stop now with a precise message instead of letting the page error out
293
+ test.abort(`Blocked dangerous request: ${url}`);
294
+ }
295
+ return route.continue();
296
+ });
297
+
298
+ await page.goto('/dashboard');
299
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
300
+ });
301
+ ```
302
+
303
+ **JavaScript**
304
+ ```javascript
305
+ const { test, expect } = require('@playwright/test');
306
+
307
+ test('fails fast if the app calls a forbidden endpoint', async ({ page }) => {
308
+ await page.route('**/api/**', (route) => {
309
+ if (route.request().url().includes('/api/admin/delete-all')) {
310
+ test.abort('Blocked dangerous request');
311
+ }
312
+ return route.continue();
313
+ });
314
+
315
+ await page.goto('/dashboard');
316
+ });
317
+ ```
318
+
319
+ See [core/fixtures-and-hooks.md](fixtures-and-hooks.md) for aborting from fixture setup.
320
+
321
+ ### Empty States and Boundary Testing
322
+
323
+ **Use when**: Testing what the UI shows when there is no data, when inputs are at their minimum or maximum values, or when inputs contain special characters.
324
+ **Avoid when**: Never. Every feature should have empty state and boundary tests.
325
+
326
+ **TypeScript**
327
+ ```typescript
328
+ import { test, expect } from '@playwright/test';
329
+
330
+ test('empty state is shown when no items exist', async ({ page }) => {
331
+ // Mock an empty response
332
+ await page.route('**/api/tasks', (route) =>
333
+ route.fulfill({ status: 200, json: [] })
334
+ );
335
+
336
+ await page.goto('/tasks');
337
+
338
+ await expect(page.getByText('No tasks yet')).toBeVisible();
339
+ await expect(page.getByRole('link', { name: 'Create your first task' })).toBeVisible();
340
+
341
+ // List elements should not be present
342
+ await expect(page.getByRole('listitem')).toHaveCount(0);
343
+ });
344
+
345
+ test('handles maximum length input', async ({ page }) => {
346
+ await page.goto('/profile');
347
+
348
+ // Fill with max-length string
349
+ const maxLengthName = 'A'.repeat(255);
350
+ await page.getByLabel('Display name').fill(maxLengthName);
351
+ await page.getByRole('button', { name: 'Save' }).click();
352
+
353
+ // Verify the name was saved (or truncated, depending on app behavior)
354
+ await expect(page.getByText('Profile updated')).toBeVisible();
355
+ });
356
+
357
+ test('handles special characters in input', async ({ page }) => {
358
+ await page.goto('/search');
359
+
360
+ const specialInputs = [
361
+ '<script>alert("xss")</script>',
362
+ '"; DROP TABLE users; --',
363
+ 'unicode: \u00e9\u00e0\u00fc\u00f1 \u4f60\u597d \ud83d\ude80',
364
+ 'null bytes: \x00\x01\x02',
365
+ 'path traversal: ../../etc/passwd',
366
+ ];
367
+
368
+ for (const input of specialInputs) {
369
+ await page.getByLabel('Search').fill(input);
370
+ await page.getByRole('button', { name: 'Search' }).click();
371
+
372
+ // App should not crash — either show results or "no results"
373
+ await expect(
374
+ page.getByText(/results|no results|no matches/i)
375
+ ).toBeVisible();
376
+ }
377
+ });
378
+
379
+ test('handles zero, one, and many items (0-1-N pattern)', async ({ page }) => {
380
+ // Zero items
381
+ await page.route('**/api/notifications', (route) =>
382
+ route.fulfill({ status: 200, json: [] })
383
+ );
384
+ await page.goto('/notifications');
385
+ await expect(page.getByText('No notifications')).toBeVisible();
386
+
387
+ // One item
388
+ await page.route('**/api/notifications', (route) =>
389
+ route.fulfill({
390
+ status: 200,
391
+ json: [{ id: 1, message: 'Welcome!' }],
392
+ })
393
+ );
394
+ await page.reload();
395
+ await expect(page.getByRole('listitem')).toHaveCount(1);
396
+ await expect(page.getByText('No notifications')).not.toBeVisible();
397
+
398
+ // Many items — verify pagination or "load more"
399
+ await page.route('**/api/notifications', (route) =>
400
+ route.fulfill({
401
+ status: 200,
402
+ json: Array.from({ length: 50 }, (_, i) => ({
403
+ id: i + 1,
404
+ message: `Notification ${i + 1}`,
405
+ })),
406
+ })
407
+ );
408
+ await page.reload();
409
+ await expect(page.getByRole('listitem').first()).toBeVisible();
410
+ await expect(page.getByRole('button', { name: /load more|show all/i })).toBeVisible();
411
+ });
412
+ ```
413
+
414
+ **JavaScript**
415
+ ```javascript
416
+ const { test, expect } = require('@playwright/test');
417
+
418
+ test('empty state is shown when no items exist', async ({ page }) => {
419
+ await page.route('**/api/tasks', (route) =>
420
+ route.fulfill({ status: 200, json: [] })
421
+ );
422
+
423
+ await page.goto('/tasks');
424
+
425
+ await expect(page.getByText('No tasks yet')).toBeVisible();
426
+ await expect(page.getByRole('link', { name: 'Create your first task' })).toBeVisible();
427
+ await expect(page.getByRole('listitem')).toHaveCount(0);
428
+ });
429
+
430
+ test('handles maximum length input', async ({ page }) => {
431
+ await page.goto('/profile');
432
+
433
+ const maxLengthName = 'A'.repeat(255);
434
+ await page.getByLabel('Display name').fill(maxLengthName);
435
+ await page.getByRole('button', { name: 'Save' }).click();
436
+
437
+ await expect(page.getByText('Profile updated')).toBeVisible();
438
+ });
439
+
440
+ test('handles special characters in input', async ({ page }) => {
441
+ await page.goto('/search');
442
+
443
+ const specialInputs = [
444
+ '<script>alert("xss")</script>',
445
+ '"; DROP TABLE users; --',
446
+ 'unicode: \u00e9\u00e0\u00fc\u00f1 \u4f60\u597d \ud83d\ude80',
447
+ 'path traversal: ../../etc/passwd',
448
+ ];
449
+
450
+ for (const input of specialInputs) {
451
+ await page.getByLabel('Search').fill(input);
452
+ await page.getByRole('button', { name: 'Search' }).click();
453
+
454
+ await expect(
455
+ page.getByText(/results|no results|no matches/i)
456
+ ).toBeVisible();
457
+ }
458
+ });
459
+
460
+ test('handles zero, one, and many items (0-1-N pattern)', async ({ page }) => {
461
+ await page.route('**/api/notifications', (route) =>
462
+ route.fulfill({ status: 200, json: [] })
463
+ );
464
+ await page.goto('/notifications');
465
+ await expect(page.getByText('No notifications')).toBeVisible();
466
+
467
+ await page.route('**/api/notifications', (route) =>
468
+ route.fulfill({
469
+ status: 200,
470
+ json: [{ id: 1, message: 'Welcome!' }],
471
+ })
472
+ );
473
+ await page.reload();
474
+ await expect(page.getByRole('listitem')).toHaveCount(1);
475
+
476
+ await page.route('**/api/notifications', (route) =>
477
+ route.fulfill({
478
+ status: 200,
479
+ json: Array.from({ length: 50 }, (_, i) => ({
480
+ id: i + 1,
481
+ message: `Notification ${i + 1}`,
482
+ })),
483
+ })
484
+ );
485
+ await page.reload();
486
+ await expect(page.getByRole('listitem').first()).toBeVisible();
487
+ await expect(page.getByRole('button', { name: /load more|show all/i })).toBeVisible();
488
+ });
489
+ ```
490
+
491
+ ### Loading States and Skeletons
492
+
493
+ **Use when**: Verifying that loading indicators, skeleton screens, or spinners appear during data fetching and disappear when data arrives.
494
+ **Avoid when**: The application renders synchronously with no loading indicators (SSR without client-side fetching).
495
+
496
+ **TypeScript**
497
+ ```typescript
498
+ import { test, expect } from '@playwright/test';
499
+
500
+ test('loading skeleton appears and resolves', async ({ page }) => {
501
+ // Delay the API response to observe the loading state
502
+ let resolveResponse: () => void;
503
+ const responseReady = new Promise<void>((resolve) => {
504
+ resolveResponse = resolve;
505
+ });
506
+
507
+ await page.route('**/api/dashboard', async (route) => {
508
+ await responseReady;
509
+ await route.fulfill({
510
+ status: 200,
511
+ json: { revenue: 12400, users: 350 },
512
+ });
513
+ });
514
+
515
+ await page.goto('/dashboard');
516
+
517
+ // Skeleton should be visible while loading
518
+ await expect(page.getByTestId('skeleton-revenue')).toBeVisible();
519
+ await expect(page.getByTestId('skeleton-users')).toBeVisible();
520
+
521
+ // Real content should not be visible yet
522
+ await expect(page.getByText('$12,400')).not.toBeVisible();
523
+
524
+ // Release the response
525
+ resolveResponse!();
526
+
527
+ // Skeleton disappears, real content appears
528
+ await expect(page.getByTestId('skeleton-revenue')).not.toBeVisible();
529
+ await expect(page.getByText('$12,400')).toBeVisible();
530
+ await expect(page.getByText('350')).toBeVisible();
531
+ });
532
+
533
+ test('spinner shown during form submission', async ({ page }) => {
534
+ let resolveSubmit: () => void;
535
+ const submitReady = new Promise<void>((resolve) => {
536
+ resolveSubmit = resolve;
537
+ });
538
+
539
+ await page.route('**/api/contact', async (route) => {
540
+ await submitReady;
541
+ await route.fulfill({ status: 200, json: { success: true } });
542
+ });
543
+
544
+ await page.goto('/contact');
545
+ await page.getByLabel('Name').fill('Jane');
546
+ await page.getByLabel('Email').fill('jane@example.com');
547
+ await page.getByLabel('Message').fill('Test');
548
+
549
+ await page.getByRole('button', { name: 'Send' }).click();
550
+
551
+ // Loading spinner/state during submission
552
+ await expect(page.getByRole('button', { name: /sending/i })).toBeVisible();
553
+ await expect(page.getByRole('button', { name: /sending/i })).toBeDisabled();
554
+
555
+ // Complete the submission
556
+ resolveSubmit!();
557
+
558
+ await expect(page.getByText('Message sent')).toBeVisible();
559
+ });
560
+ ```
561
+
562
+ **JavaScript**
563
+ ```javascript
564
+ const { test, expect } = require('@playwright/test');
565
+
566
+ test('loading skeleton appears and resolves', async ({ page }) => {
567
+ let resolveResponse;
568
+ const responseReady = new Promise((resolve) => {
569
+ resolveResponse = resolve;
570
+ });
571
+
572
+ await page.route('**/api/dashboard', async (route) => {
573
+ await responseReady;
574
+ await route.fulfill({
575
+ status: 200,
576
+ json: { revenue: 12400, users: 350 },
577
+ });
578
+ });
579
+
580
+ await page.goto('/dashboard');
581
+
582
+ await expect(page.getByTestId('skeleton-revenue')).toBeVisible();
583
+ await expect(page.getByTestId('skeleton-users')).toBeVisible();
584
+ await expect(page.getByText('$12,400')).not.toBeVisible();
585
+
586
+ resolveResponse();
587
+
588
+ await expect(page.getByTestId('skeleton-revenue')).not.toBeVisible();
589
+ await expect(page.getByText('$12,400')).toBeVisible();
590
+ await expect(page.getByText('350')).toBeVisible();
591
+ });
592
+
593
+ test('spinner shown during form submission', async ({ page }) => {
594
+ let resolveSubmit;
595
+ const submitReady = new Promise((resolve) => {
596
+ resolveSubmit = resolve;
597
+ });
598
+
599
+ await page.route('**/api/contact', async (route) => {
600
+ await submitReady;
601
+ await route.fulfill({ status: 200, json: { success: true } });
602
+ });
603
+
604
+ await page.goto('/contact');
605
+ await page.getByLabel('Name').fill('Jane');
606
+ await page.getByLabel('Email').fill('jane@example.com');
607
+ await page.getByLabel('Message').fill('Test');
608
+
609
+ await page.getByRole('button', { name: 'Send' }).click();
610
+
611
+ await expect(page.getByRole('button', { name: /sending/i })).toBeVisible();
612
+ await expect(page.getByRole('button', { name: /sending/i })).toBeDisabled();
613
+
614
+ resolveSubmit();
615
+
616
+ await expect(page.getByText('Message sent')).toBeVisible();
617
+ });
618
+ ```
619
+
620
+ ### Retry Behavior Testing
621
+
622
+ **Use when**: Testing that the application retries failed requests automatically or via a user-triggered "retry" button.
623
+ **Avoid when**: The application has no retry mechanism.
624
+
625
+ **TypeScript**
626
+ ```typescript
627
+ import { test, expect } from '@playwright/test';
628
+
629
+ test('retry button recovers from a failed API call', async ({ page }) => {
630
+ let callCount = 0;
631
+
632
+ await page.route('**/api/feed', (route) => {
633
+ callCount++;
634
+ if (callCount === 1) {
635
+ return route.fulfill({ status: 500 });
636
+ }
637
+ return route.fulfill({
638
+ status: 200,
639
+ json: { posts: [{ id: 1, title: 'Hello World' }] },
640
+ });
641
+ });
642
+
643
+ await page.goto('/feed');
644
+
645
+ // First load fails
646
+ await expect(page.getByText(/something went wrong/i)).toBeVisible();
647
+
648
+ // Click retry — second call succeeds
649
+ await page.getByRole('button', { name: 'Try again' }).click();
650
+
651
+ await expect(page.getByText('Hello World')).toBeVisible();
652
+ expect(callCount).toBe(2);
653
+ });
654
+
655
+ test('automatic retry with exponential backoff', async ({ page }) => {
656
+ const callTimestamps: number[] = [];
657
+
658
+ await page.route('**/api/status', (route) => {
659
+ callTimestamps.push(Date.now());
660
+ if (callTimestamps.length <= 3) {
661
+ return route.fulfill({ status: 503 });
662
+ }
663
+ return route.fulfill({ status: 200, json: { status: 'ok' } });
664
+ });
665
+
666
+ await page.goto('/status');
667
+
668
+ // Wait for the auto-retry to eventually succeed
669
+ await expect(page.getByText('System operational')).toBeVisible({
670
+ timeout: 30_000,
671
+ });
672
+
673
+ // Verify multiple retry attempts were made
674
+ expect(callTimestamps.length).toBeGreaterThanOrEqual(4);
675
+
676
+ // Verify backoff: gaps between retries should increase
677
+ if (callTimestamps.length >= 3) {
678
+ const gap1 = callTimestamps[1] - callTimestamps[0];
679
+ const gap2 = callTimestamps[2] - callTimestamps[1];
680
+ expect(gap2).toBeGreaterThanOrEqual(gap1);
681
+ }
682
+ });
683
+ ```
684
+
685
+ **JavaScript**
686
+ ```javascript
687
+ const { test, expect } = require('@playwright/test');
688
+
689
+ test('retry button recovers from a failed API call', async ({ page }) => {
690
+ let callCount = 0;
691
+
692
+ await page.route('**/api/feed', (route) => {
693
+ callCount++;
694
+ if (callCount === 1) {
695
+ return route.fulfill({ status: 500 });
696
+ }
697
+ return route.fulfill({
698
+ status: 200,
699
+ json: { posts: [{ id: 1, title: 'Hello World' }] },
700
+ });
701
+ });
702
+
703
+ await page.goto('/feed');
704
+
705
+ await expect(page.getByText(/something went wrong/i)).toBeVisible();
706
+
707
+ await page.getByRole('button', { name: 'Try again' }).click();
708
+
709
+ await expect(page.getByText('Hello World')).toBeVisible();
710
+ expect(callCount).toBe(2);
711
+ });
712
+
713
+ test('automatic retry with exponential backoff', async ({ page }) => {
714
+ const callTimestamps = [];
715
+
716
+ await page.route('**/api/status', (route) => {
717
+ callTimestamps.push(Date.now());
718
+ if (callTimestamps.length <= 3) {
719
+ return route.fulfill({ status: 503 });
720
+ }
721
+ return route.fulfill({ status: 200, json: { status: 'ok' } });
722
+ });
723
+
724
+ await page.goto('/status');
725
+
726
+ await expect(page.getByText('System operational')).toBeVisible({
727
+ timeout: 30_000,
728
+ });
729
+
730
+ expect(callTimestamps.length).toBeGreaterThanOrEqual(4);
731
+ });
732
+ ```
733
+
734
+ ### Browser Back/Forward Navigation
735
+
736
+ **Use when**: Testing that the application handles browser history navigation correctly — preserving state, URL updates, and content after going back or forward.
737
+ **Avoid when**: The app is a single-page application that does not use the browser history API. Focus on client-side routing tests instead.
738
+
739
+ **TypeScript**
740
+ ```typescript
741
+ import { test, expect } from '@playwright/test';
742
+
743
+ test('browser back preserves navigation context', async ({ page }) => {
744
+ await page.goto('/products');
745
+ await page.getByRole('link', { name: 'Running Shoes' }).click();
746
+ await page.waitForURL('**/products/running-shoes');
747
+
748
+ // Go back
749
+ await page.goBack();
750
+
751
+ await expect(page).toHaveURL(/\/products$/);
752
+ await expect(page.getByRole('heading', { name: 'Products' })).toBeVisible();
753
+ });
754
+
755
+ test('browser forward returns to previous page', async ({ page }) => {
756
+ await page.goto('/products');
757
+ await page.getByRole('link', { name: 'Running Shoes' }).click();
758
+ await page.waitForURL('**/products/running-shoes');
759
+
760
+ await page.goBack();
761
+ await page.goForward();
762
+
763
+ await expect(page).toHaveURL(/\/products\/running-shoes/);
764
+ await expect(page.getByRole('heading', { name: 'Running Shoes' })).toBeVisible();
765
+ });
766
+
767
+ test('form state after browser back', async ({ page }) => {
768
+ await page.goto('/checkout');
769
+
770
+ // Fill form on step 1
771
+ await page.getByLabel('Address').fill('123 Main St');
772
+ await page.getByRole('button', { name: 'Continue' }).click();
773
+
774
+ // Arrived at step 2
775
+ await expect(page.getByRole('heading', { name: 'Payment' })).toBeVisible();
776
+
777
+ // Go back to step 1
778
+ await page.goBack();
779
+
780
+ // Data should be preserved (depends on app implementation)
781
+ await expect(page.getByLabel('Address')).toHaveValue('123 Main St');
782
+ });
783
+
784
+ test('back button after form submission does not re-submit', async ({ page }) => {
785
+ await page.goto('/contact');
786
+
787
+ await page.getByLabel('Name').fill('Jane');
788
+ await page.getByLabel('Email').fill('jane@example.com');
789
+ await page.getByLabel('Message').fill('Test');
790
+ await page.getByRole('button', { name: 'Send' }).click();
791
+
792
+ await expect(page.getByText('Message sent')).toBeVisible();
793
+
794
+ // Going back should show the form, not re-submit
795
+ await page.goBack();
796
+ await expect(page.getByLabel('Name')).toBeVisible();
797
+ });
798
+ ```
799
+
800
+ **JavaScript**
801
+ ```javascript
802
+ const { test, expect } = require('@playwright/test');
803
+
804
+ test('browser back preserves navigation context', async ({ page }) => {
805
+ await page.goto('/products');
806
+ await page.getByRole('link', { name: 'Running Shoes' }).click();
807
+ await page.waitForURL('**/products/running-shoes');
808
+
809
+ await page.goBack();
810
+
811
+ await expect(page).toHaveURL(/\/products$/);
812
+ await expect(page.getByRole('heading', { name: 'Products' })).toBeVisible();
813
+ });
814
+
815
+ test('browser forward returns to previous page', async ({ page }) => {
816
+ await page.goto('/products');
817
+ await page.getByRole('link', { name: 'Running Shoes' }).click();
818
+ await page.waitForURL('**/products/running-shoes');
819
+
820
+ await page.goBack();
821
+ await page.goForward();
822
+
823
+ await expect(page).toHaveURL(/\/products\/running-shoes/);
824
+ await expect(page.getByRole('heading', { name: 'Running Shoes' })).toBeVisible();
825
+ });
826
+
827
+ test('form state after browser back', async ({ page }) => {
828
+ await page.goto('/checkout');
829
+
830
+ await page.getByLabel('Address').fill('123 Main St');
831
+ await page.getByRole('button', { name: 'Continue' }).click();
832
+
833
+ await expect(page.getByRole('heading', { name: 'Payment' })).toBeVisible();
834
+
835
+ await page.goBack();
836
+ await expect(page.getByLabel('Address')).toHaveValue('123 Main St');
837
+ });
838
+
839
+ test('back button after form submission does not re-submit', async ({ page }) => {
840
+ await page.goto('/contact');
841
+
842
+ await page.getByLabel('Name').fill('Jane');
843
+ await page.getByLabel('Email').fill('jane@example.com');
844
+ await page.getByLabel('Message').fill('Test');
845
+ await page.getByRole('button', { name: 'Send' }).click();
846
+
847
+ await expect(page.getByText('Message sent')).toBeVisible();
848
+
849
+ await page.goBack();
850
+ await expect(page.getByLabel('Name')).toBeVisible();
851
+ });
852
+ ```
853
+
854
+ ### Concurrent User Actions
855
+
856
+ **Use when**: Testing that rapid user interactions do not cause race conditions — double-clicking submit, typing while data is loading, navigating during an async operation.
857
+ **Avoid when**: The UI has no async operations that could conflict with user actions.
858
+
859
+ **TypeScript**
860
+ ```typescript
861
+ import { test, expect } from '@playwright/test';
862
+
863
+ test('double-click submit does not create duplicate entries', async ({ page }) => {
864
+ const requests: string[] = [];
865
+
866
+ await page.route('**/api/orders', (route) => {
867
+ requests.push(route.request().method());
868
+ return route.fulfill({
869
+ status: 201,
870
+ json: { id: 1, status: 'created' },
871
+ });
872
+ });
873
+
874
+ await page.goto('/checkout');
875
+ await page.getByLabel('Item').fill('Widget');
876
+
877
+ const submitButton = page.getByRole('button', { name: 'Place order' });
878
+
879
+ // Rapid double-click
880
+ await submitButton.dblclick();
881
+
882
+ // Wait for the result
883
+ await expect(page.getByText('Order confirmed')).toBeVisible();
884
+
885
+ // The app should prevent duplicate submissions
886
+ // (button disabled after first click, or server deduplication)
887
+ expect(requests.filter((m) => m === 'POST').length).toBeLessThanOrEqual(1);
888
+ });
889
+
890
+ test('typing during navigation does not crash', async ({ page }) => {
891
+ await page.goto('/search');
892
+
893
+ // Start typing and immediately navigate
894
+ await page.getByLabel('Search').pressSequentially('test query', { delay: 30 });
895
+ await page.getByRole('link', { name: 'Home' }).click();
896
+
897
+ // Should arrive at home page without errors
898
+ await page.waitForURL('**/');
899
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
900
+ });
901
+
902
+ test('rapid filter changes use latest value', async ({ page }) => {
903
+ const requestUrls: string[] = [];
904
+
905
+ await page.route('**/api/products*', (route) => {
906
+ requestUrls.push(route.request().url());
907
+ return route.fulfill({
908
+ status: 200,
909
+ json: { products: [{ name: 'Latest result' }] },
910
+ });
911
+ });
912
+
913
+ await page.goto('/products');
914
+
915
+ // Rapidly change the filter
916
+ await page.getByLabel('Category').selectOption('electronics');
917
+ await page.getByLabel('Category').selectOption('clothing');
918
+ await page.getByLabel('Category').selectOption('books');
919
+
920
+ // Wait for the final result
921
+ await expect(page.getByText('Latest result')).toBeVisible();
922
+
923
+ // The UI should show results for "books", not an earlier selection
924
+ // Some apps debounce; some cancel in-flight requests
925
+ });
926
+ ```
927
+
928
+ **JavaScript**
929
+ ```javascript
930
+ const { test, expect } = require('@playwright/test');
931
+
932
+ test('double-click submit does not create duplicate entries', async ({ page }) => {
933
+ const requests = [];
934
+
935
+ await page.route('**/api/orders', (route) => {
936
+ requests.push(route.request().method());
937
+ return route.fulfill({
938
+ status: 201,
939
+ json: { id: 1, status: 'created' },
940
+ });
941
+ });
942
+
943
+ await page.goto('/checkout');
944
+ await page.getByLabel('Item').fill('Widget');
945
+
946
+ const submitButton = page.getByRole('button', { name: 'Place order' });
947
+ await submitButton.dblclick();
948
+
949
+ await expect(page.getByText('Order confirmed')).toBeVisible();
950
+
951
+ expect(requests.filter((m) => m === 'POST').length).toBeLessThanOrEqual(1);
952
+ });
953
+
954
+ test('typing during navigation does not crash', async ({ page }) => {
955
+ await page.goto('/search');
956
+
957
+ await page.getByLabel('Search').pressSequentially('test query', { delay: 30 });
958
+ await page.getByRole('link', { name: 'Home' }).click();
959
+
960
+ await page.waitForURL('**/');
961
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
962
+ });
963
+
964
+ test('rapid filter changes use latest value', async ({ page }) => {
965
+ const requestUrls = [];
966
+
967
+ await page.route('**/api/products*', (route) => {
968
+ requestUrls.push(route.request().url());
969
+ return route.fulfill({
970
+ status: 200,
971
+ json: { products: [{ name: 'Latest result' }] },
972
+ });
973
+ });
974
+
975
+ await page.goto('/products');
976
+
977
+ await page.getByLabel('Category').selectOption('electronics');
978
+ await page.getByLabel('Category').selectOption('clothing');
979
+ await page.getByLabel('Category').selectOption('books');
980
+
981
+ await expect(page.getByText('Latest result')).toBeVisible();
982
+ });
983
+ ```
984
+
985
+ ### Graceful Degradation
986
+
987
+ **Use when**: Testing that the app continues to function when non-critical services fail (analytics, chat widget, recommendations).
988
+ **Avoid when**: The failing service is critical to the core workflow.
989
+
990
+ **TypeScript**
991
+ ```typescript
992
+ import { test, expect } from '@playwright/test';
993
+
994
+ test('page works when analytics service fails', async ({ page }) => {
995
+ // Block analytics and tracking scripts
996
+ await page.route('**/analytics/**', (route) => route.abort());
997
+ await page.route('**/tracking/**', (route) => route.abort());
998
+
999
+ await page.goto('/products');
1000
+
1001
+ // Core functionality still works
1002
+ await expect(page.getByRole('heading', { name: 'Products' })).toBeVisible();
1003
+ await page.getByRole('link', { name: 'Running Shoes' }).click();
1004
+ await expect(page.getByRole('button', { name: 'Add to cart' })).toBeEnabled();
1005
+ });
1006
+
1007
+ test('page works when recommendation engine fails', async ({ page }) => {
1008
+ await page.route('**/api/recommendations', (route) =>
1009
+ route.fulfill({ status: 500 })
1010
+ );
1011
+
1012
+ await page.goto('/products/running-shoes');
1013
+
1014
+ // Main product content loads
1015
+ await expect(page.getByRole('heading', { name: 'Running Shoes' })).toBeVisible();
1016
+ await expect(page.getByRole('button', { name: 'Add to cart' })).toBeEnabled();
1017
+
1018
+ // Recommendations section shows fallback
1019
+ await expect(
1020
+ page.getByText(/recommendations unavailable|you may also like/i)
1021
+ ).toBeVisible();
1022
+ });
1023
+
1024
+ test('page works when third-party chat widget fails to load', async ({ page }) => {
1025
+ // Block the chat widget script
1026
+ await page.route('**/chat-widget.js', (route) => route.abort());
1027
+
1028
+ await page.goto('/support');
1029
+
1030
+ // Core support page loads
1031
+ await expect(page.getByRole('heading', { name: 'Help Center' })).toBeVisible();
1032
+
1033
+ // No console errors from the blocked widget should crash the page
1034
+ const errors: string[] = [];
1035
+ page.on('pageerror', (error) => errors.push(error.message));
1036
+
1037
+ // Navigate around to ensure no cascade failures
1038
+ await page.getByRole('link', { name: 'FAQ' }).click();
1039
+ await expect(page.getByRole('heading', { name: 'FAQ' })).toBeVisible();
1040
+ });
1041
+ ```
1042
+
1043
+ **JavaScript**
1044
+ ```javascript
1045
+ const { test, expect } = require('@playwright/test');
1046
+
1047
+ test('page works when analytics service fails', async ({ page }) => {
1048
+ await page.route('**/analytics/**', (route) => route.abort());
1049
+ await page.route('**/tracking/**', (route) => route.abort());
1050
+
1051
+ await page.goto('/products');
1052
+
1053
+ await expect(page.getByRole('heading', { name: 'Products' })).toBeVisible();
1054
+ await page.getByRole('link', { name: 'Running Shoes' }).click();
1055
+ await expect(page.getByRole('button', { name: 'Add to cart' })).toBeEnabled();
1056
+ });
1057
+
1058
+ test('page works when recommendation engine fails', async ({ page }) => {
1059
+ await page.route('**/api/recommendations', (route) =>
1060
+ route.fulfill({ status: 500 })
1061
+ );
1062
+
1063
+ await page.goto('/products/running-shoes');
1064
+
1065
+ await expect(page.getByRole('heading', { name: 'Running Shoes' })).toBeVisible();
1066
+ await expect(page.getByRole('button', { name: 'Add to cart' })).toBeEnabled();
1067
+ });
1068
+
1069
+ test('page works when third-party chat widget fails to load', async ({ page }) => {
1070
+ await page.route('**/chat-widget.js', (route) => route.abort());
1071
+
1072
+ await page.goto('/support');
1073
+
1074
+ await expect(page.getByRole('heading', { name: 'Help Center' })).toBeVisible();
1075
+
1076
+ await page.getByRole('link', { name: 'FAQ' }).click();
1077
+ await expect(page.getByRole('heading', { name: 'FAQ' })).toBeVisible();
1078
+ });
1079
+ ```
1080
+
1081
+ ## Decision Guide
1082
+
1083
+ | Scenario | Approach | Key API |
1084
+ |---|---|---|
1085
+ | 404 page | Navigate to non-existent URL, assert error page | `page.goto('/nonexistent')` |
1086
+ | 500 server error | Mock route with `status: 500` | `page.route(url, route => route.fulfill({ status: 500 }))` |
1087
+ | Network failure | Abort the route | `route.abort('connectionfailed')` |
1088
+ | Offline mode | Toggle offline on the browser context | `page.context().setOffline(true)` |
1089
+ | Slow response | Delay route fulfillment with a Promise | `await new Promise(r => setTimeout(r, delay))` in route handler |
1090
+ | Empty state | Mock API to return empty array | `route.fulfill({ json: [] })` |
1091
+ | Boundary values | Fill inputs with min/max/special values | `locator.fill('A'.repeat(255))` |
1092
+ | Loading skeleton | Delay route, assert skeleton visible, release, assert content | Promise-based route handler |
1093
+ | Retry behavior | Track route call count, fail first N, succeed after | Counter in route handler |
1094
+ | Browser history | Use `page.goBack()` and `page.goForward()` | Assert URL and content after navigation |
1095
+ | Double submit | `dblclick()` on submit, verify single POST | Track requests in route handler |
1096
+ | Third-party failure | Abort non-critical routes, verify core works | `route.abort()` on optional services |
1097
+ | Console error monitoring | Listen for `pageerror` event | `page.on('pageerror', handler)` |
1098
+
1099
+ ## Anti-Patterns
1100
+
1101
+ | Don't Do This | Problem | Do This Instead |
1102
+ |---|---|---|
1103
+ | Only testing the happy path | Real users hit errors. Errors in production are more expensive than test time. | Add error/edge case tests for every feature |
1104
+ | `page.route('**/*', route => route.abort())` | Blocks ALL requests including the page itself | Target specific URLs: `page.route('**/api/specific', ...)` |
1105
+ | Using `page.waitForTimeout()` to simulate slow loading | Arbitrary, flaky, slows tests | Use promise-based route handlers to control timing precisely |
1106
+ | Hardcoding error messages in assertions | Messages change. Tests break on copy edits. | Use regex or partial matches: `getByText(/error/i)` |
1107
+ | Testing every error code in one mega-test | Hard to debug, slow, first failure hides the rest | One test per error scenario, or use `test.describe` groups |
1108
+ | Skipping empty state tests | Empty states are the first thing new users see; often broken | Always test 0-item state alongside 1-item and N-item |
1109
+ | Testing offline by disconnecting real network | Flaky, affects parallel tests, CI may not support it | Use `context.setOffline(true)` — deterministic and scoped |
1110
+ | Not cleaning up route mocks between tests | Routes persist on the page. Test B inherits test A's mocks. | Each test sets its own routes; Playwright resets between tests by default |
1111
+ | Asserting on exact error strings from the server | Couples tests to backend implementation details | Assert the UI message, not the raw API response body |
1112
+ | Using `try/catch` to "handle" expected errors | Swallows real bugs; test passes when it shouldn't | Let errors propagate; use route mocking to control the response |
1113
+
1114
+ ## Troubleshooting
1115
+
1116
+ ### Route handler is not intercepting requests
1117
+
1118
+ **Cause**: The URL pattern does not match, or the route was registered after the navigation that triggers the request.
1119
+
1120
+ ```typescript
1121
+ // Always register routes BEFORE navigating
1122
+ await page.route('**/api/data', (route) => route.fulfill({ status: 500 }));
1123
+ await page.goto('/dashboard'); // route is active before the page loads
1124
+
1125
+ // Debug: log all requests to find the actual URL pattern
1126
+ page.on('request', (req) => console.log(req.url()));
1127
+ ```
1128
+
1129
+ ### `context.setOffline(true)` does not affect Service Worker
1130
+
1131
+ **Cause**: Service Workers have their own network handling. `setOffline` simulates offline at the browser level, but a Service Worker may serve cached responses.
1132
+
1133
+ ```typescript
1134
+ // Unregister service workers first
1135
+ await page.evaluate(async () => {
1136
+ const registrations = await navigator.serviceWorker.getRegistrations();
1137
+ for (const registration of registrations) {
1138
+ await registration.unregister();
1139
+ }
1140
+ });
1141
+ await page.context().setOffline(true);
1142
+ ```
1143
+
1144
+ ### Delayed route handler causes test timeout
1145
+
1146
+ **Cause**: The promise in the route handler never resolves, or the delay exceeds the test timeout.
1147
+
1148
+ ```typescript
1149
+ // Always ensure the delay is less than the assertion timeout
1150
+ await page.route('**/api/slow', async (route) => {
1151
+ await new Promise((resolve) => setTimeout(resolve, 5_000)); // 5s delay
1152
+ await route.fulfill({ status: 200, json: {} });
1153
+ });
1154
+
1155
+ // Increase assertion timeout to account for the artificial delay
1156
+ await expect(page.getByText('Data loaded')).toBeVisible({ timeout: 10_000 });
1157
+ ```
1158
+
1159
+ ### `goBack()` does not navigate
1160
+
1161
+ **Cause**: There is no history entry to go back to. `goBack()` requires at least one previous navigation.
1162
+
1163
+ ```typescript
1164
+ // Ensure there is history before calling goBack
1165
+ await page.goto('/page-a');
1166
+ await page.goto('/page-b');
1167
+ await page.goBack(); // goes to /page-a
1168
+
1169
+ // If using SPA client-side routing, goBack may not work if the router
1170
+ // does not push to browser history. Use the app's own back button instead.
1171
+ await page.getByRole('button', { name: 'Back' }).click();
1172
+ ```
1173
+
1174
+ ## Related
1175
+
1176
+ - [core/assertions-and-waiting.md](assertions-and-waiting.md) -- assertion strategies for error states
1177
+ - [core/network-mocking.md](network-mocking.md) -- detailed network interception and mocking patterns
1178
+ - [core/forms-and-validation.md](forms-and-validation.md) -- form validation error testing
1179
+ - [core/flaky-tests.md](flaky-tests.md) -- fixing timing issues in error/edge case tests
1180
+ - [core/service-workers-and-pwa.md](service-workers-and-pwa.md) -- offline-first and PWA testing patterns
1181
+ - [core/multi-context-and-popups.md](multi-context-and-popups.md) -- testing concurrent browser contexts