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/dist/prompt.md
CHANGED
|
@@ -61,97 +61,47 @@ You can collaborate with the user - they can help with captchas, difficult eleme
|
|
|
61
61
|
|
|
62
62
|
## interaction feedback loop
|
|
63
63
|
|
|
64
|
-
Every browser interaction
|
|
64
|
+
Every browser interaction must follow **observe → act → observe**. Never chain multiple actions blindly.
|
|
65
65
|
|
|
66
|
-
**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
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.
|
|
70
|
-
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.
|
|
66
|
+
1. **Open page** — get or create your page, navigate to URL
|
|
67
|
+
2. **Observe** — print `state.page.url()` + `snapshot()`. Always print URL — pages can redirect unexpectedly.
|
|
68
|
+
3. **Check** — if page isn't ready (loading, wrong URL, content missing), wait and observe again
|
|
71
69
|
4. **Act** — perform one action (click, type, submit)
|
|
72
|
-
5. **Observe again** — print URL + snapshot to verify the action's effect
|
|
73
|
-
6. **Repeat**
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
┌─────────────────────────────────────────────┐
|
|
77
|
-
│ open page + goto URL │
|
|
78
|
-
└──────────────────┬──────────────────────────┘
|
|
79
|
-
▼
|
|
80
|
-
┌────────────────┐
|
|
81
|
-
┌───►│ observe │◄─────────────────┐
|
|
82
|
-
│ │ (url + snapshot) │ │
|
|
83
|
-
│ └───────┬────────┘ │
|
|
84
|
-
│ ▼ │
|
|
85
|
-
│ ┌────────────────┐ │
|
|
86
|
-
│ │ check │ │
|
|
87
|
-
│ │ (read result) │ │
|
|
88
|
-
│ └───┬────────┬───┘ │
|
|
89
|
-
│ not │ │ ready │
|
|
90
|
-
│ ready │ ▼ │
|
|
91
|
-
└────────┘ ┌────────────────┐ │
|
|
92
|
-
│ act │ │
|
|
93
|
-
│ (click/type) │─────────────┘
|
|
94
|
-
└────────────────┘
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
**Example: opening a Framer plugin via the command palette**
|
|
98
|
-
|
|
99
|
-
Each step is a separate execute call. Notice how every action is followed by a snapshot to verify what happened:
|
|
70
|
+
5. **Observe again** — print URL + snapshot to verify the action's effect
|
|
71
|
+
6. **Repeat** from step 3 until task is complete
|
|
100
72
|
|
|
101
73
|
```js
|
|
102
|
-
//
|
|
74
|
+
// Each step should be a separate execute call:
|
|
75
|
+
// Step 1: navigate + observe
|
|
103
76
|
state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
|
|
104
|
-
await state.page.goto('https://
|
|
77
|
+
await state.page.goto('https://example.com', { waitUntil: 'domcontentloaded' })
|
|
105
78
|
console.log('URL:', state.page.url())
|
|
106
79
|
await snapshot({ page: state.page }).then(console.log)
|
|
107
80
|
```
|
|
108
81
|
|
|
109
82
|
```js
|
|
110
|
-
// 2
|
|
111
|
-
await state.page.
|
|
83
|
+
// Step 2: act + observe
|
|
84
|
+
await state.page.locator('button:has-text("Submit")').click()
|
|
112
85
|
console.log('URL:', state.page.url())
|
|
113
|
-
await snapshot({ page: state.page
|
|
114
|
-
// If dialog didn't appear, observe again before retrying
|
|
86
|
+
await snapshot({ page: state.page }).then(console.log)
|
|
115
87
|
```
|
|
116
88
|
|
|
89
|
+
If nothing changed after an action, try `waitForPageLoad({ page: state.page, timeout: 3000 })` or you may have clicked the wrong element.
|
|
90
|
+
|
|
91
|
+
**Deeper observation** — when snapshots aren't enough to understand what happened, combine multiple channels:
|
|
92
|
+
|
|
117
93
|
```js
|
|
118
|
-
//
|
|
119
|
-
await state.page
|
|
120
|
-
|
|
121
|
-
|
|
94
|
+
// Check console for errors after an action
|
|
95
|
+
const errors = await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
|
|
96
|
+
|
|
97
|
+
// Combine snapshot + logs for full picture
|
|
98
|
+
const snap = await snapshot({ page: state.page, search: /dialog|error|message/ })
|
|
99
|
+
const logs = await getLatestLogs({ page: state.page, search: /error/i, count: 10 })
|
|
100
|
+
console.log('UI:', snap)
|
|
101
|
+
console.log('Logs:', logs)
|
|
122
102
|
```
|
|
123
103
|
|
|
124
|
-
|
|
125
|
-
// 4. Act: press Enter → observe plugin loaded
|
|
126
|
-
await state.page.keyboard.press('Enter')
|
|
127
|
-
await state.page.waitForTimeout(1000)
|
|
128
|
-
console.log('URL:', state.page.url())
|
|
129
|
-
const frame = state.page.frames().find((f) => f.url().includes('plugins.framercdn.com'))
|
|
130
|
-
await snapshot({ page: state.page, frame: frame || undefined }).then(console.log)
|
|
131
|
-
// If frame not found, wait and observe again — plugin may still be loading
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
**Other ways to observe action results:**
|
|
135
|
-
|
|
136
|
-
Snapshots are the primary feedback mechanism, but some actions have side effects that are better observed through other channels:
|
|
137
|
-
|
|
138
|
-
- **Console logs** — check for errors or app state after an action:
|
|
139
|
-
```js
|
|
140
|
-
await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
|
|
141
|
-
```
|
|
142
|
-
- **Network requests** — verify API calls were made after a form submit or button click:
|
|
143
|
-
```js
|
|
144
|
-
state.page.on('response', async (res) => {
|
|
145
|
-
if (res.url().includes('/api/')) {
|
|
146
|
-
console.log(res.status(), res.url())
|
|
147
|
-
}
|
|
148
|
-
})
|
|
149
|
-
```
|
|
150
|
-
- **URL changes** — confirm navigation happened:
|
|
151
|
-
```js
|
|
152
|
-
console.log(state.page.url())
|
|
153
|
-
```
|
|
154
|
-
- **Screenshots** — only for visual layout issues (see "choosing between snapshot methods" below).
|
|
104
|
+
Use `getLatestLogs()` for console errors, `state.page.url()` for navigation, screenshots only for visual layout issues.
|
|
155
105
|
|
|
156
106
|
## common mistakes to avoid
|
|
157
107
|
|
|
@@ -177,13 +127,9 @@ await state.page.keyboard.press('Meta+v') // always verify with screenshot!
|
|
|
177
127
|
```
|
|
178
128
|
|
|
179
129
|
**3. Using stale locators from old snapshots**
|
|
180
|
-
Locators (especially ones with `>> nth=`) can change when the page updates. Always get a fresh snapshot before clicking:
|
|
130
|
+
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:
|
|
181
131
|
|
|
182
132
|
```js
|
|
183
|
-
// BAD: using ref from minutes ago
|
|
184
|
-
await state.page.locator('[id="old-id"]').click() // element may have changed
|
|
185
|
-
|
|
186
|
-
// GOOD: get fresh snapshot, then immediately use locators from it
|
|
187
133
|
await snapshot({ page: state.page, showDiffSinceLastCall: true })
|
|
188
134
|
// Now use the NEW locators from this output
|
|
189
135
|
```
|
|
@@ -198,29 +144,22 @@ await screenshotWithAccessibilityLabels({ page: state.page })
|
|
|
198
144
|
```
|
|
199
145
|
|
|
200
146
|
**5. Text concatenation without line breaks**
|
|
201
|
-
`keyboard.type()` doesn't insert newlines from `\n` in strings. Use `keyboard.press('Enter')
|
|
147
|
+
`keyboard.type()` doesn't insert newlines from `\n` in strings. Use `keyboard.press('Enter')` between lines:
|
|
202
148
|
|
|
203
149
|
```js
|
|
204
|
-
// BAD: newlines in string don't create line breaks
|
|
205
|
-
await state.page.keyboard.type('Line 1\nLine 2') // becomes "Line 1Line 2"
|
|
206
|
-
|
|
207
|
-
// GOOD: use Enter key for line breaks
|
|
208
150
|
await state.page.keyboard.type('Line 1')
|
|
209
151
|
await state.page.keyboard.press('Enter')
|
|
210
152
|
await state.page.keyboard.type('Line 2')
|
|
211
153
|
```
|
|
212
154
|
|
|
213
155
|
**6. Quote escaping in bash**
|
|
214
|
-
Bash parses `$`, backticks, and `\` inside double-quoted strings. This silently corrupts JS code
|
|
156
|
+
Bash parses `$`, backticks, and `\` inside double-quoted strings. This silently corrupts JS code. Always use single quotes or heredoc:
|
|
215
157
|
|
|
216
158
|
```bash
|
|
217
|
-
#
|
|
218
|
-
playwriter -s 1 -e "const price = text.match(/\$[\d.]+/)"
|
|
219
|
-
|
|
220
|
-
# GOOD: single quotes — bash passes everything through literally
|
|
159
|
+
# single quotes — bash passes everything through literally
|
|
221
160
|
playwriter -s 1 -e 'await state.page.locator(`[id="_r_a_"]`).click()'
|
|
222
161
|
|
|
223
|
-
#
|
|
162
|
+
# heredoc for complex code with mixed quotes
|
|
224
163
|
playwriter -s 1 -e "$(cat <<'EOF'
|
|
225
164
|
await state.page.locator('[id="_r_a_"]').click()
|
|
226
165
|
const match = html.match(/\$[\d.]+/g)
|
|
@@ -229,17 +168,10 @@ EOF
|
|
|
229
168
|
```
|
|
230
169
|
|
|
231
170
|
**7. Using screenshots when snapshots suffice**
|
|
232
|
-
Screenshots + image analysis is expensive and slow. Only use screenshots for visual/CSS issues:
|
|
171
|
+
Screenshots + image analysis is expensive and slow. Only use screenshots for visual/CSS issues. Use snapshot for text checks:
|
|
233
172
|
|
|
234
173
|
```js
|
|
235
|
-
// BAD: screenshot to check if text appeared (wastes tokens on image analysis)
|
|
236
|
-
await state.page.screenshot({ path: 'check.png', scale: 'css' })
|
|
237
|
-
|
|
238
|
-
// GOOD: snapshot is text — fast, cheap, searchable
|
|
239
174
|
await snapshot({ page: state.page, search: /expected text/i })
|
|
240
|
-
|
|
241
|
-
// GOOD: evaluate DOM directly for content checks
|
|
242
|
-
const text = await state.page.evaluate(() => document.querySelector('.message')?.textContent)
|
|
243
175
|
```
|
|
244
176
|
|
|
245
177
|
**8. Assuming page content loaded**
|
|
@@ -254,28 +186,19 @@ await waitForPageLoad({ page: state.page, timeout: 5000 })
|
|
|
254
186
|
```
|
|
255
187
|
|
|
256
188
|
**9. Not using playwriter for JS-rendered sites**
|
|
257
|
-
Do NOT waste context trying webfetch, curl, or Playwright CLI screenshots on SPAs (Instagram, Twitter, etc.). These
|
|
189
|
+
Do NOT waste context trying webfetch, curl, or Playwright CLI screenshots on SPAs (Instagram, Twitter, etc.). These return empty HTML shells. Use playwriter directly:
|
|
258
190
|
|
|
259
191
|
```js
|
|
260
|
-
// BAD: webfetch/curl on Instagram returns empty HTML, grep finds nothing, huge context wasted
|
|
261
|
-
// BAD: Playwright CLI screenshot needs browser install, produces blank/modal-blocked images
|
|
262
|
-
|
|
263
|
-
// GOOD: use playwriter — real browser, full JS rendering, interactive
|
|
264
192
|
state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
|
|
265
193
|
await state.page.goto('https://www.instagram.com/p/ABC123/', { waitUntil: 'domcontentloaded' })
|
|
266
194
|
await waitForPageLoad({ page: state.page, timeout: 8000 })
|
|
267
195
|
await snapshot({ page: state.page, search: /cookie|consent|accept/i }).then(console.log)
|
|
268
|
-
// Now you can see modals, dismiss them, navigate carousels, extract content
|
|
269
196
|
```
|
|
270
197
|
|
|
271
198
|
**10. Login buttons that open popups**
|
|
272
|
-
Playwriter
|
|
199
|
+
Playwriter cannot control popup windows. Use cmd+click to open in a new tab instead:
|
|
273
200
|
|
|
274
201
|
```js
|
|
275
|
-
// BAD: popup window is not controllable by playwriter
|
|
276
|
-
await state.page.click('button:has-text("Login with Google")')
|
|
277
|
-
|
|
278
|
-
// GOOD: cmd+click opens in new tab that playwriter can control
|
|
279
202
|
await state.page.locator('button:has-text("Login with Google")').click({ modifiers: ['Meta'] })
|
|
280
203
|
await state.page.waitForTimeout(2000)
|
|
281
204
|
|
|
@@ -303,14 +226,9 @@ await state.page.getByRole('radio', { name: 'Nope, Vanilla' }).click()
|
|
|
303
226
|
```
|
|
304
227
|
|
|
305
228
|
**12. Never use `dispatchEvent` or `{ force: true }` to bypass blockers**
|
|
306
|
-
`dispatchEvent(new MouseEvent(...))
|
|
229
|
+
`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:
|
|
307
230
|
|
|
308
231
|
```js
|
|
309
|
-
// BAD: heading click bypasses overlay but React ignores it
|
|
310
|
-
await state.page.locator('h3:has-text("Node.js")').click({ force: true })
|
|
311
|
-
// BAD: evaluate click bypasses all Playwright input simulation
|
|
312
|
-
await state.page.evaluate(() => document.querySelector('button').click())
|
|
313
|
-
// GOOD: snapshot shows the real interactive element is a radio, not the heading
|
|
314
232
|
await state.page.getByRole('radio', { name: 'Node.js' }).click()
|
|
315
233
|
```
|
|
316
234
|
|
|
@@ -326,30 +244,12 @@ When something doesn't respond to a click, do NOT start inspecting CDP event lis
|
|
|
326
244
|
3. Take another `snapshot()` to see what changed
|
|
327
245
|
4. Only investigate DOM internals if correct interaction patterns produce zero response after 2–3 attempts
|
|
328
246
|
|
|
329
|
-
## checking page state
|
|
330
|
-
|
|
331
|
-
After any action (click, submit, navigate), verify what happened. Always print URL first, then snapshot:
|
|
332
|
-
|
|
333
|
-
```js
|
|
334
|
-
// Always print URL first, then snapshot
|
|
335
|
-
console.log('URL:', state.page.url())
|
|
336
|
-
await snapshot({ page: state.page }).then(console.log)
|
|
337
|
-
|
|
338
|
-
// Filter for specific content when snapshot is large
|
|
339
|
-
console.log('URL:', state.page.url())
|
|
340
|
-
await snapshot({ page: state.page, search: /dialog|button|error/i }).then(console.log)
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
If nothing changed, try `await waitForPageLoad({ page: state.page, timeout: 3000 })` or you may have clicked the wrong element.
|
|
344
|
-
|
|
345
247
|
## accessibility snapshots
|
|
346
248
|
|
|
347
249
|
```js
|
|
348
250
|
await snapshot({ page: state.page, search?, showDiffSinceLastCall? })
|
|
349
251
|
```
|
|
350
252
|
|
|
351
|
-
`accessibilitySnapshot` is still available as an alias for backward compatibility.
|
|
352
|
-
|
|
353
253
|
- `search` - string/regex to filter results (returns first 10 matching lines)
|
|
354
254
|
- `showDiffSinceLastCall` - returns diff since last snapshot (default: `true`, but `false` when `search` is provided). Pass `false` to get full snapshot.
|
|
355
255
|
|
|
@@ -388,12 +288,6 @@ const locator = refToLocator({ ref: 'e3' })
|
|
|
388
288
|
await state.page.locator(locator!).click()
|
|
389
289
|
```
|
|
390
290
|
|
|
391
|
-
```js
|
|
392
|
-
await state.page.locator('[id="nav-home"]').click()
|
|
393
|
-
await state.page.locator('[data-testid="docs-link"]').click()
|
|
394
|
-
await state.page.locator('role=link[name="Blog"]').click()
|
|
395
|
-
```
|
|
396
|
-
|
|
397
291
|
Search for specific elements:
|
|
398
292
|
|
|
399
293
|
```js
|
|
@@ -416,38 +310,11 @@ await snapshot({ locator: state.page.locator('form#checkout') })
|
|
|
416
310
|
|
|
417
311
|
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.
|
|
418
312
|
|
|
419
|
-
**Filtering large snapshots in JS** — when
|
|
420
|
-
|
|
421
|
-
```js
|
|
422
|
-
const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false })
|
|
423
|
-
const relevant = snap
|
|
424
|
-
.split('\n')
|
|
425
|
-
.filter((l) => l.includes('dialog') || l.includes('error') || l.includes('button'))
|
|
426
|
-
.join('\n')
|
|
427
|
-
console.log(relevant)
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
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.
|
|
313
|
+
**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')`
|
|
431
314
|
|
|
432
315
|
## choosing between snapshot methods
|
|
433
316
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
**Use `snapshot` when:**
|
|
437
|
-
|
|
438
|
-
- Page has simple, semantic structure (articles, forms, lists)
|
|
439
|
-
- You need to search for specific text or patterns
|
|
440
|
-
- Token usage matters (text is smaller than images)
|
|
441
|
-
- You need to process the output programmatically
|
|
442
|
-
|
|
443
|
-
**Use `screenshotWithAccessibilityLabels` when:**
|
|
444
|
-
|
|
445
|
-
- Page has complex visual layout (grids, galleries, dashboards, maps)
|
|
446
|
-
- Spatial position matters (e.g., "first image", "top-left button")
|
|
447
|
-
- DOM order doesn't match visual order
|
|
448
|
-
- You need to understand the visual hierarchy
|
|
449
|
-
|
|
450
|
-
**Combining both:** Use screenshot first to understand layout and identify target elements visually, then use `snapshot({ search: /pattern/ })` for efficient searching in subsequent calls.
|
|
317
|
+
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.
|
|
451
318
|
|
|
452
319
|
## selector best practices
|
|
453
320
|
|
|
@@ -533,19 +400,25 @@ await waitForPageLoad({ page: state.page, timeout: 5000 })
|
|
|
533
400
|
|
|
534
401
|
## common patterns
|
|
535
402
|
|
|
536
|
-
**Authenticated fetches** -
|
|
403
|
+
**Authenticated fetches** - fetch from within page context to include session cookies automatically:
|
|
537
404
|
|
|
538
405
|
```js
|
|
539
|
-
// BAD: curl/external requests don't have session cookies
|
|
540
|
-
// curl -H "Cookie: ..." often fails due to missing cookies or CSRF
|
|
541
|
-
|
|
542
|
-
// GOOD: fetch inside state.page.evaluate uses browser's full session
|
|
543
406
|
const data = await state.page.evaluate(async (url) => {
|
|
544
407
|
const resp = await fetch(url)
|
|
545
408
|
return await resp.text()
|
|
546
409
|
}, 'https://example.com/protected/resource')
|
|
547
410
|
```
|
|
548
411
|
|
|
412
|
+
**Read page cookies via CDP** - use `Network.getCookies` on the page CDP session:
|
|
413
|
+
|
|
414
|
+
```js
|
|
415
|
+
const cdp = await getCDPSession({ page: state.page })
|
|
416
|
+
const { cookies } = await cdp.send('Network.getCookies', { urls: [state.page.url()] })
|
|
417
|
+
console.log(cookies)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
MUST use this for page-scoped cookies in extension mode. `Storage.getCookies` is a root-session command and will fail in playwriter.
|
|
421
|
+
|
|
549
422
|
**Downloading large data** - console output truncates large strings. Trigger a browser download instead:
|
|
550
423
|
|
|
551
424
|
```js
|
|
@@ -571,16 +444,6 @@ await state.page.evaluate(async (url) => {
|
|
|
571
444
|
|
|
572
445
|
Instead, use simpler alternatives (single download via `a.click()`, store data in `state`, etc).
|
|
573
446
|
|
|
574
|
-
**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):
|
|
575
|
-
|
|
576
|
-
```js
|
|
577
|
-
await state.page.locator('a[target=_blank]').click({ modifiers: ['Meta'] })
|
|
578
|
-
await state.page.waitForTimeout(1000)
|
|
579
|
-
const pages = context.pages()
|
|
580
|
-
const newTab = pages[pages.length - 1]
|
|
581
|
-
console.log('New tab URL:', newTab.url())
|
|
582
|
-
```
|
|
583
|
-
|
|
584
447
|
**Downloads** - capture and save:
|
|
585
448
|
|
|
586
449
|
```js
|
|
@@ -681,19 +544,7 @@ const fullHtml = await getCleanHTML({ locator: state.page, showDiffSinceLastCall
|
|
|
681
544
|
- `showDiffSinceLastCall` - returns diff since last call (default: `true`, but `false` when `search` is provided). Pass `false` to get full HTML.
|
|
682
545
|
- `includeStyles` - keep style and class attributes (default: false)
|
|
683
546
|
|
|
684
|
-
|
|
685
|
-
The function cleans HTML for compact, readable output:
|
|
686
|
-
|
|
687
|
-
- **Removes tags**: script, style, link, meta, noscript, svg, head
|
|
688
|
-
- **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>`)
|
|
689
|
-
- **Removes empty elements**: Elements with no attributes and no content are removed
|
|
690
|
-
- **Truncates long values**: Attribute values >200 chars and text content >500 chars are truncated
|
|
691
|
-
|
|
692
|
-
**Attributes kept (summary):**
|
|
693
|
-
|
|
694
|
-
- Common semantic and ARIA attributes (e.g., `href`, `name`, `type`, `aria-*`)
|
|
695
|
-
- All `data-*` test attributes
|
|
696
|
-
- Frequently used test IDs and special attributes (e.g., `testid`, `qa`, `e2e`, `vimium-label`)
|
|
547
|
+
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-*`).
|
|
697
548
|
|
|
698
549
|
**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:
|
|
699
550
|
|
|
@@ -722,12 +573,6 @@ The main article content as plain text, with paragraphs preserved...
|
|
|
722
573
|
- `search` - string/regex to filter content (returns first 10 matching lines with 5 lines context)
|
|
723
574
|
- `showDiffSinceLastCall` - returns diff since last call (default: `true`, but `false` when `search` is provided). Pass `false` to get full content.
|
|
724
575
|
|
|
725
|
-
**Use cases:**
|
|
726
|
-
|
|
727
|
-
- Extract article text for LLM processing without HTML noise
|
|
728
|
-
- Get readable content from news sites, blogs, documentation
|
|
729
|
-
- Compare content changes after interactions
|
|
730
|
-
|
|
731
576
|
**waitForPageLoad** - smart load detection that ignores analytics/ads:
|
|
732
577
|
|
|
733
578
|
```js
|
|
@@ -808,94 +653,51 @@ Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkbo
|
|
|
808
653
|
|
|
809
654
|
**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`.
|
|
810
655
|
|
|
811
|
-
**recording.start / recording.stop** - record the page as a video at native FPS (30-60fps). Uses `chrome.tabCapture`
|
|
812
|
-
|
|
813
|
-
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.
|
|
814
|
-
|
|
815
|
-
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.
|
|
656
|
+
**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`).
|
|
816
657
|
|
|
817
|
-
|
|
658
|
+
For demos, use interaction methods (`locator.click()`, `page.mouse.move()`) instead of `goto()` to show realistic cursor motion.
|
|
818
659
|
|
|
819
660
|
```js
|
|
820
|
-
// Start recording - outputPath must be specified upfront
|
|
821
661
|
await recording.start({
|
|
822
662
|
page: state.page,
|
|
823
663
|
outputPath: './recording.mp4',
|
|
824
|
-
frameRate: 30, // default
|
|
825
|
-
audio: false, // default
|
|
826
|
-
videoBitsPerSecond: 2500000,
|
|
664
|
+
frameRate: 30, // default
|
|
665
|
+
audio: false, // default (tab audio)
|
|
666
|
+
videoBitsPerSecond: 2500000,
|
|
667
|
+
aspectRatio: { width: 16, height: 9 }, // default, set null to skip
|
|
668
|
+
maxDurationMs: 15 * 60 * 1000, // default, set 0 to disable
|
|
827
669
|
})
|
|
828
670
|
|
|
829
|
-
//
|
|
671
|
+
// Recording survives navigation
|
|
830
672
|
await state.page.click('a')
|
|
831
673
|
await state.page.waitForLoadState('domcontentloaded')
|
|
832
|
-
await state.page.goBack()
|
|
833
674
|
|
|
834
|
-
// Stop
|
|
835
|
-
|
|
836
|
-
console.log(`Saved ${size} bytes, duration: ${duration}ms`)
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
Additional recording utilities:
|
|
840
|
-
|
|
841
|
-
```js
|
|
842
|
-
// Check if recording is active
|
|
843
|
-
const { isRecording, startedAt } = await recording.isRecording({ page: state.page })
|
|
675
|
+
// Stop — save full result including executionTimestamps for createDemoVideo
|
|
676
|
+
state.recordingResult = await recording.stop({ page: state.page })
|
|
844
677
|
|
|
845
|
-
//
|
|
846
|
-
await recording.cancel({ page: state.page })
|
|
678
|
+
// Other: recording.isRecording({ page }), recording.cancel({ page })
|
|
847
679
|
```
|
|
848
680
|
|
|
849
|
-
**ghostCursor.show / ghostCursor.hide** -
|
|
681
|
+
**ghostCursor.show / ghostCursor.hide** - show/hide cursor overlay for screenshots and demos:
|
|
850
682
|
|
|
851
683
|
```js
|
|
852
|
-
|
|
853
|
-
await ghostCursor.show({ page: state.page })
|
|
854
|
-
|
|
855
|
-
// Optional styles: 'minimal' (default triangular pointer), 'dot', 'screenstudio'
|
|
856
|
-
await ghostCursor.show({ page: state.page, style: 'minimal' })
|
|
857
|
-
|
|
858
|
-
// Hide cursor overlay
|
|
684
|
+
await ghostCursor.show({ page: state.page, style: 'minimal' }) // 'minimal', 'dot', 'screenstudio'
|
|
859
685
|
await ghostCursor.hide({ page: state.page })
|
|
860
686
|
```
|
|
861
687
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
**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.
|
|
865
|
-
|
|
866
|
-
**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.
|
|
867
|
-
|
|
868
|
-
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.
|
|
869
|
-
|
|
870
|
-
A 1-second buffer is preserved around each interaction so viewers see context before and after each action.
|
|
871
|
-
|
|
872
|
-
Requires `ffmpeg` and `ffprobe` installed on the system.
|
|
873
|
-
|
|
874
|
-
**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.
|
|
875
|
-
|
|
876
|
-
```js
|
|
877
|
-
// Start recording
|
|
878
|
-
await recording.start({ page: state.page, outputPath: './recording.mp4' })
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
```js
|
|
882
|
-
// ... multiple execute() calls with browser interactions ...
|
|
883
|
-
// Each call's timing is tracked automatically while recording is active
|
|
884
|
-
```
|
|
688
|
+
**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.
|
|
885
689
|
|
|
886
690
|
```js
|
|
887
|
-
//
|
|
888
|
-
|
|
691
|
+
// After recording.stop(), save full result to state (executionTimestamps powers idle detection)
|
|
692
|
+
state.recordingResult = await recording.stop({ page: state.page })
|
|
889
693
|
|
|
890
|
-
//
|
|
694
|
+
// In a SEPARATE execute call with --timeout 120000:
|
|
891
695
|
const demoPath = await createDemoVideo({
|
|
892
|
-
recordingPath: recordingResult.path,
|
|
893
|
-
durationMs: recordingResult.duration,
|
|
894
|
-
executionTimestamps: recordingResult.executionTimestamps,
|
|
895
|
-
speed:
|
|
896
|
-
// outputFile: './demo.mp4', // optional, defaults to recording-demo.mp4
|
|
696
|
+
recordingPath: state.recordingResult.path,
|
|
697
|
+
durationMs: state.recordingResult.duration,
|
|
698
|
+
executionTimestamps: state.recordingResult.executionTimestamps,
|
|
699
|
+
speed: 6, // default 6x for idle sections
|
|
897
700
|
})
|
|
898
|
-
console.log('Demo video:', demoPath)
|
|
899
701
|
```
|
|
900
702
|
|
|
901
703
|
## pinned elements
|
|
@@ -995,60 +797,7 @@ console.log(data)
|
|
|
995
797
|
|
|
996
798
|
Clean up listeners when done: `state.page.removeAllListeners('request'); state.page.removeAllListeners('response');`
|
|
997
799
|
|
|
998
|
-
##
|
|
999
|
-
|
|
1000
|
-
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:
|
|
1001
|
-
|
|
1002
|
-
**1. Console logs** — use `getLatestLogs` to check for errors:
|
|
1003
|
-
|
|
1004
|
-
```js
|
|
1005
|
-
const errors = await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
|
|
1006
|
-
const appLogs = await getLatestLogs({ page: state.page, search: /myComponent|state/i })
|
|
1007
|
-
```
|
|
1008
|
-
|
|
1009
|
-
**2. DOM inspection via evaluate** — check content directly without screenshots:
|
|
1010
|
-
|
|
1011
|
-
```js
|
|
1012
|
-
const info = await state.page.evaluate(() => {
|
|
1013
|
-
const msgs = document.querySelectorAll('.message')
|
|
1014
|
-
return Array.from(msgs).map((m) => ({
|
|
1015
|
-
text: m.textContent?.slice(0, 200),
|
|
1016
|
-
visible: m.offsetHeight > 0,
|
|
1017
|
-
}))
|
|
1018
|
-
})
|
|
1019
|
-
console.log(JSON.stringify(info, null, 2))
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
**3. Combine snapshot + logs for full picture:**
|
|
1023
|
-
|
|
1024
|
-
```js
|
|
1025
|
-
await state.page.keyboard.press('Enter')
|
|
1026
|
-
await state.page.waitForTimeout(2000)
|
|
1027
|
-
|
|
1028
|
-
const snap = await snapshot({ page: state.page, search: /dialog|error|message/ })
|
|
1029
|
-
const logs = await getLatestLogs({ page: state.page, search: /error/i, count: 10 })
|
|
1030
|
-
console.log('UI:', snap)
|
|
1031
|
-
console.log('Logs:', logs)
|
|
1032
|
-
```
|
|
1033
|
-
|
|
1034
|
-
## capabilities
|
|
1035
|
-
|
|
1036
|
-
Examples of what playwriter can do:
|
|
1037
|
-
|
|
1038
|
-
- Monitor console logs while user reproduces a bug
|
|
1039
|
-
- Intercept network requests to reverse-engineer APIs and build SDKs
|
|
1040
|
-
- Scrape data by replaying paginated API calls instead of scrolling DOM
|
|
1041
|
-
- Get accessibility snapshot to find elements, then automate interactions
|
|
1042
|
-
- Use visual screenshots to understand complex layouts like image grids, dashboards, or maps
|
|
1043
|
-
- Debug issues by collecting logs and controlling the page simultaneously
|
|
1044
|
-
- Handle popups, downloads, iframes, and dialog boxes
|
|
1045
|
-
- Record videos of browser sessions that survive page navigation
|
|
1046
|
-
|
|
1047
|
-
## computer use
|
|
1048
|
-
|
|
1049
|
-
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.
|
|
1050
|
-
|
|
1051
|
-
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.
|
|
800
|
+
## computer use (low-level mouse/keyboard)
|
|
1052
801
|
|
|
1053
802
|
### clicking
|
|
1054
803
|
|
|
@@ -1151,19 +900,4 @@ Prefer locator-based actions over coordinates — locators are stable across scr
|
|
|
1151
900
|
|
|
1152
901
|
## Ghost Browser integration
|
|
1153
902
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
```js
|
|
1157
|
-
// List identities and open tabs in different ones
|
|
1158
|
-
const identities = await chrome.projects.getIdentitiesList()
|
|
1159
|
-
await chrome.ghostPublicAPI.openTab({ url: 'https://reddit.com', identity: identities[0].id })
|
|
1160
|
-
|
|
1161
|
-
// Assign proxies per tab or identity
|
|
1162
|
-
const proxies = await chrome.ghostProxies.getList()
|
|
1163
|
-
await chrome.ghostProxies.setTabProxy(tabId, proxies[0].id)
|
|
1164
|
-
```
|
|
1165
|
-
|
|
1166
|
-
For complete API reference with all methods, types, and examples, read:
|
|
1167
|
-
`extension/src/ghost-browser-api.d.ts`
|
|
1168
|
-
|
|
1169
|
-
Note: Only works in Ghost Browser. In regular Chrome, calls fail with "not available".
|
|
903
|
+
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.
|
package/dist/readability.js
CHANGED
|
@@ -4,15 +4,29 @@
|
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
function __accessProp(key) {
|
|
8
|
+
return this[key];
|
|
9
|
+
}
|
|
10
|
+
var __toESMCache_node;
|
|
11
|
+
var __toESMCache_esm;
|
|
7
12
|
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
+
var canCache = mod != null && typeof mod === "object";
|
|
14
|
+
if (canCache) {
|
|
15
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
+
var cached = cache.get(mod);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
8
20
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
21
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
22
|
for (let key of __getOwnPropNames(mod))
|
|
11
23
|
if (!__hasOwnProp.call(to, key))
|
|
12
24
|
__defProp(to, key, {
|
|
13
|
-
get: (
|
|
25
|
+
get: __accessProp.bind(mod, key),
|
|
14
26
|
enumerable: true
|
|
15
27
|
});
|
|
28
|
+
if (canCache)
|
|
29
|
+
cache.set(mod, to);
|
|
16
30
|
return to;
|
|
17
31
|
};
|
|
18
32
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
@@ -1654,7 +1668,7 @@
|
|
|
1654
1668
|
};
|
|
1655
1669
|
});
|
|
1656
1670
|
|
|
1657
|
-
// dist/_readability-entry-
|
|
1671
|
+
// dist/_readability-entry-64982-1773397884858.js
|
|
1658
1672
|
var import_readability = __toESM(require_readability(), 1);
|
|
1659
1673
|
globalThis.__readability = { Readability: import_readability.Readability, isProbablyReaderable: import_readability.isProbablyReaderable };
|
|
1660
1674
|
})();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recording-ghost-cursor.d.ts","sourceRoot":"","sources":["../src/recording-ghost-cursor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AACnE,OAAO,
|
|
1
|
+
{"version":3,"file":"recording-ghost-cursor.d.ts","sourceRoot":"","sources":["../src/recording-ghost-cursor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AACnE,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,mBAAmB,CAAA;AAE1B,UAAU,0BAA0B;IAClC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CACpC;AAED,UAAU,sBAAsB;IAC9B,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,8BAA8B;IACzC,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAA6C;IACvF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAqC;IAC5E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAEvC,OAAO,EAAE;QAAE,MAAM,EAAE,0BAA0B,CAAA;KAAE;IAI3D,0BAA0B,CAAC,OAAO,EAAE;QAClC,OAAO,EAAE,cAAc,CAAA;QACvB,WAAW,EAAE,IAAI,CAAA;QACjB,MAAM,CAAC,EAAE,sBAAsB,CAAA;KAChC,GAAG,IAAI;IAoBF,kBAAkB,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAmC1D,mBAAmB,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAa3D,IAAI,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,aAAa,CAAC,EAAE,wBAAwB,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtF,IAAI,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAInD"}
|