@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.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- 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).
|