playwriter 0.0.80 → 0.0.89
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/dist/a11y-client.js +18 -8
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +3 -1
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/bippy.js +1 -1
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +84 -0
- package/dist/cdp-relay.js.map +1 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +8 -6
- package/dist/executor.js.map +1 -1
- package/dist/ffmpeg.d.ts +6 -6
- package/dist/ffmpeg.d.ts.map +1 -1
- package/dist/ffmpeg.js +6 -6
- package/dist/ffmpeg.js.map +1 -1
- package/dist/ghost-cursor-client.js +15 -9
- package/dist/prompt.md +71 -337
- package/dist/readability.js +16 -2
- package/dist/recording-ghost-cursor.d.ts.map +1 -1
- package/dist/recording-ghost-cursor.js +1 -1
- package/dist/recording-ghost-cursor.js.map +1 -1
- package/dist/relay-client.js +1 -1
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +344 -16
- package/dist/relay-core.test.js.map +1 -1
- package/dist/relay-navigation.test.d.ts.map +1 -1
- package/dist/relay-navigation.test.js +115 -0
- package/dist/relay-navigation.test.js.map +1 -1
- package/dist/screen-recording.d.ts +24 -0
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +62 -0
- package/dist/screen-recording.js.map +1 -1
- package/dist/screen-recording.test.d.ts +2 -0
- package/dist/screen-recording.test.d.ts.map +1 -0
- package/dist/screen-recording.test.js +102 -0
- package/dist/screen-recording.test.js.map +1 -0
- package/dist/selector-generator.js +1 -1
- package/package.json +2 -2
- package/src/aria-snapshot.ts +3 -1
- package/src/aria-snapshots/github-interactive.txt +2 -0
- package/src/aria-snapshots/github-raw.txt +4 -0
- package/src/aria-snapshots/hackernews-interactive.txt +238 -241
- package/src/aria-snapshots/hackernews-raw.txt +267 -271
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/cdp-relay.ts +110 -0
- package/src/executor.ts +8 -6
- package/src/ffmpeg.ts +8 -8
- package/src/ghost-cursor-client.ts +3 -2
- package/src/recording-ghost-cursor.ts +7 -1
- package/src/relay-client.ts +1 -1
- package/src/relay-core.test.ts +378 -17
- package/src/relay-navigation.test.ts +132 -0
- package/src/screen-recording.test.ts +111 -0
- package/src/screen-recording.ts +81 -0
- package/src/skill.md +71 -339
- package/src/snapshots/shadcn-ui-accessibility-full.md +182 -180
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +120 -118
package/src/skill.md
CHANGED
|
@@ -52,8 +52,6 @@ playwriter -s <sessionId> -e "<code>"
|
|
|
52
52
|
|
|
53
53
|
The `-s` flag specifies a session ID (required). Get one with `playwriter session new`. Use the same session to persist state across commands.
|
|
54
54
|
|
|
55
|
-
Default timeout is 10 seconds. you can increase the timeout with `--timeout <ms>`
|
|
56
|
-
|
|
57
55
|
**Examples:**
|
|
58
56
|
|
|
59
57
|
```bash
|
|
@@ -187,97 +185,47 @@ You can collaborate with the user - they can help with captchas, difficult eleme
|
|
|
187
185
|
|
|
188
186
|
## interaction feedback loop
|
|
189
187
|
|
|
190
|
-
Every browser interaction
|
|
191
|
-
|
|
192
|
-
**Core loop:**
|
|
188
|
+
Every browser interaction must follow **observe → act → observe**. Never chain multiple actions blindly.
|
|
193
189
|
|
|
194
|
-
1. **Open page** — get or create your page
|
|
195
|
-
2. **Observe** — print `state.page.url()`
|
|
196
|
-
3. **Check** —
|
|
190
|
+
1. **Open page** — get or create your page, navigate to URL
|
|
191
|
+
2. **Observe** — print `state.page.url()` + `snapshot()`. Always print URL — pages can redirect unexpectedly.
|
|
192
|
+
3. **Check** — if page isn't ready (loading, wrong URL, content missing), wait and observe again
|
|
197
193
|
4. **Act** — perform one action (click, type, submit)
|
|
198
|
-
5. **Observe again** — print URL + snapshot to verify the action's effect
|
|
199
|
-
6. **Repeat**
|
|
200
|
-
|
|
201
|
-
```
|
|
202
|
-
┌─────────────────────────────────────────────┐
|
|
203
|
-
│ open page + goto URL │
|
|
204
|
-
└──────────────────┬──────────────────────────┘
|
|
205
|
-
▼
|
|
206
|
-
┌────────────────┐
|
|
207
|
-
┌───►│ observe │◄─────────────────┐
|
|
208
|
-
│ │ (url + snapshot) │ │
|
|
209
|
-
│ └───────┬────────┘ │
|
|
210
|
-
│ ▼ │
|
|
211
|
-
│ ┌────────────────┐ │
|
|
212
|
-
│ │ check │ │
|
|
213
|
-
│ │ (read result) │ │
|
|
214
|
-
│ └───┬────────┬───┘ │
|
|
215
|
-
│ not │ │ ready │
|
|
216
|
-
│ ready │ ▼ │
|
|
217
|
-
└────────┘ ┌────────────────┐ │
|
|
218
|
-
│ act │ │
|
|
219
|
-
│ (click/type) │─────────────┘
|
|
220
|
-
└────────────────┘
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
**Example: opening a Framer plugin via the command palette**
|
|
224
|
-
|
|
225
|
-
Each step is a separate execute call. Notice how every action is followed by a snapshot to verify what happened:
|
|
194
|
+
5. **Observe again** — print URL + snapshot to verify the action's effect
|
|
195
|
+
6. **Repeat** from step 3 until task is complete
|
|
226
196
|
|
|
227
197
|
```js
|
|
228
|
-
//
|
|
198
|
+
// Each step should be a separate execute call:
|
|
199
|
+
// Step 1: navigate + observe
|
|
229
200
|
state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
|
|
230
|
-
await state.page.goto('https://
|
|
201
|
+
await state.page.goto('https://example.com', { waitUntil: 'domcontentloaded' })
|
|
231
202
|
console.log('URL:', state.page.url())
|
|
232
203
|
await snapshot({ page: state.page }).then(console.log)
|
|
233
204
|
```
|
|
234
205
|
|
|
235
206
|
```js
|
|
236
|
-
// 2
|
|
237
|
-
await state.page.
|
|
207
|
+
// Step 2: act + observe
|
|
208
|
+
await state.page.locator('button:has-text("Submit")').click()
|
|
238
209
|
console.log('URL:', state.page.url())
|
|
239
|
-
await snapshot({ page: state.page
|
|
240
|
-
// If dialog didn't appear, observe again before retrying
|
|
210
|
+
await snapshot({ page: state.page }).then(console.log)
|
|
241
211
|
```
|
|
242
212
|
|
|
213
|
+
If nothing changed after an action, try `waitForPageLoad({ page: state.page, timeout: 3000 })` or you may have clicked the wrong element.
|
|
214
|
+
|
|
215
|
+
**Deeper observation** — when snapshots aren't enough to understand what happened, combine multiple channels:
|
|
216
|
+
|
|
243
217
|
```js
|
|
244
|
-
//
|
|
245
|
-
await state.page
|
|
246
|
-
|
|
247
|
-
|
|
218
|
+
// Check console for errors after an action
|
|
219
|
+
const errors = await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
|
|
220
|
+
|
|
221
|
+
// Combine snapshot + logs for full picture
|
|
222
|
+
const snap = await snapshot({ page: state.page, search: /dialog|error|message/ })
|
|
223
|
+
const logs = await getLatestLogs({ page: state.page, search: /error/i, count: 10 })
|
|
224
|
+
console.log('UI:', snap)
|
|
225
|
+
console.log('Logs:', logs)
|
|
248
226
|
```
|
|
249
227
|
|
|
250
|
-
|
|
251
|
-
// 4. Act: press Enter → observe plugin loaded
|
|
252
|
-
await state.page.keyboard.press('Enter')
|
|
253
|
-
await state.page.waitForTimeout(1000)
|
|
254
|
-
console.log('URL:', state.page.url())
|
|
255
|
-
const frame = state.page.frames().find((f) => f.url().includes('plugins.framercdn.com'))
|
|
256
|
-
await snapshot({ page: state.page, frame: frame || undefined }).then(console.log)
|
|
257
|
-
// If frame not found, wait and observe again — plugin may still be loading
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
**Other ways to observe action results:**
|
|
261
|
-
|
|
262
|
-
Snapshots are the primary feedback mechanism, but some actions have side effects that are better observed through other channels:
|
|
263
|
-
|
|
264
|
-
- **Console logs** — check for errors or app state after an action:
|
|
265
|
-
```js
|
|
266
|
-
await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
|
|
267
|
-
```
|
|
268
|
-
- **Network requests** — verify API calls were made after a form submit or button click:
|
|
269
|
-
```js
|
|
270
|
-
state.page.on('response', async (res) => {
|
|
271
|
-
if (res.url().includes('/api/')) {
|
|
272
|
-
console.log(res.status(), res.url())
|
|
273
|
-
}
|
|
274
|
-
})
|
|
275
|
-
```
|
|
276
|
-
- **URL changes** — confirm navigation happened:
|
|
277
|
-
```js
|
|
278
|
-
console.log(state.page.url())
|
|
279
|
-
```
|
|
280
|
-
- **Screenshots** — only for visual layout issues (see "choosing between snapshot methods" below).
|
|
228
|
+
Use `getLatestLogs()` for console errors, `state.page.url()` for navigation, screenshots only for visual layout issues.
|
|
281
229
|
|
|
282
230
|
## common mistakes to avoid
|
|
283
231
|
|
|
@@ -303,13 +251,9 @@ await state.page.keyboard.press('Meta+v') // always verify with screenshot!
|
|
|
303
251
|
```
|
|
304
252
|
|
|
305
253
|
**3. Using stale locators from old snapshots**
|
|
306
|
-
Locators (especially ones with `>> nth=`) can change when the page updates. Always get a fresh snapshot before clicking:
|
|
254
|
+
Locators (especially ones with `>> nth=`) can change when the page updates. Always get a fresh snapshot before clicking, then immediately use locators from that output:
|
|
307
255
|
|
|
308
256
|
```js
|
|
309
|
-
// BAD: using ref from minutes ago
|
|
310
|
-
await state.page.locator('[id="old-id"]').click() // element may have changed
|
|
311
|
-
|
|
312
|
-
// GOOD: get fresh snapshot, then immediately use locators from it
|
|
313
257
|
await snapshot({ page: state.page, showDiffSinceLastCall: true })
|
|
314
258
|
// Now use the NEW locators from this output
|
|
315
259
|
```
|
|
@@ -324,29 +268,22 @@ await screenshotWithAccessibilityLabels({ page: state.page })
|
|
|
324
268
|
```
|
|
325
269
|
|
|
326
270
|
**5. Text concatenation without line breaks**
|
|
327
|
-
`keyboard.type()` doesn't insert newlines from `\n` in strings. Use `keyboard.press('Enter')
|
|
271
|
+
`keyboard.type()` doesn't insert newlines from `\n` in strings. Use `keyboard.press('Enter')` between lines:
|
|
328
272
|
|
|
329
273
|
```js
|
|
330
|
-
// BAD: newlines in string don't create line breaks
|
|
331
|
-
await state.page.keyboard.type('Line 1\nLine 2') // becomes "Line 1Line 2"
|
|
332
|
-
|
|
333
|
-
// GOOD: use Enter key for line breaks
|
|
334
274
|
await state.page.keyboard.type('Line 1')
|
|
335
275
|
await state.page.keyboard.press('Enter')
|
|
336
276
|
await state.page.keyboard.type('Line 2')
|
|
337
277
|
```
|
|
338
278
|
|
|
339
279
|
**6. Quote escaping in bash**
|
|
340
|
-
Bash parses `$`, backticks, and `\` inside double-quoted strings. This silently corrupts JS code
|
|
280
|
+
Bash parses `$`, backticks, and `\` inside double-quoted strings. This silently corrupts JS code. Always use single quotes or heredoc:
|
|
341
281
|
|
|
342
282
|
```bash
|
|
343
|
-
#
|
|
344
|
-
playwriter -s 1 -e "const price = text.match(/\$[\d.]+/)"
|
|
345
|
-
|
|
346
|
-
# GOOD: single quotes — bash passes everything through literally
|
|
283
|
+
# single quotes — bash passes everything through literally
|
|
347
284
|
playwriter -s 1 -e 'await state.page.locator(`[id="_r_a_"]`).click()'
|
|
348
285
|
|
|
349
|
-
#
|
|
286
|
+
# heredoc for complex code with mixed quotes
|
|
350
287
|
playwriter -s 1 -e "$(cat <<'EOF'
|
|
351
288
|
await state.page.locator('[id="_r_a_"]').click()
|
|
352
289
|
const match = html.match(/\$[\d.]+/g)
|
|
@@ -355,17 +292,10 @@ EOF
|
|
|
355
292
|
```
|
|
356
293
|
|
|
357
294
|
**7. Using screenshots when snapshots suffice**
|
|
358
|
-
Screenshots + image analysis is expensive and slow. Only use screenshots for visual/CSS issues:
|
|
295
|
+
Screenshots + image analysis is expensive and slow. Only use screenshots for visual/CSS issues. Use snapshot for text checks:
|
|
359
296
|
|
|
360
297
|
```js
|
|
361
|
-
// BAD: screenshot to check if text appeared (wastes tokens on image analysis)
|
|
362
|
-
await state.page.screenshot({ path: 'check.png', scale: 'css' })
|
|
363
|
-
|
|
364
|
-
// GOOD: snapshot is text — fast, cheap, searchable
|
|
365
298
|
await snapshot({ page: state.page, search: /expected text/i })
|
|
366
|
-
|
|
367
|
-
// GOOD: evaluate DOM directly for content checks
|
|
368
|
-
const text = await state.page.evaluate(() => document.querySelector('.message')?.textContent)
|
|
369
299
|
```
|
|
370
300
|
|
|
371
301
|
**8. Assuming page content loaded**
|
|
@@ -380,28 +310,19 @@ await waitForPageLoad({ page: state.page, timeout: 5000 })
|
|
|
380
310
|
```
|
|
381
311
|
|
|
382
312
|
**9. Not using playwriter for JS-rendered sites**
|
|
383
|
-
Do NOT waste context trying webfetch, curl, or Playwright CLI screenshots on SPAs (Instagram, Twitter, etc.). These
|
|
313
|
+
Do NOT waste context trying webfetch, curl, or Playwright CLI screenshots on SPAs (Instagram, Twitter, etc.). These return empty HTML shells. Use playwriter directly:
|
|
384
314
|
|
|
385
315
|
```js
|
|
386
|
-
// BAD: webfetch/curl on Instagram returns empty HTML, grep finds nothing, huge context wasted
|
|
387
|
-
// BAD: Playwright CLI screenshot needs browser install, produces blank/modal-blocked images
|
|
388
|
-
|
|
389
|
-
// GOOD: use playwriter — real browser, full JS rendering, interactive
|
|
390
316
|
state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
|
|
391
317
|
await state.page.goto('https://www.instagram.com/p/ABC123/', { waitUntil: 'domcontentloaded' })
|
|
392
318
|
await waitForPageLoad({ page: state.page, timeout: 8000 })
|
|
393
319
|
await snapshot({ page: state.page, search: /cookie|consent|accept/i }).then(console.log)
|
|
394
|
-
// Now you can see modals, dismiss them, navigate carousels, extract content
|
|
395
320
|
```
|
|
396
321
|
|
|
397
322
|
**10. Login buttons that open popups**
|
|
398
|
-
Playwriter
|
|
323
|
+
Playwriter cannot control popup windows. Use cmd+click to open in a new tab instead:
|
|
399
324
|
|
|
400
325
|
```js
|
|
401
|
-
// BAD: popup window is not controllable by playwriter
|
|
402
|
-
await state.page.click('button:has-text("Login with Google")')
|
|
403
|
-
|
|
404
|
-
// GOOD: cmd+click opens in new tab that playwriter can control
|
|
405
326
|
await state.page.locator('button:has-text("Login with Google")').click({ modifiers: ['Meta'] })
|
|
406
327
|
await state.page.waitForTimeout(2000)
|
|
407
328
|
|
|
@@ -429,14 +350,9 @@ await state.page.getByRole('radio', { name: 'Nope, Vanilla' }).click()
|
|
|
429
350
|
```
|
|
430
351
|
|
|
431
352
|
**12. Never use `dispatchEvent` or `{ force: true }` to bypass blockers**
|
|
432
|
-
`dispatchEvent(new MouseEvent(...))
|
|
353
|
+
`dispatchEvent(new MouseEvent(...))`, `{ force: true }`, and `element.click()` inside `page.evaluate()` bypass Playwright checks but **do not trigger React/Vue/Svelte handlers** — state won't update. Use snapshot to find the real interactive element:
|
|
433
354
|
|
|
434
355
|
```js
|
|
435
|
-
// BAD: heading click bypasses overlay but React ignores it
|
|
436
|
-
await state.page.locator('h3:has-text("Node.js")').click({ force: true })
|
|
437
|
-
// BAD: evaluate click bypasses all Playwright input simulation
|
|
438
|
-
await state.page.evaluate(() => document.querySelector('button').click())
|
|
439
|
-
// GOOD: snapshot shows the real interactive element is a radio, not the heading
|
|
440
356
|
await state.page.getByRole('radio', { name: 'Node.js' }).click()
|
|
441
357
|
```
|
|
442
358
|
|
|
@@ -452,30 +368,12 @@ When something doesn't respond to a click, do NOT start inspecting CDP event lis
|
|
|
452
368
|
3. Take another `snapshot()` to see what changed
|
|
453
369
|
4. Only investigate DOM internals if correct interaction patterns produce zero response after 2–3 attempts
|
|
454
370
|
|
|
455
|
-
## checking page state
|
|
456
|
-
|
|
457
|
-
After any action (click, submit, navigate), verify what happened. Always print URL first, then snapshot:
|
|
458
|
-
|
|
459
|
-
```js
|
|
460
|
-
// Always print URL first, then snapshot
|
|
461
|
-
console.log('URL:', state.page.url())
|
|
462
|
-
await snapshot({ page: state.page }).then(console.log)
|
|
463
|
-
|
|
464
|
-
// Filter for specific content when snapshot is large
|
|
465
|
-
console.log('URL:', state.page.url())
|
|
466
|
-
await snapshot({ page: state.page, search: /dialog|button|error/i }).then(console.log)
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
If nothing changed, try `await waitForPageLoad({ page: state.page, timeout: 3000 })` or you may have clicked the wrong element.
|
|
470
|
-
|
|
471
371
|
## accessibility snapshots
|
|
472
372
|
|
|
473
373
|
```js
|
|
474
374
|
await snapshot({ page: state.page, search?, showDiffSinceLastCall? })
|
|
475
375
|
```
|
|
476
376
|
|
|
477
|
-
`accessibilitySnapshot` is still available as an alias for backward compatibility.
|
|
478
|
-
|
|
479
377
|
- `search` - string/regex to filter results (returns first 10 matching lines)
|
|
480
378
|
- `showDiffSinceLastCall` - returns diff since last snapshot (default: `true`, but `false` when `search` is provided). Pass `false` to get full snapshot.
|
|
481
379
|
|
|
@@ -514,12 +412,6 @@ const locator = refToLocator({ ref: 'e3' })
|
|
|
514
412
|
await state.page.locator(locator!).click()
|
|
515
413
|
```
|
|
516
414
|
|
|
517
|
-
```js
|
|
518
|
-
await state.page.locator('[id="nav-home"]').click()
|
|
519
|
-
await state.page.locator('[data-testid="docs-link"]').click()
|
|
520
|
-
await state.page.locator('role=link[name="Blog"]').click()
|
|
521
|
-
```
|
|
522
|
-
|
|
523
415
|
Search for specific elements:
|
|
524
416
|
|
|
525
417
|
```js
|
|
@@ -542,38 +434,11 @@ await snapshot({ locator: state.page.locator('form#checkout') })
|
|
|
542
434
|
|
|
543
435
|
Use this whenever the full page snapshot is dominated by navigation or layout elements you don't need. It saves significant tokens and makes the output much easier to parse.
|
|
544
436
|
|
|
545
|
-
**Filtering large snapshots in JS** — when
|
|
546
|
-
|
|
547
|
-
```js
|
|
548
|
-
const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false })
|
|
549
|
-
const relevant = snap
|
|
550
|
-
.split('\n')
|
|
551
|
-
.filter((l) => l.includes('dialog') || l.includes('error') || l.includes('button'))
|
|
552
|
-
.join('\n')
|
|
553
|
-
console.log(relevant)
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
This is much cheaper than taking a screenshot — use it as your primary debugging tool for verifying text content, checking if elements exist, or confirming state changes.
|
|
437
|
+
**Filtering large snapshots in JS** — when `search` isn't enough, filter the string directly: `snap.split('\n').filter(l => l.includes('dialog') || l.includes('error')).join('\n')`
|
|
557
438
|
|
|
558
439
|
## choosing between snapshot methods
|
|
559
440
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
**Use `snapshot` when:**
|
|
563
|
-
|
|
564
|
-
- Page has simple, semantic structure (articles, forms, lists)
|
|
565
|
-
- You need to search for specific text or patterns
|
|
566
|
-
- Token usage matters (text is smaller than images)
|
|
567
|
-
- You need to process the output programmatically
|
|
568
|
-
|
|
569
|
-
**Use `screenshotWithAccessibilityLabels` when:**
|
|
570
|
-
|
|
571
|
-
- Page has complex visual layout (grids, galleries, dashboards, maps)
|
|
572
|
-
- Spatial position matters (e.g., "first image", "top-left button")
|
|
573
|
-
- DOM order doesn't match visual order
|
|
574
|
-
- You need to understand the visual hierarchy
|
|
575
|
-
|
|
576
|
-
**Combining both:** Use screenshot first to understand layout and identify target elements visually, then use `snapshot({ search: /pattern/ })` for efficient searching in subsequent calls.
|
|
441
|
+
Use `snapshot` for text-heavy pages (forms, articles) — fast, cheap, searchable. Use `screenshotWithAccessibilityLabels` for complex visual layouts (grids, galleries, dashboards) where spatial position matters. Both share the same ref system and can be combined.
|
|
577
442
|
|
|
578
443
|
## selector best practices
|
|
579
444
|
|
|
@@ -659,19 +524,25 @@ await waitForPageLoad({ page: state.page, timeout: 5000 })
|
|
|
659
524
|
|
|
660
525
|
## common patterns
|
|
661
526
|
|
|
662
|
-
**Authenticated fetches** -
|
|
527
|
+
**Authenticated fetches** - fetch from within page context to include session cookies automatically:
|
|
663
528
|
|
|
664
529
|
```js
|
|
665
|
-
// BAD: curl/external requests don't have session cookies
|
|
666
|
-
// curl -H "Cookie: ..." often fails due to missing cookies or CSRF
|
|
667
|
-
|
|
668
|
-
// GOOD: fetch inside state.page.evaluate uses browser's full session
|
|
669
530
|
const data = await state.page.evaluate(async (url) => {
|
|
670
531
|
const resp = await fetch(url)
|
|
671
532
|
return await resp.text()
|
|
672
533
|
}, 'https://example.com/protected/resource')
|
|
673
534
|
```
|
|
674
535
|
|
|
536
|
+
**Read page cookies via CDP** - use `Network.getCookies` on the page CDP session:
|
|
537
|
+
|
|
538
|
+
```js
|
|
539
|
+
const cdp = await getCDPSession({ page: state.page })
|
|
540
|
+
const { cookies } = await cdp.send('Network.getCookies', { urls: [state.page.url()] })
|
|
541
|
+
console.log(cookies)
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
MUST use this for page-scoped cookies in extension mode. `Storage.getCookies` is a root-session command and will fail in playwriter.
|
|
545
|
+
|
|
675
546
|
**Downloading large data** - console output truncates large strings. Trigger a browser download instead:
|
|
676
547
|
|
|
677
548
|
```js
|
|
@@ -697,16 +568,6 @@ await state.page.evaluate(async (url) => {
|
|
|
697
568
|
|
|
698
569
|
Instead, use simpler alternatives (single download via `a.click()`, store data in `state`, etc).
|
|
699
570
|
|
|
700
|
-
**Links that open new tabs** - playwriter cannot control popup windows opened via `window.open`. Use cmd+click to open in a controllable new tab instead (see mistake #9 above for a full example):
|
|
701
|
-
|
|
702
|
-
```js
|
|
703
|
-
await state.page.locator('a[target=_blank]').click({ modifiers: ['Meta'] })
|
|
704
|
-
await state.page.waitForTimeout(1000)
|
|
705
|
-
const pages = context.pages()
|
|
706
|
-
const newTab = pages[pages.length - 1]
|
|
707
|
-
console.log('New tab URL:', newTab.url())
|
|
708
|
-
```
|
|
709
|
-
|
|
710
571
|
**Downloads** - capture and save:
|
|
711
572
|
|
|
712
573
|
```js
|
|
@@ -807,19 +668,7 @@ const fullHtml = await getCleanHTML({ locator: state.page, showDiffSinceLastCall
|
|
|
807
668
|
- `showDiffSinceLastCall` - returns diff since last call (default: `true`, but `false` when `search` is provided). Pass `false` to get full HTML.
|
|
808
669
|
- `includeStyles` - keep style and class attributes (default: false)
|
|
809
670
|
|
|
810
|
-
|
|
811
|
-
The function cleans HTML for compact, readable output:
|
|
812
|
-
|
|
813
|
-
- **Removes tags**: script, style, link, meta, noscript, svg, head
|
|
814
|
-
- **Unwraps nested wrappers**: Empty divs/spans with no attributes that only wrap a single child are collapsed (e.g., `<div><div><div><p>text</p></div></div></div>` → `<div><p>text</p></div>`)
|
|
815
|
-
- **Removes empty elements**: Elements with no attributes and no content are removed
|
|
816
|
-
- **Truncates long values**: Attribute values >200 chars and text content >500 chars are truncated
|
|
817
|
-
|
|
818
|
-
**Attributes kept (summary):**
|
|
819
|
-
|
|
820
|
-
- Common semantic and ARIA attributes (e.g., `href`, `name`, `type`, `aria-*`)
|
|
821
|
-
- All `data-*` test attributes
|
|
822
|
-
- Frequently used test IDs and special attributes (e.g., `testid`, `qa`, `e2e`, `vimium-label`)
|
|
671
|
+
Cleans HTML automatically: removes script/style/svg/head tags, unwraps empty wrappers, removes empty elements, truncates long values. Keeps semantic attributes (`href`, `name`, `type`, `aria-*`, `data-*`).
|
|
823
672
|
|
|
824
673
|
**getPageMarkdown** - extract main page content as plain text using Mozilla Readability (same algorithm as Firefox Reader View). Strips navigation, ads, sidebars, and other clutter. Returns formatted text with title, author, and content:
|
|
825
674
|
|
|
@@ -848,12 +697,6 @@ The main article content as plain text, with paragraphs preserved...
|
|
|
848
697
|
- `search` - string/regex to filter content (returns first 10 matching lines with 5 lines context)
|
|
849
698
|
- `showDiffSinceLastCall` - returns diff since last call (default: `true`, but `false` when `search` is provided). Pass `false` to get full content.
|
|
850
699
|
|
|
851
|
-
**Use cases:**
|
|
852
|
-
|
|
853
|
-
- Extract article text for LLM processing without HTML noise
|
|
854
|
-
- Get readable content from news sites, blogs, documentation
|
|
855
|
-
- Compare content changes after interactions
|
|
856
|
-
|
|
857
700
|
**waitForPageLoad** - smart load detection that ignores analytics/ads:
|
|
858
701
|
|
|
859
702
|
```js
|
|
@@ -934,94 +777,51 @@ Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkbo
|
|
|
934
777
|
|
|
935
778
|
**resizeImage** - shrink an image in-place so it consumes fewer tokens when read back into context. `await resizeImage({ input: './screenshot.png' })`. Also accepts `width`, `height`, `maxDimension`, `quality`, `output`.
|
|
936
779
|
|
|
937
|
-
**recording.start / recording.stop** - record the page as a video at native FPS (30-60fps). Uses `chrome.tabCapture`
|
|
938
|
-
|
|
939
|
-
While recording is active, Playwriter automatically overlays a smooth ghost cursor that follows automated mouse actions (`page.mouse.*`, `locator.click()`, hover flows) using `page.onMouseAction` from the Playwright fork.
|
|
780
|
+
**recording.start / recording.stop** - record the page as a video at native FPS (30-60fps). Uses `chrome.tabCapture` so **recording survives page navigation**. Auto-overlays a ghost cursor that follows mouse actions. Requires user to have clicked the Playwriter extension icon on the tab. Auto-resizes viewport to 16:9 (override with `aspectRatio: null`). Auto-stops after 15 min (override with `maxDurationMs`).
|
|
940
781
|
|
|
941
|
-
For demos
|
|
942
|
-
|
|
943
|
-
**Note**: Recording requires the user to have clicked the Playwriter extension icon on the tab. This grants `activeTab` permission needed for `chrome.tabCapture`. Recording works on tabs where the icon was clicked - if you need to record a new tab, ask the user to click the icon on it first.
|
|
782
|
+
For demos, use interaction methods (`locator.click()`, `page.mouse.move()`) instead of `goto()` to show realistic cursor motion.
|
|
944
783
|
|
|
945
784
|
```js
|
|
946
|
-
// Start recording - outputPath must be specified upfront
|
|
947
785
|
await recording.start({
|
|
948
786
|
page: state.page,
|
|
949
787
|
outputPath: './recording.mp4',
|
|
950
|
-
frameRate: 30, // default
|
|
951
|
-
audio: false, // default
|
|
952
|
-
videoBitsPerSecond: 2500000,
|
|
788
|
+
frameRate: 30, // default
|
|
789
|
+
audio: false, // default (tab audio)
|
|
790
|
+
videoBitsPerSecond: 2500000,
|
|
791
|
+
aspectRatio: { width: 16, height: 9 }, // default, set null to skip
|
|
792
|
+
maxDurationMs: 15 * 60 * 1000, // default, set 0 to disable
|
|
953
793
|
})
|
|
954
794
|
|
|
955
|
-
//
|
|
795
|
+
// Recording survives navigation
|
|
956
796
|
await state.page.click('a')
|
|
957
797
|
await state.page.waitForLoadState('domcontentloaded')
|
|
958
|
-
await state.page.goBack()
|
|
959
|
-
|
|
960
|
-
// Stop and get result
|
|
961
|
-
const { path, duration, size } = await recording.stop({ page: state.page })
|
|
962
|
-
console.log(`Saved ${size} bytes, duration: ${duration}ms`)
|
|
963
|
-
```
|
|
964
|
-
|
|
965
|
-
Additional recording utilities:
|
|
966
798
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const { isRecording, startedAt } = await recording.isRecording({ page: state.page })
|
|
799
|
+
// Stop — save full result including executionTimestamps for createDemoVideo
|
|
800
|
+
state.recordingResult = await recording.stop({ page: state.page })
|
|
970
801
|
|
|
971
|
-
//
|
|
972
|
-
await recording.cancel({ page: state.page })
|
|
802
|
+
// Other: recording.isRecording({ page }), recording.cancel({ page })
|
|
973
803
|
```
|
|
974
804
|
|
|
975
|
-
**ghostCursor.show / ghostCursor.hide** -
|
|
805
|
+
**ghostCursor.show / ghostCursor.hide** - show/hide cursor overlay for screenshots and demos:
|
|
976
806
|
|
|
977
807
|
```js
|
|
978
|
-
|
|
979
|
-
await ghostCursor.show({ page: state.page })
|
|
980
|
-
|
|
981
|
-
// Optional styles: 'minimal' (default triangular pointer), 'dot', 'screenstudio'
|
|
982
|
-
await ghostCursor.show({ page: state.page, style: 'minimal' })
|
|
983
|
-
|
|
984
|
-
// Hide cursor overlay
|
|
808
|
+
await ghostCursor.show({ page: state.page, style: 'minimal' }) // 'minimal', 'dot', 'screenstudio'
|
|
985
809
|
await ghostCursor.hide({ page: state.page })
|
|
986
810
|
```
|
|
987
811
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
**Key difference from getDisplayMedia**: This approach uses `chrome.tabCapture` which runs in the extension context, not the page. The recording persists across navigations because the extension holds the `MediaRecorder`, not the page's JavaScript context.
|
|
991
|
-
|
|
992
|
-
**createDemoVideo** - create a polished demo video from a recording by automatically speeding up idle sections (time between execute() calls) while keeping interactions at normal speed. Useful for creating demo videos of agent workflows without long pauses.
|
|
993
|
-
|
|
994
|
-
While recording is active, playwriter tracks when each `execute()` call starts and ends. `recording.stop()` returns these timestamps alongside the video file. `createDemoVideo` uses this data to identify idle gaps and speed them up with ffmpeg in a single pass.
|
|
995
|
-
|
|
996
|
-
A 1-second buffer is preserved around each interaction so viewers see context before and after each action.
|
|
997
|
-
|
|
998
|
-
Requires `ffmpeg` and `ffprobe` installed on the system.
|
|
999
|
-
|
|
1000
|
-
**Timeout**: `createDemoVideo` runs ffmpeg on the full recording and can take 60–120+ seconds. Always pass `--timeout 120000` (or higher) to the playwriter execute call that contains it, otherwise it will silently time out before the file is written.
|
|
812
|
+
**createDemoVideo** - speeds up idle sections (time between execute() calls) while keeping interactions at normal speed. Requires `ffmpeg`/`ffprobe`. Timestamps are tracked automatically during recording and returned by `recording.stop()`. **Timeout**: can take 60–120+ seconds, always pass `--timeout 120000` or higher.
|
|
1001
813
|
|
|
1002
814
|
```js
|
|
1003
|
-
//
|
|
1004
|
-
await recording.
|
|
1005
|
-
```
|
|
815
|
+
// After recording.stop(), save full result to state (executionTimestamps powers idle detection)
|
|
816
|
+
state.recordingResult = await recording.stop({ page: state.page })
|
|
1006
817
|
|
|
1007
|
-
|
|
1008
|
-
// ... multiple execute() calls with browser interactions ...
|
|
1009
|
-
// Each call's timing is tracked automatically while recording is active
|
|
1010
|
-
```
|
|
1011
|
-
|
|
1012
|
-
```js
|
|
1013
|
-
// Stop recording — executionTimestamps is included in the result
|
|
1014
|
-
const recordingResult = await recording.stop({ page: state.page })
|
|
1015
|
-
|
|
1016
|
-
// Create demo video — idle gaps are sped up 4x (default)
|
|
818
|
+
// In a SEPARATE execute call with --timeout 120000:
|
|
1017
819
|
const demoPath = await createDemoVideo({
|
|
1018
|
-
recordingPath: recordingResult.path,
|
|
1019
|
-
durationMs: recordingResult.duration,
|
|
1020
|
-
executionTimestamps: recordingResult.executionTimestamps,
|
|
1021
|
-
speed:
|
|
1022
|
-
// outputFile: './demo.mp4', // optional, defaults to recording-demo.mp4
|
|
820
|
+
recordingPath: state.recordingResult.path,
|
|
821
|
+
durationMs: state.recordingResult.duration,
|
|
822
|
+
executionTimestamps: state.recordingResult.executionTimestamps,
|
|
823
|
+
speed: 6, // default 6x for idle sections
|
|
1023
824
|
})
|
|
1024
|
-
console.log('Demo video:', demoPath)
|
|
1025
825
|
```
|
|
1026
826
|
|
|
1027
827
|
## pinned elements
|
|
@@ -1121,60 +921,7 @@ console.log(data)
|
|
|
1121
921
|
|
|
1122
922
|
Clean up listeners when done: `state.page.removeAllListeners('request'); state.page.removeAllListeners('response');`
|
|
1123
923
|
|
|
1124
|
-
##
|
|
1125
|
-
|
|
1126
|
-
When debugging why a web app isn't working (e.g., content not rendering, API errors, state issues), use these techniques **before** resorting to screenshots:
|
|
1127
|
-
|
|
1128
|
-
**1. Console logs** — use `getLatestLogs` to check for errors:
|
|
1129
|
-
|
|
1130
|
-
```js
|
|
1131
|
-
const errors = await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
|
|
1132
|
-
const appLogs = await getLatestLogs({ page: state.page, search: /myComponent|state/i })
|
|
1133
|
-
```
|
|
1134
|
-
|
|
1135
|
-
**2. DOM inspection via evaluate** — check content directly without screenshots:
|
|
1136
|
-
|
|
1137
|
-
```js
|
|
1138
|
-
const info = await state.page.evaluate(() => {
|
|
1139
|
-
const msgs = document.querySelectorAll('.message')
|
|
1140
|
-
return Array.from(msgs).map((m) => ({
|
|
1141
|
-
text: m.textContent?.slice(0, 200),
|
|
1142
|
-
visible: m.offsetHeight > 0,
|
|
1143
|
-
}))
|
|
1144
|
-
})
|
|
1145
|
-
console.log(JSON.stringify(info, null, 2))
|
|
1146
|
-
```
|
|
1147
|
-
|
|
1148
|
-
**3. Combine snapshot + logs for full picture:**
|
|
1149
|
-
|
|
1150
|
-
```js
|
|
1151
|
-
await state.page.keyboard.press('Enter')
|
|
1152
|
-
await state.page.waitForTimeout(2000)
|
|
1153
|
-
|
|
1154
|
-
const snap = await snapshot({ page: state.page, search: /dialog|error|message/ })
|
|
1155
|
-
const logs = await getLatestLogs({ page: state.page, search: /error/i, count: 10 })
|
|
1156
|
-
console.log('UI:', snap)
|
|
1157
|
-
console.log('Logs:', logs)
|
|
1158
|
-
```
|
|
1159
|
-
|
|
1160
|
-
## capabilities
|
|
1161
|
-
|
|
1162
|
-
Examples of what playwriter can do:
|
|
1163
|
-
|
|
1164
|
-
- Monitor console logs while user reproduces a bug
|
|
1165
|
-
- Intercept network requests to reverse-engineer APIs and build SDKs
|
|
1166
|
-
- Scrape data by replaying paginated API calls instead of scrolling DOM
|
|
1167
|
-
- Get accessibility snapshot to find elements, then automate interactions
|
|
1168
|
-
- Use visual screenshots to understand complex layouts like image grids, dashboards, or maps
|
|
1169
|
-
- Debug issues by collecting logs and controlling the page simultaneously
|
|
1170
|
-
- Handle popups, downloads, iframes, and dialog boxes
|
|
1171
|
-
- Record videos of browser sessions that survive page navigation
|
|
1172
|
-
|
|
1173
|
-
## computer use
|
|
1174
|
-
|
|
1175
|
-
Playwriter provides the same browser control as Anthropic's `computer_20250124` tool and the Claude Chrome extension, using Playwright APIs instead of screenshot-based coordinate clicking. No computer use beta needed.
|
|
1176
|
-
|
|
1177
|
-
This section covers low-level mouse/keyboard APIs not documented elsewhere. For locator-based clicking, screenshots, navigation, forms, evaluate, snapshots, and network interception see their dedicated sections above.
|
|
924
|
+
## computer use (low-level mouse/keyboard)
|
|
1178
925
|
|
|
1179
926
|
### clicking
|
|
1180
927
|
|
|
@@ -1277,19 +1024,4 @@ Prefer locator-based actions over coordinates — locators are stable across scr
|
|
|
1277
1024
|
|
|
1278
1025
|
## Ghost Browser integration
|
|
1279
1026
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
```js
|
|
1283
|
-
// List identities and open tabs in different ones
|
|
1284
|
-
const identities = await chrome.projects.getIdentitiesList()
|
|
1285
|
-
await chrome.ghostPublicAPI.openTab({ url: 'https://reddit.com', identity: identities[0].id })
|
|
1286
|
-
|
|
1287
|
-
// Assign proxies per tab or identity
|
|
1288
|
-
const proxies = await chrome.ghostProxies.getList()
|
|
1289
|
-
await chrome.ghostProxies.setTabProxy(tabId, proxies[0].id)
|
|
1290
|
-
```
|
|
1291
|
-
|
|
1292
|
-
For complete API reference with all methods, types, and examples, read:
|
|
1293
|
-
`extension/src/ghost-browser-api.d.ts`
|
|
1294
|
-
|
|
1295
|
-
Note: Only works in Ghost Browser. In regular Chrome, calls fail with "not available".
|
|
1027
|
+
When running in [Ghost Browser](https://ghostbrowser.com/), the `chrome` object exposes APIs for multi-identity automation (identities, proxies, sessions). See `extension/src/ghost-browser-api.d.ts` for full API reference. Only works in Ghost Browser — calls fail in regular Chrome.
|