@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,1903 @@
1
+ # Playwright Error Index
2
+
3
+ Quick-reference for specific Playwright error messages. Find your error, understand the cause, apply the fix.
4
+
5
+ > **How to use**: Search this file for the exact error text you see in your terminal or test report. Each entry gives you the cause and a working fix.
6
+
7
+ ---
8
+
9
+ ## Locator & Element Errors
10
+
11
+ ---
12
+
13
+ ### "locator.click: Target closed"
14
+
15
+ **Cause**: The page or frame navigated away (or was closed) before Playwright finished executing the action on the element.
16
+
17
+ **Common triggers**:
18
+ - Clicking a link that triggers navigation while another action is still pending
19
+ - A form submit causes a page reload before a subsequent `click()` or `fill()` resolves
20
+ - Calling `page.close()` or `context.close()` in a `finally` block that races with an in-flight action
21
+ - An unhandled exception in the application triggers an error page redirect
22
+
23
+ **Fix**: Wait for navigation to complete before performing the next action, or use `Promise.all` to coordinate the click and navigation together.
24
+
25
+ ```typescript
26
+ // TypeScript — wait for navigation caused by clicking a link
27
+ await Promise.all([
28
+ page.waitForURL('**/dashboard'),
29
+ page.getByRole('link', { name: 'Dashboard' }).click(),
30
+ ]);
31
+ ```
32
+
33
+ ```javascript
34
+ // JavaScript — same pattern
35
+ await Promise.all([
36
+ page.waitForURL('**/dashboard'),
37
+ page.getByRole('link', { name: 'Dashboard' }).click(),
38
+ ]);
39
+ ```
40
+
41
+ If the page is closing intentionally (e.g., a popup), capture a reference before the action:
42
+
43
+ ```typescript
44
+ // TypeScript — handle popup that closes itself
45
+ const popupPromise = page.waitForEvent('popup');
46
+ await page.getByRole('button', { name: 'Open popup' }).click();
47
+ const popup = await popupPromise;
48
+ await popup.waitForLoadState();
49
+ // interact with popup before it closes
50
+ await popup.getByRole('button', { name: 'Confirm' }).click();
51
+ ```
52
+
53
+ ```javascript
54
+ // JavaScript — handle popup that closes itself
55
+ const popupPromise = page.waitForEvent('popup');
56
+ await page.getByRole('button', { name: 'Open popup' }).click();
57
+ const popup = await popupPromise;
58
+ await popup.waitForLoadState();
59
+ await popup.getByRole('button', { name: 'Confirm' }).click();
60
+ ```
61
+
62
+ **Related**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/multi-context-and-popups.md](multi-context-and-popups.md)
63
+
64
+ ---
65
+
66
+ ### "waiting for locator('...') to be visible"
67
+
68
+ **Cause**: Playwright's auto-waiting timed out because the element never appeared in the DOM or remained hidden (e.g., `display: none`, `visibility: hidden`, zero size, or behind `aria-hidden`).
69
+
70
+ **Common triggers**:
71
+ - The element renders conditionally and the condition was not met (missing data, unauthenticated state)
72
+ - Content loads asynchronously and the locator was evaluated before the API response arrived
73
+ - The selector is wrong — typo in role, name, or test ID
74
+ - The element exists but is off-screen or inside a collapsed container
75
+ - CSS animation or transition keeps the element at `opacity: 0` without making it visible to Playwright
76
+
77
+ **Fix**: First confirm the locator matches what you expect using the Playwright Inspector. Then ensure the precondition for the element to appear is met.
78
+
79
+ ```typescript
80
+ // TypeScript — debug: check what the locator resolves to
81
+ console.log(await page.getByRole('button', { name: 'Submit' }).count());
82
+ // If 0, the element is not in the DOM — check your selector or page state
83
+
84
+ // Correct approach: wait for the data to load first
85
+ await page.waitForResponse(resp =>
86
+ resp.url().includes('/api/data') && resp.status() === 200
87
+ );
88
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
89
+ ```
90
+
91
+ ```javascript
92
+ // JavaScript — same approach
93
+ console.log(await page.getByRole('button', { name: 'Submit' }).count());
94
+
95
+ await page.waitForResponse(resp =>
96
+ resp.url().includes('/api/data') && resp.status() === 200
97
+ );
98
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
99
+ ```
100
+
101
+ **Related**: [core/locators.md](locators.md), [core/assertions-and-waiting.md](assertions-and-waiting.md)
102
+
103
+ ---
104
+
105
+ ### "locator.click: Error: strict mode violation"
106
+
107
+ **Cause**: The locator matched more than one element and Playwright's strict mode (enabled by default) refuses to act on ambiguous matches.
108
+
109
+ **Common triggers**:
110
+ - Using `getByRole('button')` when multiple buttons exist on the page
111
+ - Using `getByText('Save')` when both "Save" and "Save as draft" are present
112
+ - Partial text matching when `exact: true` is needed
113
+ - The same component is rendered multiple times (e.g., in a list)
114
+
115
+ **Fix**: Make the locator more specific so it resolves to exactly one element.
116
+
117
+ ```typescript
118
+ // TypeScript
119
+
120
+ // BAD — matches multiple buttons
121
+ await page.getByRole('button', { name: 'Save' }).click();
122
+
123
+ // GOOD — use exact matching
124
+ await page.getByRole('button', { name: 'Save', exact: true }).click();
125
+
126
+ // GOOD — scope to a specific container
127
+ await page.getByRole('dialog')
128
+ .getByRole('button', { name: 'Save' }).click();
129
+
130
+ // GOOD — use .first(), .last(), .nth() when order is meaningful
131
+ await page.getByRole('listitem').first().click();
132
+
133
+ // GOOD — chain with filter for list items
134
+ await page.getByRole('listitem')
135
+ .filter({ hasText: 'Project Alpha' })
136
+ .getByRole('button', { name: 'Delete' }).click();
137
+ ```
138
+
139
+ ```javascript
140
+ // JavaScript
141
+
142
+ // BAD — matches multiple buttons
143
+ await page.getByRole('button', { name: 'Save' }).click();
144
+
145
+ // GOOD — use exact matching
146
+ await page.getByRole('button', { name: 'Save', exact: true }).click();
147
+
148
+ // GOOD — scope to a specific container
149
+ await page.getByRole('dialog')
150
+ .getByRole('button', { name: 'Save' }).click();
151
+
152
+ // GOOD — use .first(), .last(), .nth() when order is meaningful
153
+ await page.getByRole('listitem').first().click();
154
+
155
+ // GOOD — chain with filter for list items
156
+ await page.getByRole('listitem')
157
+ .filter({ hasText: 'Project Alpha' })
158
+ .getByRole('button', { name: 'Delete' }).click();
159
+ ```
160
+
161
+ **Related**: [core/locators.md](locators.md), [core/locator-strategy.md](locator-strategy.md)
162
+
163
+ ---
164
+
165
+ ### "Error: expect(locator).toBeVisible() — locator resolved to X elements"
166
+
167
+ **Cause**: Same root cause as strict mode violation above — the assertion's locator matched multiple elements, so Playwright cannot determine which one to assert on.
168
+
169
+ **Common triggers**:
170
+ - Same as strict mode violation triggers above
171
+ - Writing `expect(page.locator('.item')).toBeVisible()` when there are many `.item` elements
172
+
173
+ **Fix**: Narrow the locator to a single element (see strict mode violation fix above). Alternatively, if you intentionally want to check all matches:
174
+
175
+ ```typescript
176
+ // TypeScript — assert count instead of visibility
177
+ await expect(page.getByRole('listitem')).toHaveCount(5);
178
+
179
+ // Or assert on a specific one
180
+ await expect(page.getByRole('listitem').first()).toBeVisible();
181
+
182
+ // Or assert all are visible with a loop
183
+ for (const item of await page.getByRole('listitem').all()) {
184
+ await expect(item).toBeVisible();
185
+ }
186
+ ```
187
+
188
+ ```javascript
189
+ // JavaScript — assert count instead of visibility
190
+ await expect(page.getByRole('listitem')).toHaveCount(5);
191
+
192
+ // Or assert on a specific one
193
+ await expect(page.getByRole('listitem').first()).toBeVisible();
194
+
195
+ // Or assert all are visible with a loop
196
+ for (const item of await page.getByRole('listitem').all()) {
197
+ await expect(item).toBeVisible();
198
+ }
199
+ ```
200
+
201
+ **Related**: [core/locators.md](locators.md), [core/assertions-and-waiting.md](assertions-and-waiting.md)
202
+
203
+ ---
204
+
205
+ ### "locator.fill: Error: Element is not an <input>, <textarea> or [contenteditable] element"
206
+
207
+ **Cause**: `fill()` only works on `<input>`, `<textarea>`, or elements with `contenteditable`. The locator resolved to a different element type (e.g., a `<div>`, `<label>`, or the `<form>` itself).
208
+
209
+ **Common triggers**:
210
+ - Locating by label text but the label and input are not properly associated via `for`/`id`
211
+ - Using `getByText()` instead of `getByLabel()` or `getByRole('textbox')`
212
+ - Custom components that wrap the actual `<input>` in a styled `<div>`
213
+ - Rich text editors that use `contenteditable` on an inner element, not the outer wrapper
214
+
215
+ **Fix**: Target the actual input element.
216
+
217
+ ```typescript
218
+ // TypeScript
219
+
220
+ // BAD — targets the label, not the input
221
+ await page.getByText('Email').fill('user@example.com');
222
+
223
+ // GOOD — getByLabel finds the associated input
224
+ await page.getByLabel('Email').fill('user@example.com');
225
+
226
+ // GOOD — target by role
227
+ await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
228
+
229
+ // For contenteditable rich text editors
230
+ await page.locator('[contenteditable="true"]').fill('Hello world');
231
+
232
+ // If fill() still doesn't work (e.g., custom widget), use keyboard input
233
+ await page.getByRole('textbox', { name: 'Email' }).click();
234
+ await page.keyboard.type('user@example.com');
235
+ ```
236
+
237
+ ```javascript
238
+ // JavaScript
239
+
240
+ // BAD — targets the label, not the input
241
+ await page.getByText('Email').fill('user@example.com');
242
+
243
+ // GOOD — getByLabel finds the associated input
244
+ await page.getByLabel('Email').fill('user@example.com');
245
+
246
+ // GOOD — target by role
247
+ await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
248
+
249
+ // For contenteditable rich text editors
250
+ await page.locator('[contenteditable="true"]').fill('Hello world');
251
+
252
+ // If fill() still doesn't work (e.g., custom widget), use keyboard input
253
+ await page.getByRole('textbox', { name: 'Email' }).click();
254
+ await page.keyboard.type('user@example.com');
255
+ ```
256
+
257
+ **Related**: [core/locators.md](locators.md), [core/forms-and-validation.md](forms-and-validation.md)
258
+
259
+ ---
260
+
261
+ ### "Error: elementHandle.click: Node is not an Element"
262
+
263
+ **Cause**: You are using the legacy ElementHandle API and the handle has gone stale — the underlying DOM node was removed or replaced by a re-render.
264
+
265
+ **Common triggers**:
266
+ - Storing an `elementHandle` in a variable and using it after a navigation or React/Vue re-render
267
+ - Using `page.$()` or `page.$$()` instead of Locator API
268
+ - Framework hydration replaces the server-rendered node with a client-rendered one
269
+
270
+ **Fix**: Switch from ElementHandle to Locator. Locators re-query the DOM on every action and never go stale.
271
+
272
+ ```typescript
273
+ // TypeScript
274
+
275
+ // BAD — stale handle
276
+ const button = await page.$('button.submit');
277
+ // ... page re-renders ...
278
+ await button!.click(); // Node is not an Element
279
+
280
+ // GOOD — locator always re-queries
281
+ const button = page.getByRole('button', { name: 'Submit' });
282
+ // ... page re-renders ...
283
+ await button.click(); // works fine
284
+ ```
285
+
286
+ ```javascript
287
+ // JavaScript
288
+
289
+ // BAD — stale handle
290
+ const button = await page.$('button.submit');
291
+ // ... page re-renders ...
292
+ await button.click(); // Node is not an Element
293
+
294
+ // GOOD — locator always re-queries
295
+ const button = page.getByRole('button', { name: 'Submit' });
296
+ // ... page re-renders ...
297
+ await button.click(); // works fine
298
+ ```
299
+
300
+ **Related**: [core/locators.md](locators.md)
301
+
302
+ ---
303
+
304
+ ### "Error: locator.click: Timeout 30000ms exceeded"
305
+
306
+ **Cause**: The element was found in the DOM but was not actionable within the timeout. "Actionable" means visible, stable (not animating), enabled, and not obscured by another element.
307
+
308
+ **Common triggers**:
309
+ - An overlay, modal, or toast is covering the element
310
+ - The element is disabled (`disabled` attribute or `aria-disabled="true"`)
311
+ - CSS animation has not completed (element is still moving)
312
+ - A loading spinner overlays the button
313
+ - The element is outside the viewport and Playwright's auto-scroll failed
314
+
315
+ **Fix**: Identify what is blocking the action using traces, then address it.
316
+
317
+ ```typescript
318
+ // TypeScript
319
+
320
+ // Step 1: Enable tracing to diagnose
321
+ // In playwright.config.ts:
322
+ // use: { trace: 'on-first-retry' }
323
+ // Then run: npx playwright show-trace trace.zip
324
+
325
+ // Common fix: wait for the overlay to disappear
326
+ await expect(page.locator('.loading-overlay')).toBeHidden();
327
+ await page.getByRole('button', { name: 'Submit' }).click();
328
+
329
+ // Common fix: force click when you know the overlay is benign
330
+ // (use sparingly — this skips actionability checks)
331
+ await page.getByRole('button', { name: 'Submit' }).click({ force: true });
332
+
333
+ // Common fix: wait for the element to be enabled
334
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
335
+ await page.getByRole('button', { name: 'Submit' }).click();
336
+ ```
337
+
338
+ ```javascript
339
+ // JavaScript
340
+
341
+ // Common fix: wait for the overlay to disappear
342
+ await expect(page.locator('.loading-overlay')).toBeHidden();
343
+ await page.getByRole('button', { name: 'Submit' }).click();
344
+
345
+ // Common fix: force click (use sparingly)
346
+ await page.getByRole('button', { name: 'Submit' }).click({ force: true });
347
+
348
+ // Common fix: wait for the element to be enabled
349
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
350
+ await page.getByRole('button', { name: 'Submit' }).click();
351
+ ```
352
+
353
+ **Related**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/debugging.md](debugging.md)
354
+
355
+ ---
356
+
357
+ ## Navigation & Page Errors
358
+
359
+ ---
360
+
361
+ ### "page.goto: net::ERR_CONNECTION_REFUSED"
362
+
363
+ **Cause**: Playwright could not connect to the target URL. The server is not running or is not listening on the expected host/port.
364
+
365
+ **Common triggers**:
366
+ - Forgot to start the dev server before running tests
367
+ - Dev server is on a different port than `baseURL` in config
368
+ - Server is listening on `127.0.0.1` but test uses `localhost` (or vice versa) and the OS resolves them differently
369
+ - In CI, the application build/start step failed silently
370
+ - Docker container networking: the app runs inside a container but the test tries to reach `localhost`
371
+
372
+ **Fix**: Use the `webServer` config option to let Playwright start and manage the dev server automatically.
373
+
374
+ ```typescript
375
+ // TypeScript — playwright.config.ts
376
+ import { defineConfig } from '@playwright/test';
377
+
378
+ export default defineConfig({
379
+ webServer: {
380
+ command: 'npm run dev',
381
+ url: 'http://localhost:3000',
382
+ reuseExistingServer: !process.env.CI,
383
+ timeout: 120_000, // 2 minutes for server start
384
+ },
385
+ use: {
386
+ baseURL: 'http://localhost:3000',
387
+ },
388
+ });
389
+ ```
390
+
391
+ ```javascript
392
+ // JavaScript — playwright.config.js
393
+ const { defineConfig } = require('@playwright/test');
394
+
395
+ module.exports = defineConfig({
396
+ webServer: {
397
+ command: 'npm run dev',
398
+ url: 'http://localhost:3000',
399
+ reuseExistingServer: !process.env.CI,
400
+ timeout: 120_000,
401
+ },
402
+ use: {
403
+ baseURL: 'http://localhost:3000',
404
+ },
405
+ });
406
+ ```
407
+
408
+ **Related**: [core/configuration.md](configuration.md), [ci/ci-github-actions.md](../ci/ci-github-actions.md)
409
+
410
+ ---
411
+
412
+ ### "page.goto: Timeout 30000ms exceeded"
413
+
414
+ **Cause**: The page did not reach the `load` state (by default) within the navigation timeout. The server responded but the page took too long to fully load.
415
+
416
+ **Common triggers**:
417
+ - Large assets (images, fonts, scripts) slow the page load
418
+ - Third-party scripts (analytics, ads) blocking the load event
419
+ - The page makes many API calls before becoming interactive
420
+ - The server responded with a redirect chain that takes too long
421
+
422
+ **Fix**: Use a more appropriate `waitUntil` option or increase the navigation timeout.
423
+
424
+ ```typescript
425
+ // TypeScript
426
+
427
+ // Use 'domcontentloaded' instead of 'load' if you don't need all assets
428
+ await page.goto('/dashboard', { waitUntil: 'domcontentloaded' });
429
+
430
+ // Or increase the navigation timeout for known slow pages
431
+ await page.goto('/dashboard', { timeout: 60_000 });
432
+
433
+ // Or set it globally in config
434
+ // playwright.config.ts
435
+ export default defineConfig({
436
+ use: {
437
+ navigationTimeout: 60_000,
438
+ },
439
+ });
440
+ ```
441
+
442
+ ```javascript
443
+ // JavaScript
444
+
445
+ await page.goto('/dashboard', { waitUntil: 'domcontentloaded' });
446
+
447
+ await page.goto('/dashboard', { timeout: 60_000 });
448
+
449
+ // playwright.config.js
450
+ module.exports = defineConfig({
451
+ use: {
452
+ navigationTimeout: 60_000,
453
+ },
454
+ });
455
+ ```
456
+
457
+ **Related**: [core/configuration.md](configuration.md)
458
+
459
+ ---
460
+
461
+ ### "Error: page.goto: Navigation failed because page was closed!"
462
+
463
+ **Cause**: The page or browser context was closed while `goto()` was still in progress. This typically happens in teardown/cleanup race conditions.
464
+
465
+ **Common triggers**:
466
+ - `afterEach` or a fixture closes the page/context before navigation completes
467
+ - Another test or hook called `browser.close()` prematurely
468
+ - A test failed and the cleanup ran while an async navigation was still pending
469
+ - Using `test.fixme()` or `test.skip()` after an async operation has started
470
+
471
+ **Fix**: Ensure cleanup only runs after all page operations complete. Use fixtures for lifecycle management instead of manual `afterEach`.
472
+
473
+ ```typescript
474
+ // TypeScript
475
+
476
+ // BAD — race condition between test body and cleanup
477
+ test.afterEach(async ({ page }) => {
478
+ await page.close(); // may race with in-flight navigation
479
+ });
480
+
481
+ // GOOD — let Playwright manage page lifecycle via fixtures
482
+ // Playwright automatically creates and destroys the page per test
483
+ // No manual cleanup needed
484
+
485
+ // If you need custom cleanup, make sure to await all navigation first
486
+ test('navigates to dashboard', async ({ page }) => {
487
+ const responsePromise = page.waitForResponse('**/api/user');
488
+ await page.goto('/dashboard');
489
+ await responsePromise; // ensure all async ops are done
490
+ // test assertions here
491
+ });
492
+ ```
493
+
494
+ ```javascript
495
+ // JavaScript — same pattern
496
+
497
+ test('navigates to dashboard', async ({ page }) => {
498
+ const responsePromise = page.waitForResponse('**/api/user');
499
+ await page.goto('/dashboard');
500
+ await responsePromise;
501
+ // test assertions here
502
+ });
503
+ ```
504
+
505
+ **Related**: [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
506
+
507
+ ---
508
+
509
+ ### "page.waitForNavigation: Timeout 30000ms exceeded"
510
+
511
+ **Cause**: `waitForNavigation()` waited for a full page navigation that never happened. In SPAs, client-side routing does not trigger a navigation event.
512
+
513
+ **Common triggers**:
514
+ - Using `waitForNavigation()` with a SPA that uses React Router, Vue Router, or Next.js client-side navigation
515
+ - The navigation already happened before `waitForNavigation()` was called (race condition)
516
+ - Using the deprecated pattern when `waitForURL()` is the correct replacement
517
+
518
+ **Fix**: Replace `waitForNavigation()` with `waitForURL()` which works with both SPAs and traditional page loads.
519
+
520
+ ```typescript
521
+ // TypeScript
522
+
523
+ // BAD — deprecated, race-prone, doesn't work with SPA routing
524
+ await page.waitForNavigation();
525
+
526
+ // GOOD — works with SPAs and full navigations
527
+ await page.getByRole('link', { name: 'Dashboard' }).click();
528
+ await page.waitForURL('**/dashboard');
529
+
530
+ // GOOD — combine with glob patterns
531
+ await page.waitForURL(/\/dashboard\/\d+/);
532
+
533
+ // GOOD — for SPA route changes, assert on visible content
534
+ await page.getByRole('link', { name: 'Settings' }).click();
535
+ await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
536
+ ```
537
+
538
+ ```javascript
539
+ // JavaScript
540
+
541
+ // BAD — deprecated, race-prone
542
+ await page.waitForNavigation();
543
+
544
+ // GOOD — works with SPAs and full navigations
545
+ await page.getByRole('link', { name: 'Dashboard' }).click();
546
+ await page.waitForURL('**/dashboard');
547
+
548
+ // GOOD — combine with glob patterns
549
+ await page.waitForURL(/\/dashboard\/\d+/);
550
+
551
+ // GOOD — for SPA route changes, assert on visible content
552
+ await page.getByRole('link', { name: 'Settings' }).click();
553
+ await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
554
+ ```
555
+
556
+ **Related**: [core/assertions-and-waiting.md](assertions-and-waiting.md)
557
+
558
+ ---
559
+
560
+ ### "Error: frame.goto: Frame was detached"
561
+
562
+ **Cause**: The iframe was removed from the DOM (detached) while Playwright was navigating or interacting with it.
563
+
564
+ **Common triggers**:
565
+ - The parent page re-rendered and recreated the iframe element
566
+ - JavaScript on the parent page removed and re-added the iframe
567
+ - A SPA route change unmounted the component containing the iframe
568
+ - An ad iframe reloaded itself
569
+
570
+ **Fix**: Re-acquire the frame reference after the parent page updates.
571
+
572
+ ```typescript
573
+ // TypeScript
574
+
575
+ // BAD — frame reference becomes stale
576
+ const frame = page.frameLocator('#my-iframe');
577
+ // ... parent page re-renders ...
578
+ await frame.getByRole('button').click(); // Frame was detached
579
+
580
+ // GOOD — use frameLocator (re-evaluates each time)
581
+ // frameLocator itself is lazy, but the frame it points to must exist
582
+ await expect(page.frameLocator('#my-iframe').getByRole('button')).toBeVisible();
583
+ await page.frameLocator('#my-iframe').getByRole('button').click();
584
+
585
+ // GOOD — wait for the iframe to be present after a re-render
586
+ await expect(page.locator('#my-iframe')).toBeAttached();
587
+ await page.frameLocator('#my-iframe').getByRole('button').click();
588
+ ```
589
+
590
+ ```javascript
591
+ // JavaScript
592
+
593
+ // GOOD — frameLocator re-evaluates lazily
594
+ await expect(page.frameLocator('#my-iframe').getByRole('button')).toBeVisible();
595
+ await page.frameLocator('#my-iframe').getByRole('button').click();
596
+
597
+ // GOOD — wait for the iframe to be present after a re-render
598
+ await expect(page.locator('#my-iframe')).toBeAttached();
599
+ await page.frameLocator('#my-iframe').getByRole('button').click();
600
+ ```
601
+
602
+ **Related**: [core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md)
603
+
604
+ ---
605
+
606
+ ## Test Framework Errors
607
+
608
+ ---
609
+
610
+ ### "Error: Test timeout of 30000ms exceeded"
611
+
612
+ **Cause**: The entire test (including all hooks and assertions) did not complete within the configured test timeout.
613
+
614
+ **Common triggers**:
615
+ - Waiting for an element that never appears (wrong locator, missing data)
616
+ - Unresolved `page.waitForEvent()` — the expected event never fires
617
+ - Slow test environment (CI with limited resources)
618
+ - Too many sequential actions in a single test
619
+ - A `beforeEach` hook performs expensive setup (login, data seeding)
620
+
621
+ **Fix**: Identify the slow step using `test.step()` and traces, then either fix the root cause or adjust the timeout.
622
+
623
+ ```typescript
624
+ // TypeScript
625
+
626
+ // Increase timeout for a specific slow test
627
+ test('generates large report', async ({ page }) => {
628
+ test.setTimeout(120_000); // 2 minutes for this test only
629
+ // ...
630
+ });
631
+
632
+ // Increase timeout globally in config
633
+ // playwright.config.ts
634
+ export default defineConfig({
635
+ timeout: 60_000, // 60 seconds per test
636
+ });
637
+
638
+ // Better: diagnose with test.step() to find what's slow
639
+ test('checkout flow', async ({ page }) => {
640
+ await test.step('add item to cart', async () => {
641
+ await page.goto('/products');
642
+ await page.getByRole('button', { name: 'Add to Cart' }).click();
643
+ });
644
+
645
+ await test.step('complete checkout', async () => {
646
+ await page.getByRole('link', { name: 'Cart' }).click();
647
+ await page.getByRole('button', { name: 'Checkout' }).click();
648
+ });
649
+ });
650
+ ```
651
+
652
+ ```javascript
653
+ // JavaScript
654
+
655
+ test('generates large report', async ({ page }) => {
656
+ test.setTimeout(120_000);
657
+ // ...
658
+ });
659
+
660
+ // playwright.config.js
661
+ module.exports = defineConfig({
662
+ timeout: 60_000,
663
+ });
664
+
665
+ test('checkout flow', async ({ page }) => {
666
+ await test.step('add item to cart', async () => {
667
+ await page.goto('/products');
668
+ await page.getByRole('button', { name: 'Add to Cart' }).click();
669
+ });
670
+
671
+ await test.step('complete checkout', async () => {
672
+ await page.getByRole('link', { name: 'Cart' }).click();
673
+ await page.getByRole('button', { name: 'Checkout' }).click();
674
+ });
675
+ });
676
+ ```
677
+
678
+ **Related**: [core/configuration.md](configuration.md), [core/debugging.md](debugging.md)
679
+
680
+ ---
681
+
682
+ ### "Error: expect(received).toMatchSnapshot()"
683
+
684
+ **Cause**: The current screenshot or text does not match the stored baseline snapshot.
685
+
686
+ **Common triggers**:
687
+ - Legitimate UI changes that require updating the snapshot
688
+ - Different rendering between local and CI (fonts, anti-aliasing, OS-level rendering)
689
+ - Dynamic content (timestamps, random IDs, ads) that changes between runs
690
+ - Different viewport sizes or device emulation settings between environments
691
+
692
+ **Fix**: Update the snapshot if the change is intentional, or mask/hide dynamic areas.
693
+
694
+ ```typescript
695
+ // TypeScript
696
+
697
+ // Update snapshots after intentional changes
698
+ // Run: npx playwright test --update-snapshots
699
+
700
+ // Mask dynamic content before taking a screenshot
701
+ await expect(page).toHaveScreenshot('dashboard.png', {
702
+ mask: [
703
+ page.locator('.timestamp'),
704
+ page.locator('.random-ad'),
705
+ ],
706
+ maxDiffPixelRatio: 0.01, // allow 1% pixel difference
707
+ });
708
+
709
+ // Use text snapshot with a sanitizer for dynamic values
710
+ const content = await page.getByRole('main').textContent();
711
+ const sanitized = content!.replace(/\d{4}-\d{2}-\d{2}/g, 'DATE');
712
+ expect(sanitized).toMatchSnapshot('dashboard-content.txt');
713
+ ```
714
+
715
+ ```javascript
716
+ // JavaScript
717
+
718
+ // Update snapshots: npx playwright test --update-snapshots
719
+
720
+ await expect(page).toHaveScreenshot('dashboard.png', {
721
+ mask: [
722
+ page.locator('.timestamp'),
723
+ page.locator('.random-ad'),
724
+ ],
725
+ maxDiffPixelRatio: 0.01,
726
+ });
727
+
728
+ const content = await page.getByRole('main').textContent();
729
+ const sanitized = content.replace(/\d{4}-\d{2}-\d{2}/g, 'DATE');
730
+ expect(sanitized).toMatchSnapshot('dashboard-content.txt');
731
+ ```
732
+
733
+ **Related**: [core/visual-regression.md](visual-regression.md)
734
+
735
+ ---
736
+
737
+ ### "Error: browserType.launch: Executable doesn't exist"
738
+
739
+ **Cause**: The Playwright browser binaries are not installed on this machine or they were installed for a different Playwright version.
740
+
741
+ **Common triggers**:
742
+ - Fresh clone of a project without running the install step
743
+ - Upgrading `@playwright/test` without reinstalling browsers
744
+ - CI environment missing the install step
745
+ - Using `npm ci` which cleans `node_modules` but does not trigger the browser install postinstall script
746
+ - Multiple Playwright versions installed (global vs local)
747
+
748
+ **Fix**: Install the browsers.
749
+
750
+ ```bash
751
+ # Install all browsers
752
+ npx playwright install
753
+
754
+ # Install only the browser you need (faster in CI)
755
+ npx playwright install chromium
756
+
757
+ # Install browsers with OS dependencies (needed in CI/Docker)
758
+ npx playwright install --with-deps
759
+
760
+ # If you're on a CI Dockerfile, add this to your Dockerfile
761
+ # RUN npx playwright install --with-deps chromium
762
+ ```
763
+
764
+ For CI pipelines, always include the install step:
765
+
766
+ ```yaml
767
+ # GitHub Actions example
768
+ - name: Install Playwright Browsers
769
+ run: npx playwright install --with-deps
770
+ ```
771
+
772
+ **Related**: [ci/ci-github-actions.md](../ci/ci-github-actions.md), [ci/docker-and-containers.md](../ci/docker-and-containers.md)
773
+
774
+ ---
775
+
776
+ ### "Error: Cannot use import statement outside a module"
777
+
778
+ **Cause**: Node.js is running the test file as CommonJS but the file uses ES module `import` syntax without the right configuration.
779
+
780
+ **Common triggers**:
781
+ - Missing or misconfigured `tsconfig.json`
782
+ - Using `.ts` files without `@playwright/test` properly resolving TypeScript
783
+ - `package.json` does not have `"type": "module"` but test files use `import`
784
+ - Running test files directly with `node` instead of through `npx playwright test`
785
+ - Conflicting Babel or ts-node configuration
786
+
787
+ **Fix**: Ensure TypeScript is configured correctly and always run tests through the Playwright CLI.
788
+
789
+ ```bash
790
+ # Always run tests through the Playwright CLI — never with node directly
791
+ npx playwright test
792
+
793
+ # NOT this:
794
+ # node tests/example.spec.ts <-- will fail
795
+ ```
796
+
797
+ ```typescript
798
+ // TypeScript — tsconfig.json (minimal working config)
799
+ {
800
+ "compilerOptions": {
801
+ "target": "ES2020",
802
+ "module": "ESNext",
803
+ "moduleResolution": "bundler",
804
+ "strict": true,
805
+ "esModuleInterop": true,
806
+ "skipLibCheck": true
807
+ }
808
+ }
809
+ ```
810
+
811
+ If using JavaScript without TypeScript:
812
+
813
+ ```javascript
814
+ // JavaScript — ensure package.json has "type": "module"
815
+ // Or rename files to .mjs
816
+
817
+ // In playwright.config.js — use require if package.json lacks "type": "module"
818
+ const { defineConfig } = require('@playwright/test');
819
+ module.exports = defineConfig({ /* ... */ });
820
+ ```
821
+
822
+ **Related**: [core/configuration.md](configuration.md)
823
+
824
+ ---
825
+
826
+ ### "Error: fixture \"xxx\" has already been registered"
827
+
828
+ **Cause**: A custom fixture was defined with the same name in multiple `test.extend()` calls that got merged, or the same fixture name was registered twice in the same extension.
829
+
830
+ **Common triggers**:
831
+ - Two different fixture files both define a fixture with the same name
832
+ - Importing the extended `test` object from multiple files that each add overlapping fixtures
833
+ - Copy-pasting fixture definitions without renaming
834
+
835
+ **Fix**: Ensure each fixture name is unique across your merged fixture chain. Use a single fixture file that combines all extensions.
836
+
837
+ ```typescript
838
+ // TypeScript
839
+
840
+ // BAD — two separate extensions with the same fixture name
841
+ // fixtures/auth.ts
842
+ export const test = base.extend<{ user: User }>({
843
+ user: async ({}, use) => { /* ... */ },
844
+ });
845
+
846
+ // fixtures/data.ts — CONFLICT: "user" already exists
847
+ export const test = base.extend<{ user: User }>({
848
+ user: async ({}, use) => { /* ... */ },
849
+ });
850
+
851
+ // GOOD — single combined fixture file
852
+ // fixtures/index.ts
853
+ import { test as base } from '@playwright/test';
854
+
855
+ type MyFixtures = {
856
+ authenticatedUser: User;
857
+ testData: TestData;
858
+ };
859
+
860
+ export const test = base.extend<MyFixtures>({
861
+ authenticatedUser: async ({}, use) => {
862
+ const user = await createUser();
863
+ await use(user);
864
+ await deleteUser(user);
865
+ },
866
+ testData: async ({}, use) => {
867
+ const data = await seedData();
868
+ await use(data);
869
+ await cleanupData(data);
870
+ },
871
+ });
872
+
873
+ export { expect } from '@playwright/test';
874
+ ```
875
+
876
+ ```javascript
877
+ // JavaScript
878
+
879
+ // GOOD — single combined fixture file
880
+ // fixtures/index.js
881
+ const { test: base } = require('@playwright/test');
882
+
883
+ const test = base.extend({
884
+ authenticatedUser: async ({}, use) => {
885
+ const user = await createUser();
886
+ await use(user);
887
+ await deleteUser(user);
888
+ },
889
+ testData: async ({}, use) => {
890
+ const data = await seedData();
891
+ await use(data);
892
+ await cleanupData(data);
893
+ },
894
+ });
895
+
896
+ module.exports = { test };
897
+ ```
898
+
899
+ **Related**: [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
900
+
901
+ ---
902
+
903
+ ## Network & API Errors
904
+
905
+ ---
906
+
907
+ ### "page.route: Pattern should start with..."
908
+
909
+ **Cause**: The URL pattern passed to `page.route()` does not match the expected format. Playwright expects a glob pattern (starting with `**/` or `http`), a regex, or a predicate function.
910
+
911
+ **Common triggers**:
912
+ - Passing a bare path like `/api/users` instead of `**/api/users`
913
+ - Missing the `**` glob prefix for relative patterns
914
+ - Passing an object or invalid type instead of a string/regex
915
+
916
+ **Fix**: Use the correct pattern format.
917
+
918
+ ```typescript
919
+ // TypeScript
920
+
921
+ // BAD — bare path without glob prefix
922
+ await page.route('/api/users', route => route.fulfill({ body: '[]' }));
923
+
924
+ // GOOD — glob pattern
925
+ await page.route('**/api/users', route =>
926
+ route.fulfill({
927
+ status: 200,
928
+ contentType: 'application/json',
929
+ body: JSON.stringify([{ id: 1, name: 'Alice' }]),
930
+ })
931
+ );
932
+
933
+ // GOOD — regex pattern
934
+ await page.route(/\/api\/users\/\d+/, route =>
935
+ route.fulfill({
936
+ status: 200,
937
+ contentType: 'application/json',
938
+ body: JSON.stringify({ id: 1, name: 'Alice' }),
939
+ })
940
+ );
941
+
942
+ // GOOD — predicate function for complex matching
943
+ await page.route(
944
+ url => url.pathname.startsWith('/api/') && url.searchParams.has('filter'),
945
+ route => route.fulfill({ body: '[]' })
946
+ );
947
+ ```
948
+
949
+ ```javascript
950
+ // JavaScript
951
+
952
+ // GOOD — glob pattern
953
+ await page.route('**/api/users', route =>
954
+ route.fulfill({
955
+ status: 200,
956
+ contentType: 'application/json',
957
+ body: JSON.stringify([{ id: 1, name: 'Alice' }]),
958
+ })
959
+ );
960
+
961
+ // GOOD — regex pattern
962
+ await page.route(/\/api\/users\/\d+/, route =>
963
+ route.fulfill({
964
+ status: 200,
965
+ contentType: 'application/json',
966
+ body: JSON.stringify({ id: 1, name: 'Alice' }),
967
+ })
968
+ );
969
+
970
+ // GOOD — predicate function
971
+ await page.route(
972
+ url => url.pathname.startsWith('/api/') && url.searchParams.has('filter'),
973
+ route => route.fulfill({ body: '[]' })
974
+ );
975
+ ```
976
+
977
+ **Related**: [core/network-mocking.md](network-mocking.md)
978
+
979
+ ---
980
+
981
+ ### "Error: apiRequestContext.get: connect ECONNREFUSED"
982
+
983
+ **Cause**: The API request made via `request.get()` (or `.post()`, `.put()`, etc.) could not connect to the target server. Same underlying issue as `page.goto: net::ERR_CONNECTION_REFUSED` but for API testing.
984
+
985
+ **Common triggers**:
986
+ - API server not running when using `request` fixture
987
+ - Wrong `baseURL` in the config or in the API request context
988
+ - Running API tests against a service that hasn't started yet
989
+ - In CI, the API service container is not ready
990
+
991
+ **Fix**: Ensure the API server is running. Use `webServer` config to auto-start it, or add a health check.
992
+
993
+ ```typescript
994
+ // TypeScript — playwright.config.ts
995
+ import { defineConfig } from '@playwright/test';
996
+
997
+ export default defineConfig({
998
+ webServer: [
999
+ {
1000
+ command: 'npm run start:api',
1001
+ url: 'http://localhost:4000/health',
1002
+ reuseExistingServer: !process.env.CI,
1003
+ },
1004
+ {
1005
+ command: 'npm run start:web',
1006
+ url: 'http://localhost:3000',
1007
+ reuseExistingServer: !process.env.CI,
1008
+ },
1009
+ ],
1010
+ use: {
1011
+ baseURL: 'http://localhost:3000',
1012
+ },
1013
+ });
1014
+
1015
+ // In tests, use the request fixture with explicit baseURL for API calls
1016
+ test('fetch users', async ({ request }) => {
1017
+ const response = await request.get('http://localhost:4000/api/users');
1018
+ expect(response.ok()).toBeTruthy();
1019
+ });
1020
+ ```
1021
+
1022
+ ```javascript
1023
+ // JavaScript — playwright.config.js
1024
+ const { defineConfig } = require('@playwright/test');
1025
+
1026
+ module.exports = defineConfig({
1027
+ webServer: [
1028
+ {
1029
+ command: 'npm run start:api',
1030
+ url: 'http://localhost:4000/health',
1031
+ reuseExistingServer: !process.env.CI,
1032
+ },
1033
+ {
1034
+ command: 'npm run start:web',
1035
+ url: 'http://localhost:3000',
1036
+ reuseExistingServer: !process.env.CI,
1037
+ },
1038
+ ],
1039
+ use: {
1040
+ baseURL: 'http://localhost:3000',
1041
+ },
1042
+ });
1043
+ ```
1044
+
1045
+ **Related**: [core/api-testing.md](api-testing.md), [core/configuration.md](configuration.md)
1046
+
1047
+ ---
1048
+
1049
+ ### "Request was not handled: GET https://..."
1050
+
1051
+ **Cause**: A route handler was registered with `page.route()` using `{ times: N }` or `page.unrouteAll()` was called, and a subsequent request matching that pattern was not intercepted — Playwright warns you that a request fell through.
1052
+
1053
+ **Common triggers**:
1054
+ - Using `page.route()` with `{ times: 1 }` but the app makes the same request more than once
1055
+ - Calling `route.fallback()` or `route.continue()` without a handler to catch it
1056
+ - Calling `page.unrouteAll({ behavior: 'wait' })` mid-test and the app makes another request
1057
+
1058
+ **Fix**: Ensure your route handler covers all expected requests, or let unhandled requests pass through.
1059
+
1060
+ ```typescript
1061
+ // TypeScript
1062
+
1063
+ // BAD — only handles the first request, second one is unhandled
1064
+ await page.route('**/api/data', route =>
1065
+ route.fulfill({ body: '{}' }),
1066
+ { times: 1 }
1067
+ );
1068
+
1069
+ // GOOD — handle all requests to this URL
1070
+ await page.route('**/api/data', route =>
1071
+ route.fulfill({
1072
+ status: 200,
1073
+ contentType: 'application/json',
1074
+ body: JSON.stringify({ items: [] }),
1075
+ })
1076
+ );
1077
+
1078
+ // GOOD — if you only want to intercept once, use waitForResponse for subsequent
1079
+ await page.route('**/api/data', route =>
1080
+ route.fulfill({ body: '{"items": []}' }),
1081
+ { times: 1 }
1082
+ );
1083
+ // For the second call, let it go to the real server (no route needed)
1084
+ ```
1085
+
1086
+ ```javascript
1087
+ // JavaScript
1088
+
1089
+ // GOOD — handle all requests to this URL
1090
+ await page.route('**/api/data', route =>
1091
+ route.fulfill({
1092
+ status: 200,
1093
+ contentType: 'application/json',
1094
+ body: JSON.stringify({ items: [] }),
1095
+ })
1096
+ );
1097
+ ```
1098
+
1099
+ **Related**: [core/network-mocking.md](network-mocking.md)
1100
+
1101
+ ---
1102
+
1103
+ ## Authentication & State Errors
1104
+
1105
+ ---
1106
+
1107
+ ### "Error: browserContext.storageState: No such file"
1108
+
1109
+ **Cause**: The `storageState` path referenced in the config or test does not point to an existing file. The auth state file has not been generated yet.
1110
+
1111
+ **Common triggers**:
1112
+ - Running tests before running the auth setup project
1113
+ - The `globalSetup` or auth setup project did not complete successfully
1114
+ - The file path in the config does not match the path used in the setup
1115
+ - `.gitignore` excluded the storage state file and it was not regenerated after a fresh clone
1116
+ - CI cache expired and the state file was not recreated
1117
+
1118
+ **Fix**: Ensure the auth setup runs first and the file path is consistent.
1119
+
1120
+ ```typescript
1121
+ // TypeScript — playwright.config.ts
1122
+ import { defineConfig } from '@playwright/test';
1123
+
1124
+ const authFile = 'playwright/.auth/user.json';
1125
+
1126
+ export default defineConfig({
1127
+ projects: [
1128
+ // Setup project runs first and creates the auth state file
1129
+ {
1130
+ name: 'setup',
1131
+ testMatch: /.*\.setup\.ts/,
1132
+ },
1133
+ {
1134
+ name: 'chromium',
1135
+ use: {
1136
+ storageState: authFile,
1137
+ },
1138
+ dependencies: ['setup'], // ensures setup runs first
1139
+ },
1140
+ ],
1141
+ });
1142
+
1143
+ // auth.setup.ts
1144
+ import { test as setup, expect } from '@playwright/test';
1145
+
1146
+ const authFile = 'playwright/.auth/user.json';
1147
+
1148
+ setup('authenticate', async ({ page }) => {
1149
+ await page.goto('/login');
1150
+ await page.getByLabel('Email').fill('user@example.com');
1151
+ await page.getByLabel('Password').fill('password123');
1152
+ await page.getByRole('button', { name: 'Sign in' }).click();
1153
+ await page.waitForURL('/dashboard');
1154
+ // Save the authenticated state
1155
+ await page.context().storageState({ path: authFile });
1156
+ });
1157
+ ```
1158
+
1159
+ ```javascript
1160
+ // JavaScript — playwright.config.js
1161
+ const { defineConfig } = require('@playwright/test');
1162
+
1163
+ const authFile = 'playwright/.auth/user.json';
1164
+
1165
+ module.exports = defineConfig({
1166
+ projects: [
1167
+ {
1168
+ name: 'setup',
1169
+ testMatch: /.*\.setup\.js/,
1170
+ },
1171
+ {
1172
+ name: 'chromium',
1173
+ use: {
1174
+ storageState: authFile,
1175
+ },
1176
+ dependencies: ['setup'],
1177
+ },
1178
+ ],
1179
+ });
1180
+
1181
+ // auth.setup.js
1182
+ const { test: setup, expect } = require('@playwright/test');
1183
+
1184
+ const authFile = 'playwright/.auth/user.json';
1185
+
1186
+ setup('authenticate', async ({ page }) => {
1187
+ await page.goto('/login');
1188
+ await page.getByLabel('Email').fill('user@example.com');
1189
+ await page.getByLabel('Password').fill('password123');
1190
+ await page.getByRole('button', { name: 'Sign in' }).click();
1191
+ await page.waitForURL('/dashboard');
1192
+ await page.context().storageState({ path: authFile });
1193
+ });
1194
+ ```
1195
+
1196
+ Ensure the auth directory exists and is in `.gitignore`:
1197
+
1198
+ ```bash
1199
+ mkdir -p playwright/.auth
1200
+ echo "playwright/.auth/" >> .gitignore
1201
+ ```
1202
+
1203
+ **Related**: [core/authentication.md](authentication.md), [core/auth-flows.md](auth-flows.md)
1204
+
1205
+ ---
1206
+
1207
+ ### "Error: Target page, context or browser has been closed"
1208
+
1209
+ **Cause**: Code attempted to use a `page`, `context`, or `browser` object that has already been closed or disposed.
1210
+
1211
+ **Common triggers**:
1212
+ - Using a `page` reference after `page.close()` was called
1213
+ - Storing a `page` or `context` in a module-level variable and reusing it across tests
1214
+ - A fixture tore down the browser context while an async callback was still running
1215
+ - A `beforeAll` created a context that was closed before all tests finished using it
1216
+ - Using `browser.newContext()` manually and forgetting to manage its lifecycle
1217
+
1218
+ **Fix**: Never store page/context references in shared mutable state. Use Playwright fixtures for lifecycle management.
1219
+
1220
+ ```typescript
1221
+ // TypeScript
1222
+
1223
+ // BAD — shared module-level variable
1224
+ let sharedPage: Page;
1225
+ test.beforeAll(async ({ browser }) => {
1226
+ sharedPage = await browser.newPage();
1227
+ });
1228
+ test.afterAll(async () => {
1229
+ await sharedPage.close();
1230
+ });
1231
+ test('first', async () => {
1232
+ await sharedPage.goto('/'); // might fail if afterAll from another describe ran first
1233
+ });
1234
+
1235
+ // GOOD — use the built-in page fixture (one per test, auto-managed)
1236
+ test('first', async ({ page }) => {
1237
+ await page.goto('/'); // always a fresh, open page
1238
+ });
1239
+
1240
+ // GOOD — for shared state across tests, use a worker-scoped fixture
1241
+ import { test as base } from '@playwright/test';
1242
+
1243
+ export const test = base.extend<{}, { adminContext: BrowserContext }>({
1244
+ adminContext: [async ({ browser }, use) => {
1245
+ const context = await browser.newContext();
1246
+ await use(context);
1247
+ await context.close();
1248
+ }, { scope: 'worker' }],
1249
+ });
1250
+ ```
1251
+
1252
+ ```javascript
1253
+ // JavaScript
1254
+
1255
+ // GOOD — use the built-in page fixture
1256
+ test('first', async ({ page }) => {
1257
+ await page.goto('/');
1258
+ });
1259
+
1260
+ // GOOD — for shared state, use a worker-scoped fixture
1261
+ const { test: base } = require('@playwright/test');
1262
+
1263
+ const test = base.extend({
1264
+ adminContext: [async ({ browser }, use) => {
1265
+ const context = await browser.newContext();
1266
+ await use(context);
1267
+ await context.close();
1268
+ }, { scope: 'worker' }],
1269
+ });
1270
+ ```
1271
+
1272
+ **Related**: [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
1273
+
1274
+ ---
1275
+
1276
+ ## CI-Specific Errors
1277
+
1278
+ ---
1279
+
1280
+ ### "Error: browserType.launch: Browser closed unexpectedly"
1281
+
1282
+ **Cause**: The browser process crashed immediately after launch, usually because required OS-level dependencies are missing in the CI environment.
1283
+
1284
+ **Common triggers**:
1285
+ - Running on a minimal Docker image (e.g., `node:alpine`) without browser dependencies
1286
+ - Missing shared libraries (`libgbm`, `libnss3`, `libatk-bridge`, etc.) on Linux CI
1287
+ - Insufficient shared memory (`/dev/shm` too small) — browsers use shared memory for rendering
1288
+ - Running as root in Docker without `--no-sandbox` (Chromium refuses to run as root with sandbox)
1289
+ - Out-of-memory kill on CI (browser process is OOM-killed by the OS)
1290
+
1291
+ **Fix**: Install OS-level dependencies and configure the environment.
1292
+
1293
+ ```bash
1294
+ # Install browsers with their OS dependencies (recommended)
1295
+ npx playwright install --with-deps chromium
1296
+
1297
+ # If using Docker, use the official Playwright image
1298
+ # Dockerfile
1299
+ FROM mcr.microsoft.com/playwright:v1.50.0-noble
1300
+ WORKDIR /app
1301
+ COPY . .
1302
+ RUN npm ci
1303
+ RUN npx playwright install chromium
1304
+ CMD ["npx", "playwright", "test"]
1305
+ ```
1306
+
1307
+ For shared memory issues:
1308
+
1309
+ ```yaml
1310
+ # GitHub Actions — increase /dev/shm
1311
+ jobs:
1312
+ test:
1313
+ runs-on: ubuntu-latest
1314
+ container:
1315
+ image: mcr.microsoft.com/playwright:v1.50.0-noble
1316
+ options: --shm-size=2gb
1317
+
1318
+ # Docker run — increase shared memory
1319
+ # docker run --shm-size=2gb my-test-image
1320
+ ```
1321
+
1322
+ For running as root in Docker:
1323
+
1324
+ ```typescript
1325
+ // TypeScript — playwright.config.ts (only for Docker/CI)
1326
+ export default defineConfig({
1327
+ use: {
1328
+ launchOptions: {
1329
+ args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
1330
+ },
1331
+ },
1332
+ });
1333
+ ```
1334
+
1335
+ ```javascript
1336
+ // JavaScript — playwright.config.js
1337
+ module.exports = defineConfig({
1338
+ use: {
1339
+ launchOptions: {
1340
+ args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
1341
+ },
1342
+ },
1343
+ });
1344
+ ```
1345
+
1346
+ **Related**: [ci/ci-github-actions.md](../ci/ci-github-actions.md), [ci/docker-and-containers.md](../ci/docker-and-containers.md)
1347
+
1348
+ ---
1349
+
1350
+ ### "Error: Playwright Test needs to be invoked via 'npx playwright test'"
1351
+
1352
+ **Cause**: The test file was executed directly with `node` or `ts-node` instead of through the Playwright test runner.
1353
+
1354
+ **Common triggers**:
1355
+ - Running `node tests/example.spec.ts` or `ts-node tests/example.spec.ts`
1356
+ - A misconfigured IDE run configuration that invokes `node` directly
1357
+ - Using Jest or Mocha to run Playwright test files that import from `@playwright/test`
1358
+ - A CI script that calls the wrong command
1359
+
1360
+ **Fix**: Always use the Playwright CLI to run tests.
1361
+
1362
+ ```bash
1363
+ # Correct invocations
1364
+ npx playwright test # run all tests
1365
+ npx playwright test tests/login.spec.ts # run specific file
1366
+ npx playwright test --grep "login" # run tests matching pattern
1367
+ npx playwright test --project=chromium # run specific project
1368
+ npx playwright test --debug # run with inspector
1369
+
1370
+ # If using a package.json script
1371
+ # package.json
1372
+ # "scripts": { "test:e2e": "playwright test" }
1373
+ npm run test:e2e
1374
+ ```
1375
+
1376
+ For IDE configuration (VS Code):
1377
+
1378
+ ```json
1379
+ // .vscode/settings.json — install the Playwright VS Code extension instead of custom configs
1380
+ {
1381
+ "playwright.reuseBrowser": true
1382
+ }
1383
+ ```
1384
+
1385
+ **Related**: [core/configuration.md](configuration.md)
1386
+
1387
+ ---
1388
+
1389
+ ## Additional Common Errors
1390
+
1391
+ ---
1392
+
1393
+ ### "Error: page.evaluate: Execution context was destroyed"
1394
+
1395
+ **Cause**: The page navigated or reloaded while `page.evaluate()` was executing JavaScript in the browser context.
1396
+
1397
+ **Common triggers**:
1398
+ - Calling `evaluate()` while the page is in the middle of a navigation
1399
+ - A timer or event on the page triggers a redirect during evaluation
1400
+ - Running `evaluate()` in a `beforeEach` that races with `page.goto()`
1401
+
1402
+ **Fix**: Ensure the page is stable before evaluating.
1403
+
1404
+ ```typescript
1405
+ // TypeScript
1406
+
1407
+ // BAD — evaluate races with navigation
1408
+ await page.goto('/dashboard');
1409
+ const title = await page.evaluate(() => document.title); // context might be destroyed
1410
+
1411
+ // GOOD — wait for load state first
1412
+ await page.goto('/dashboard');
1413
+ await page.waitForLoadState('domcontentloaded');
1414
+ const title = await page.evaluate(() => document.title);
1415
+
1416
+ // GOOD — prefer locator-based assertions (no evaluate needed)
1417
+ await page.goto('/dashboard');
1418
+ await expect(page).toHaveTitle('Dashboard');
1419
+ ```
1420
+
1421
+ ```javascript
1422
+ // JavaScript
1423
+
1424
+ // GOOD — wait for load state first
1425
+ await page.goto('/dashboard');
1426
+ await page.waitForLoadState('domcontentloaded');
1427
+ const title = await page.evaluate(() => document.title);
1428
+
1429
+ // GOOD — prefer locator-based assertions
1430
+ await page.goto('/dashboard');
1431
+ await expect(page).toHaveTitle('Dashboard');
1432
+ ```
1433
+
1434
+ **Related**: [core/assertions-and-waiting.md](assertions-and-waiting.md)
1435
+
1436
+ ---
1437
+
1438
+ ### "Error: page.screenshot: Cannot take a screenshot larger than..."
1439
+
1440
+ **Cause**: The requested screenshot dimensions exceed Playwright's limit (typically 16384x16384 pixels). This happens with full-page screenshots of very long pages.
1441
+
1442
+ **Common triggers**:
1443
+ - `page.screenshot({ fullPage: true })` on an infinitely-scrolling page
1444
+ - Pages with extremely tall content (long data tables, chat logs)
1445
+ - CSS bugs that create enormous element heights
1446
+
1447
+ **Fix**: Clip the screenshot to a specific region or limit the viewport.
1448
+
1449
+ ```typescript
1450
+ // TypeScript
1451
+
1452
+ // BAD — might exceed size limits on tall pages
1453
+ await page.screenshot({ fullPage: true, path: 'full.png' });
1454
+
1455
+ // GOOD — clip to a specific region
1456
+ await page.screenshot({
1457
+ path: 'clipped.png',
1458
+ clip: { x: 0, y: 0, width: 1280, height: 2000 },
1459
+ });
1460
+
1461
+ // GOOD — screenshot a specific element instead
1462
+ await page.getByRole('main').screenshot({ path: 'main-content.png' });
1463
+
1464
+ // GOOD — set a reasonable viewport before full-page screenshot
1465
+ await page.setViewportSize({ width: 1280, height: 720 });
1466
+ await page.screenshot({ fullPage: true, path: 'full.png' });
1467
+ ```
1468
+
1469
+ ```javascript
1470
+ // JavaScript
1471
+
1472
+ // GOOD — clip to a specific region
1473
+ await page.screenshot({
1474
+ path: 'clipped.png',
1475
+ clip: { x: 0, y: 0, width: 1280, height: 2000 },
1476
+ });
1477
+
1478
+ // GOOD — screenshot a specific element
1479
+ await page.getByRole('main').screenshot({ path: 'main-content.png' });
1480
+ ```
1481
+
1482
+ **Related**: [core/visual-regression.md](visual-regression.md)
1483
+
1484
+ ---
1485
+
1486
+ ### "Error: waiting for locator('...').toBeAttached()"
1487
+
1488
+ **Cause**: The element never entered the DOM within the assertion timeout. Unlike `toBeVisible()`, `toBeAttached()` only checks DOM presence — but the element was never rendered at all.
1489
+
1490
+ **Common triggers**:
1491
+ - The component is conditionally rendered and the condition is not met
1492
+ - A network request that populates the data has not completed
1493
+ - Wrong selector that does not match any element
1494
+ - The element is rendered by a lazy-loaded chunk that has not downloaded yet
1495
+
1496
+ **Fix**: Verify the precondition for the element to render.
1497
+
1498
+ ```typescript
1499
+ // TypeScript
1500
+
1501
+ // Ensure the data loads before asserting
1502
+ await page.goto('/users');
1503
+ await page.waitForResponse(resp => resp.url().includes('/api/users'));
1504
+ await expect(page.getByRole('table')).toBeAttached();
1505
+
1506
+ // For lazy-loaded content, wait longer or trigger the load
1507
+ await page.getByRole('tab', { name: 'Advanced' }).click();
1508
+ await expect(page.getByTestId('advanced-panel')).toBeAttached({ timeout: 10_000 });
1509
+ ```
1510
+
1511
+ ```javascript
1512
+ // JavaScript
1513
+
1514
+ await page.goto('/users');
1515
+ await page.waitForResponse(resp => resp.url().includes('/api/users'));
1516
+ await expect(page.getByRole('table')).toBeAttached();
1517
+
1518
+ await page.getByRole('tab', { name: 'Advanced' }).click();
1519
+ await expect(page.getByTestId('advanced-panel')).toBeAttached({ timeout: 10_000 });
1520
+ ```
1521
+
1522
+ **Related**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/locators.md](locators.md)
1523
+
1524
+ ---
1525
+
1526
+ ### "Error: protocol error: Target.createTarget: Failed to create target"
1527
+
1528
+ **Cause**: The browser could not create a new tab or context, usually due to resource exhaustion.
1529
+
1530
+ **Common triggers**:
1531
+ - Opening too many pages or contexts without closing them
1532
+ - Memory leak in tests — each test opens a context but never closes it
1533
+ - Running many parallel workers on a resource-constrained CI machine
1534
+ - Browser process is unstable after a previous crash
1535
+
1536
+ **Fix**: Reduce parallelism, close unused contexts, or use fewer workers.
1537
+
1538
+ ```typescript
1539
+ // TypeScript — playwright.config.ts
1540
+
1541
+ export default defineConfig({
1542
+ // Reduce parallelism if resources are limited
1543
+ workers: process.env.CI ? 2 : undefined,
1544
+
1545
+ // Limit to one browser context per worker
1546
+ fullyParallel: false, // run test files serially within a worker
1547
+ });
1548
+ ```
1549
+
1550
+ ```javascript
1551
+ // JavaScript — playwright.config.js
1552
+
1553
+ module.exports = defineConfig({
1554
+ workers: process.env.CI ? 2 : undefined,
1555
+ fullyParallel: false,
1556
+ });
1557
+ ```
1558
+
1559
+ If you manually create contexts, always close them:
1560
+
1561
+ ```typescript
1562
+ // TypeScript
1563
+ test('multi-user test', async ({ browser }) => {
1564
+ const userContext = await browser.newContext();
1565
+ const adminContext = await browser.newContext();
1566
+ try {
1567
+ const userPage = await userContext.newPage();
1568
+ const adminPage = await adminContext.newPage();
1569
+ // ... test logic
1570
+ } finally {
1571
+ await userContext.close();
1572
+ await adminContext.close();
1573
+ }
1574
+ });
1575
+ ```
1576
+
1577
+ ```javascript
1578
+ // JavaScript
1579
+ test('multi-user test', async ({ browser }) => {
1580
+ const userContext = await browser.newContext();
1581
+ const adminContext = await browser.newContext();
1582
+ try {
1583
+ const userPage = await userContext.newPage();
1584
+ const adminPage = await adminContext.newPage();
1585
+ // ... test logic
1586
+ } finally {
1587
+ await userContext.close();
1588
+ await adminContext.close();
1589
+ }
1590
+ });
1591
+ ```
1592
+
1593
+ **Related**: [ci/parallel-and-sharding.md](../ci/parallel-and-sharding.md), [core/multi-user-and-collaboration.md](multi-user-and-collaboration.md)
1594
+
1595
+ ---
1596
+
1597
+ ### "Error: expect(locator).toHaveText() — expected string but received array"
1598
+
1599
+ **Cause**: The locator matched multiple elements and `toHaveText()` returned an array of strings instead of a single string.
1600
+
1601
+ **Common triggers**:
1602
+ - Using a broad locator like `page.locator('.item')` that matches a list
1603
+ - Expecting a single heading but the selector matches multiple
1604
+ - Not scoping the locator to a specific container
1605
+
1606
+ **Fix**: Either narrow the locator to one element or use the array form of `toHaveText()`.
1607
+
1608
+ ```typescript
1609
+ // TypeScript
1610
+
1611
+ // BAD — locator matches multiple elements
1612
+ await expect(page.locator('.item')).toHaveText('First Item');
1613
+
1614
+ // GOOD — narrow to one element
1615
+ await expect(page.locator('.item').first()).toHaveText('First Item');
1616
+
1617
+ // GOOD — assert against an array when multiple elements are expected
1618
+ await expect(page.locator('.item')).toHaveText([
1619
+ 'First Item',
1620
+ 'Second Item',
1621
+ 'Third Item',
1622
+ ]);
1623
+
1624
+ // GOOD — use a more specific locator
1625
+ await expect(page.getByRole('heading', { level: 1 })).toHaveText('Welcome');
1626
+ ```
1627
+
1628
+ ```javascript
1629
+ // JavaScript
1630
+
1631
+ // GOOD — narrow to one element
1632
+ await expect(page.locator('.item').first()).toHaveText('First Item');
1633
+
1634
+ // GOOD — assert against an array
1635
+ await expect(page.locator('.item')).toHaveText([
1636
+ 'First Item',
1637
+ 'Second Item',
1638
+ 'Third Item',
1639
+ ]);
1640
+
1641
+ // GOOD — use a more specific locator
1642
+ await expect(page.getByRole('heading', { level: 1 })).toHaveText('Welcome');
1643
+ ```
1644
+
1645
+ **Related**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/locators.md](locators.md)
1646
+
1647
+ ---
1648
+
1649
+ ### "Error: page.waitForSelector: Timeout 30000ms exceeded (deprecated)"
1650
+
1651
+ **Cause**: `page.waitForSelector()` timed out. This method is deprecated in favor of locator-based assertions.
1652
+
1653
+ **Common triggers**:
1654
+ - Using legacy `page.waitForSelector()` from older tutorials or migration from Puppeteer
1655
+ - The selector does not match any element in the DOM
1656
+ - The element exists but does not meet the state requirement (e.g., `{ state: 'visible' }` but element is hidden)
1657
+
1658
+ **Fix**: Replace `waitForSelector()` with locator-based assertions.
1659
+
1660
+ ```typescript
1661
+ // TypeScript
1662
+
1663
+ // BAD — deprecated, no auto-retry, not composable
1664
+ await page.waitForSelector('.loading', { state: 'hidden' });
1665
+ await page.waitForSelector('.content', { state: 'visible' });
1666
+
1667
+ // GOOD — locator-based assertions with auto-retry
1668
+ await expect(page.locator('.loading')).toBeHidden();
1669
+ await expect(page.locator('.content')).toBeVisible();
1670
+
1671
+ // GOOD — use role-based locators for even more resilience
1672
+ await expect(page.getByRole('progressbar')).toBeHidden();
1673
+ await expect(page.getByRole('main')).toBeVisible();
1674
+ ```
1675
+
1676
+ ```javascript
1677
+ // JavaScript
1678
+
1679
+ // BAD — deprecated
1680
+ await page.waitForSelector('.loading', { state: 'hidden' });
1681
+ await page.waitForSelector('.content', { state: 'visible' });
1682
+
1683
+ // GOOD — locator-based assertions
1684
+ await expect(page.locator('.loading')).toBeHidden();
1685
+ await expect(page.locator('.content')).toBeVisible();
1686
+
1687
+ // GOOD — role-based locators
1688
+ await expect(page.getByRole('progressbar')).toBeHidden();
1689
+ await expect(page.getByRole('main')).toBeVisible();
1690
+ ```
1691
+
1692
+ **Related**: [core/locators.md](locators.md), [core/assertions-and-waiting.md](assertions-and-waiting.md), [migration/from-selenium.md](../migration/from-selenium.md)
1693
+
1694
+ ---
1695
+
1696
+ ### "Error: Test was expected to have a title matching /.../"
1697
+
1698
+ **Cause**: The `expect(page).toHaveTitle()` assertion failed because the page title did not match the expected value or pattern within the timeout.
1699
+
1700
+ **Common triggers**:
1701
+ - The page has not finished loading and the title is still the initial empty or default value
1702
+ - The SPA updates the document title asynchronously after rendering
1703
+ - The expected title has a typo or does not match case-sensitively
1704
+ - The page redirected to an error page with a different title
1705
+
1706
+ **Fix**: Ensure the page is fully loaded and use the correct expected value.
1707
+
1708
+ ```typescript
1709
+ // TypeScript
1710
+
1711
+ // toHaveTitle auto-retries, but ensure the page has loaded
1712
+ await page.goto('/dashboard');
1713
+ await expect(page).toHaveTitle('Dashboard | MyApp');
1714
+
1715
+ // Use regex for flexible matching
1716
+ await expect(page).toHaveTitle(/Dashboard/);
1717
+
1718
+ // Debug: check what the actual title is
1719
+ console.log(await page.title());
1720
+ ```
1721
+
1722
+ ```javascript
1723
+ // JavaScript
1724
+
1725
+ await page.goto('/dashboard');
1726
+ await expect(page).toHaveTitle('Dashboard | MyApp');
1727
+
1728
+ // Use regex for flexible matching
1729
+ await expect(page).toHaveTitle(/Dashboard/);
1730
+
1731
+ // Debug: check what the actual title is
1732
+ console.log(await page.title());
1733
+ ```
1734
+
1735
+ **Related**: [core/assertions-and-waiting.md](assertions-and-waiting.md)
1736
+
1737
+ ---
1738
+
1739
+ ### "Error: page.type: Element is not focusable"
1740
+
1741
+ **Cause**: `page.type()` or `locator.type()` could not focus the target element before typing. The element may be hidden, disabled, or not an input.
1742
+
1743
+ **Common triggers**:
1744
+ - The element is covered by an overlay or modal
1745
+ - The element has `tabindex="-1"` and is not natively focusable
1746
+ - Using `type()` instead of `fill()` — `fill()` is preferred in almost all cases
1747
+ - The element is inside a Shadow DOM and the locator does not pierce it
1748
+
1749
+ **Fix**: Use `fill()` instead of `type()`. Only use `type()` when you specifically need to simulate individual keystrokes.
1750
+
1751
+ ```typescript
1752
+ // TypeScript
1753
+
1754
+ // BAD — type() is slower and more fragile
1755
+ await page.locator('#search').type('hello');
1756
+
1757
+ // GOOD — fill() clears and sets the value directly
1758
+ await page.getByRole('searchbox').fill('hello');
1759
+
1760
+ // When you truly need keystroke-by-keystroke input (e.g., autocomplete trigger)
1761
+ await page.getByRole('searchbox').pressSequentially('hello', { delay: 100 });
1762
+ ```
1763
+
1764
+ ```javascript
1765
+ // JavaScript
1766
+
1767
+ // GOOD — fill() is preferred
1768
+ await page.getByRole('searchbox').fill('hello');
1769
+
1770
+ // Keystroke-by-keystroke when needed (e.g., autocomplete)
1771
+ await page.getByRole('searchbox').pressSequentially('hello', { delay: 100 });
1772
+ ```
1773
+
1774
+ **Related**: [core/locators.md](locators.md), [core/forms-and-validation.md](forms-and-validation.md)
1775
+
1776
+ ---
1777
+
1778
+ ### "Error: page.setInputFiles: Non-multiple file input can only accept single file"
1779
+
1780
+ **Cause**: Attempted to upload multiple files to an `<input type="file">` that does not have the `multiple` attribute.
1781
+
1782
+ **Common triggers**:
1783
+ - Passing an array of files to a single-file upload input
1784
+ - The app uses a custom upload component that only handles one file at a time
1785
+
1786
+ **Fix**: Upload one file at a time, or ensure the input supports multiple files.
1787
+
1788
+ ```typescript
1789
+ // TypeScript
1790
+
1791
+ // BAD — input does not have multiple attribute
1792
+ await page.getByLabel('Upload file').setInputFiles([
1793
+ 'file1.pdf',
1794
+ 'file2.pdf', // error: non-multiple input
1795
+ ]);
1796
+
1797
+ // GOOD — single file
1798
+ await page.getByLabel('Upload file').setInputFiles('file1.pdf');
1799
+
1800
+ // GOOD — for multiple file input (must have multiple attribute in HTML)
1801
+ await page.getByLabel('Upload files').setInputFiles([
1802
+ 'file1.pdf',
1803
+ 'file2.pdf',
1804
+ ]);
1805
+
1806
+ // To clear the file input
1807
+ await page.getByLabel('Upload file').setInputFiles([]);
1808
+ ```
1809
+
1810
+ ```javascript
1811
+ // JavaScript
1812
+
1813
+ // GOOD — single file
1814
+ await page.getByLabel('Upload file').setInputFiles('file1.pdf');
1815
+
1816
+ // GOOD — multiple files (input must have multiple attribute)
1817
+ await page.getByLabel('Upload files').setInputFiles([
1818
+ 'file1.pdf',
1819
+ 'file2.pdf',
1820
+ ]);
1821
+
1822
+ // To clear the file input
1823
+ await page.getByLabel('Upload file').setInputFiles([]);
1824
+ ```
1825
+
1826
+ **Related**: [core/file-operations.md](file-operations.md), [core/file-upload-download.md](file-upload-download.md)
1827
+
1828
+ ---
1829
+
1830
+ ### "Error: browser.newContext: Could not parse content-type application/json"
1831
+
1832
+ **Cause**: The `storageState` option was given a path to a file with invalid JSON content, or the file is not a valid Playwright storage state format.
1833
+
1834
+ **Common triggers**:
1835
+ - The auth setup wrote an empty file or incomplete JSON
1836
+ - The storage state file was corrupted or truncated
1837
+ - Manually editing the storage state file and introducing a syntax error
1838
+ - The auth setup failed silently (login did not complete) and saved an invalid state
1839
+
1840
+ **Fix**: Delete the storage state file and regenerate it.
1841
+
1842
+ ```bash
1843
+ # Delete the corrupted file
1844
+ rm -f playwright/.auth/user.json
1845
+
1846
+ # Re-run the setup
1847
+ npx playwright test --project=setup
1848
+ ```
1849
+
1850
+ To prevent this, add error checking in the auth setup:
1851
+
1852
+ ```typescript
1853
+ // TypeScript — auth.setup.ts
1854
+ import { test as setup, expect } from '@playwright/test';
1855
+
1856
+ const authFile = 'playwright/.auth/user.json';
1857
+
1858
+ setup('authenticate', async ({ page }) => {
1859
+ await page.goto('/login');
1860
+ await page.getByLabel('Email').fill('user@example.com');
1861
+ await page.getByLabel('Password').fill('password123');
1862
+ await page.getByRole('button', { name: 'Sign in' }).click();
1863
+
1864
+ // Verify login actually succeeded before saving state
1865
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
1866
+
1867
+ await page.context().storageState({ path: authFile });
1868
+ });
1869
+ ```
1870
+
1871
+ ```javascript
1872
+ // JavaScript — auth.setup.js
1873
+ const { test: setup, expect } = require('@playwright/test');
1874
+
1875
+ const authFile = 'playwright/.auth/user.json';
1876
+
1877
+ setup('authenticate', async ({ page }) => {
1878
+ await page.goto('/login');
1879
+ await page.getByLabel('Email').fill('user@example.com');
1880
+ await page.getByLabel('Password').fill('password123');
1881
+ await page.getByRole('button', { name: 'Sign in' }).click();
1882
+
1883
+ // Verify login actually succeeded before saving state
1884
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
1885
+
1886
+ await page.context().storageState({ path: authFile });
1887
+ });
1888
+ ```
1889
+
1890
+ **Related**: [core/authentication.md](authentication.md), [core/auth-flows.md](auth-flows.md)
1891
+
1892
+ ---
1893
+
1894
+ ## Quick Diagnostic Checklist
1895
+
1896
+ When you hit an error not listed above, run through this checklist:
1897
+
1898
+ 1. **Read the full error** — Playwright errors include the locator, action, and a snippet of the page state. The answer is often in the details.
1899
+ 2. **Enable traces** — Add `trace: 'on'` temporarily or `trace: 'on-first-retry'` in config. Run `npx playwright show-trace trace.zip` to see screenshots, DOM, network, and console at the point of failure.
1900
+ 3. **Use the Playwright Inspector** — Run `npx playwright test --debug` to step through the test interactively.
1901
+ 4. **Check the locator** — Run `npx playwright codegen <url>` to verify locators against the live page.
1902
+ 5. **Check the Playwright version** — Run `npx playwright --version`. Ensure `@playwright/test` and browser binaries are the same version.
1903
+ 6. **Search the Playwright issue tracker** — Many errors have known solutions in [github.com/microsoft/playwright/issues](https://github.com/microsoft/playwright/issues).