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.
Files changed (58) hide show
  1. package/dist/a11y-client.js +18 -8
  2. package/dist/aria-snapshot.d.ts.map +1 -1
  3. package/dist/aria-snapshot.js +3 -1
  4. package/dist/aria-snapshot.js.map +1 -1
  5. package/dist/bippy.js +1 -1
  6. package/dist/cdp-relay.d.ts.map +1 -1
  7. package/dist/cdp-relay.js +84 -0
  8. package/dist/cdp-relay.js.map +1 -1
  9. package/dist/executor.d.ts.map +1 -1
  10. package/dist/executor.js +8 -6
  11. package/dist/executor.js.map +1 -1
  12. package/dist/ffmpeg.d.ts +6 -6
  13. package/dist/ffmpeg.d.ts.map +1 -1
  14. package/dist/ffmpeg.js +6 -6
  15. package/dist/ffmpeg.js.map +1 -1
  16. package/dist/ghost-cursor-client.js +15 -9
  17. package/dist/prompt.md +71 -337
  18. package/dist/readability.js +16 -2
  19. package/dist/recording-ghost-cursor.d.ts.map +1 -1
  20. package/dist/recording-ghost-cursor.js +1 -1
  21. package/dist/recording-ghost-cursor.js.map +1 -1
  22. package/dist/relay-client.js +1 -1
  23. package/dist/relay-client.js.map +1 -1
  24. package/dist/relay-core.test.d.ts.map +1 -1
  25. package/dist/relay-core.test.js +344 -16
  26. package/dist/relay-core.test.js.map +1 -1
  27. package/dist/relay-navigation.test.d.ts.map +1 -1
  28. package/dist/relay-navigation.test.js +115 -0
  29. package/dist/relay-navigation.test.js.map +1 -1
  30. package/dist/screen-recording.d.ts +24 -0
  31. package/dist/screen-recording.d.ts.map +1 -1
  32. package/dist/screen-recording.js +62 -0
  33. package/dist/screen-recording.js.map +1 -1
  34. package/dist/screen-recording.test.d.ts +2 -0
  35. package/dist/screen-recording.test.d.ts.map +1 -0
  36. package/dist/screen-recording.test.js +102 -0
  37. package/dist/screen-recording.test.js.map +1 -0
  38. package/dist/selector-generator.js +1 -1
  39. package/package.json +2 -2
  40. package/src/aria-snapshot.ts +3 -1
  41. package/src/aria-snapshots/github-interactive.txt +2 -0
  42. package/src/aria-snapshots/github-raw.txt +4 -0
  43. package/src/aria-snapshots/hackernews-interactive.txt +238 -241
  44. package/src/aria-snapshots/hackernews-raw.txt +267 -271
  45. package/src/assets/aria-labels-hacker-news.png +0 -0
  46. package/src/cdp-relay.ts +110 -0
  47. package/src/executor.ts +8 -6
  48. package/src/ffmpeg.ts +8 -8
  49. package/src/ghost-cursor-client.ts +3 -2
  50. package/src/recording-ghost-cursor.ts +7 -1
  51. package/src/relay-client.ts +1 -1
  52. package/src/relay-core.test.ts +378 -17
  53. package/src/relay-navigation.test.ts +132 -0
  54. package/src/screen-recording.test.ts +111 -0
  55. package/src/screen-recording.ts +81 -0
  56. package/src/skill.md +71 -339
  57. package/src/snapshots/shadcn-ui-accessibility-full.md +182 -180
  58. 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 should follow a **observe → act → observe** loop. After every action, you must check its result before proceeding. Never chain multiple actions blindly — the page may not have responded as expected.
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 and navigate to the target URL
195
- 2. **Observe** — print `state.page.url()` and take an accessibility snapshot. Always print the URL so you know where you are — pages can redirect, and actions can trigger unexpected navigation.
196
- 3. **Check** — read the snapshot and URL. If the page isn't ready (still loading, expected content missing, wrong URL), **wait and observe again** — don't act on stale or incomplete state. Only proceed when you can identify the element to interact with.
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. If the action didn't take effect (nothing changed, page still loading), wait and observe again before proceeding.
199
- 6. **Repeat** — continue from step 3 until the task is complete
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
- // 1. Open page and observe always print URL first
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://framer.com/projects/my-project', { waitUntil: 'domcontentloaded' })
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. Act: open command palette → observe result
237
- await state.page.keyboard.press('Meta+k')
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, search: /dialog|Search/ }).then(console.log)
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
- // 3. Act: type search query observe result
245
- await state.page.keyboard.type('MCP')
246
- console.log('URL:', state.page.url())
247
- await snapshot({ page: state.page, search: /MCP/ }).then(console.log)
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
- ```js
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 containing dollar signs (regex like `/\$[\d.]+/`), template literals, or backslash patterns.
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
- # BAD: double quotes — bash interprets $ and backticks in your JS
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
- # GOOD: heredoc for complex code with mixed quotes
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 sites return empty HTML shells — the real content is rendered by JavaScript. Use playwriter with a real browser session instead:
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 extension cannot control popup windows. If a login button opens a popup (common with OAuth/SSO), use cmd+click to open in a new tab instead:
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(...))` and `{ force: true }` bypass Playwright checks but **do not trigger React/Vue/Svelte handlers** — state won't update. The same applies to `element.click()` inside `page.evaluate()`. If a click "succeeds" but nothing changes, you're either clicking the wrong node or using the wrong interaction pattern:
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 the built-in `search` isn't enough (e.g., you need multiple patterns or custom logic), filter the snapshot string directly:
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
- Both `snapshot` and `screenshotWithAccessibilityLabels` use the same ref system, so you can combine them effectively.
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** - to access protected resources, fetch from within page context (includes session cookies automatically):
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
- **HTML processing:**
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` in the extension context, so **recording survives page navigation**. Video is saved as mp4.
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 where cursor movement should be visible and human-like, drive the page with interaction methods (`locator.click()`, `page.click()`, `page.mouse.move()`, `press`, typing). Avoid skipping interactions with direct state jumps (for example, `goto(itemUrl)` instead of clicking the link) when your goal is to show realistic pointer motion in the recording.
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: 30
951
- audio: false, // default: false (tab audio)
952
- videoBitsPerSecond: 2500000, // 2.5 Mbps
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
- // Navigate around - recording continues!
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
- ```js
968
- // Check if recording is active
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
- // Cancel recording without saving
972
- await recording.cancel({ page: state.page })
802
+ // Other: recording.isRecording({ page }), recording.cancel({ page })
973
803
  ```
974
804
 
975
- **ghostCursor.show / ghostCursor.hide** - manually show or hide the in-page cursor overlay. Useful for screenshots and demos even when recording is not running.
805
+ **ghostCursor.show / ghostCursor.hide** - show/hide cursor overlay for screenshots and demos:
976
806
 
977
807
  ```js
978
- // Show cursor in the center (or keep current position if already visible)
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
- `startRecording`, `stopRecording`, `isRecording`, and `cancelRecording` remain available as backward-compatible aliases.
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
- // Start recording
1004
- await recording.start({ page: state.page, outputPath: './recording.mp4' })
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
- ```js
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: 5, // optional, default 5x for idle sections
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
- ## debugging web apps
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
- Playwriter supports [Ghost Browser](https://ghostbrowser.com/) for multi-identity automation. When running in Ghost Browser, the `chrome` object exposes APIs to control identities, proxies, and sessions - useful for managing multiple accounts, rotating proxies, or isolated cookie sessions.
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.