playwriter 0.0.105 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/bippy.js +5 -5
  2. package/dist/cdp-relay.d.ts.map +1 -1
  3. package/dist/cdp-relay.js +17 -5
  4. package/dist/cdp-relay.js.map +1 -1
  5. package/dist/cli-help.test.d.ts +2 -0
  6. package/dist/cli-help.test.d.ts.map +1 -0
  7. package/dist/cli-help.test.js +53 -0
  8. package/dist/cli-help.test.js.map +1 -0
  9. package/dist/cli.js +74 -25
  10. package/dist/cli.js.map +1 -1
  11. package/dist/executor.d.ts +1 -0
  12. package/dist/executor.d.ts.map +1 -1
  13. package/dist/executor.js +55 -12
  14. package/dist/executor.js.map +1 -1
  15. package/dist/extension/background.js +675 -27
  16. package/dist/extension/manifest.json +1 -1
  17. package/dist/ghost-cursor-client.js +170 -83
  18. package/dist/{recording-ghost-cursor.d.ts → ghost-cursor-controller.d.ts} +15 -10
  19. package/dist/ghost-cursor-controller.d.ts.map +1 -0
  20. package/dist/ghost-cursor-controller.js +98 -0
  21. package/dist/ghost-cursor-controller.js.map +1 -0
  22. package/dist/ghost-cursor.d.ts.map +1 -1
  23. package/dist/ghost-cursor.js +42 -26
  24. package/dist/ghost-cursor.js.map +1 -1
  25. package/dist/mcp.d.ts.map +1 -1
  26. package/dist/mcp.js +6 -1
  27. package/dist/mcp.js.map +1 -1
  28. package/dist/on-mouse-action.test.js +25 -0
  29. package/dist/on-mouse-action.test.js.map +1 -1
  30. package/dist/performance-examples.d.ts +5 -0
  31. package/dist/performance-examples.d.ts.map +1 -0
  32. package/dist/performance-examples.js +112 -0
  33. package/dist/performance-examples.js.map +1 -0
  34. package/dist/performance-profiling.md +417 -0
  35. package/dist/prompt.md +22 -8
  36. package/dist/react-source.d.ts +44 -0
  37. package/dist/react-source.d.ts.map +1 -1
  38. package/dist/react-source.js +207 -20
  39. package/dist/react-source.js.map +1 -1
  40. package/dist/readability.js +1 -1
  41. package/dist/relay-core.test.d.ts.map +1 -1
  42. package/dist/relay-core.test.js +101 -1
  43. package/dist/relay-core.test.js.map +1 -1
  44. package/dist/relay-session.test.js +34 -6
  45. package/dist/relay-session.test.js.map +1 -1
  46. package/dist/screen-recording.d.ts +2 -2
  47. package/dist/screen-recording.d.ts.map +1 -1
  48. package/dist/screen-recording.js +19 -7
  49. package/dist/screen-recording.js.map +1 -1
  50. package/dist/selector-generator.js +1 -1
  51. package/package.json +7 -7
  52. package/src/aria-snapshots/github-interactive.txt +5 -3
  53. package/src/aria-snapshots/github-raw.txt +8 -5
  54. package/src/aria-snapshots/hackernews-interactive.txt +241 -238
  55. package/src/aria-snapshots/hackernews-raw.txt +269 -265
  56. package/src/aria-snapshots/prosemirror-interactive.txt +3 -1
  57. package/src/aria-snapshots/prosemirror-raw.txt +4 -1
  58. package/src/assets/aria-labels-hacker-news.png +0 -0
  59. package/src/assets/aria-labels-old-reddit.png +0 -0
  60. package/src/cdp-relay.ts +17 -5
  61. package/src/cli-help.test.ts +63 -0
  62. package/src/cli.ts +80 -28
  63. package/src/executor.ts +65 -15
  64. package/src/ghost-cursor-client.ts +221 -96
  65. package/src/{recording-ghost-cursor.ts → ghost-cursor-controller.ts} +50 -34
  66. package/src/ghost-cursor.ts +54 -41
  67. package/src/mcp.ts +6 -1
  68. package/src/on-mouse-action.test.ts +30 -0
  69. package/src/performance-examples.ts +186 -0
  70. package/src/react-source.ts +310 -24
  71. package/src/relay-core.test.ts +117 -0
  72. package/src/relay-session.test.ts +36 -10
  73. package/src/screen-recording.ts +23 -10
  74. package/src/skill.md +33 -9
  75. package/src/snapshots/shadcn-ui-accessibility-full.md +6 -3
  76. package/src/snapshots/shadcn-ui-accessibility-interactive.md +2 -0
  77. package/dist/recording-ghost-cursor.d.ts.map +0 -1
  78. package/dist/recording-ghost-cursor.js +0 -79
  79. package/dist/recording-ghost-cursor.js.map +0 -1
@@ -0,0 +1,417 @@
1
+ # Profile Website Performance with Playwriter
2
+
3
+ Playwriter can profile a real website in your own Chrome using **CDP**, **Navigation Timing**,
4
+ and **PerformanceObserver**.
5
+
6
+ Use it to answer four practical questions quickly:
7
+
8
+ 1. **Did the page render fast enough?**
9
+ 2. **What requests cost the most bytes?**
10
+ 3. **What blocked first paint or LCP?**
11
+ 4. **What blocked interactivity?**
12
+
13
+ ## What to measure
14
+
15
+ | Metric | Good | Needs work | Usually means |
16
+ | --- | --- | --- | --- |
17
+ | **TTFB** | under **800ms** | over **1.2s** | slow server or cache miss |
18
+ | **FCP** | under **1.8s** | over **3s** | content appears late |
19
+ | **LCP** | under **2.5s** | over **4s** | hero image, font, CSS, server, or JS delay |
20
+ | **CLS** | under **0.1** | over **0.25** | unstable layout |
21
+ | **Long task** | under **50ms** | over **100ms** | main thread blocked by JS |
22
+ | **JS transfer** | under **250KB** | over **500KB** | too much hydration or client code |
23
+ | **Font / media transfer** | context dependent | large above-the-fold assets | fonts, posters, videos, hero images |
24
+
25
+ ## What usually blocks what
26
+
27
+ - **First paint / FCP** is usually gated by **TTFB**, critical HTML, critical CSS, and above-the-fold fonts/images.
28
+ - **LCP** is usually gated by the **largest hero asset**. Common causes: hero image, poster image, custom font, render-blocking CSS, or slow server response.
29
+ - **Interactivity** is usually gated by **long tasks**. Common causes: too much JS on startup, hydration, or a large framework chunk.
30
+ - **Load event** often stays late because of **non-critical assets** like videos, analytics, background images, and delayed client bundles.
31
+
32
+ ## Quick commands
33
+
34
+ Create a session and open a page:
35
+
36
+ ```bash
37
+ playwriter session new
38
+ playwriter -s 1 -e 'state.page = context.pages().find((p) => p.url() === "about:blank") ?? (await context.newPage()); await state.page.goto("https://example.com", { waitUntil: "domcontentloaded" })'
39
+ ```
40
+
41
+ Collect a concise vitals report:
42
+
43
+ ```bash
44
+ playwriter -s 1 -e "$(cat <<'EOF'
45
+ await state.page.evaluate(() => {
46
+ const metrics = { paints: {}, lcp: 0, cls: 0 }
47
+ globalThis.__pwMetrics = metrics
48
+
49
+ new PerformanceObserver((list) => {
50
+ for (const entry of list.getEntries()) {
51
+ metrics.paints[entry.name] = entry.startTime
52
+ }
53
+ }).observe({ type: 'paint', buffered: true })
54
+
55
+ new PerformanceObserver((list) => {
56
+ const lastEntry = list.getEntries().at(-1)
57
+ if (lastEntry) {
58
+ metrics.lcp = lastEntry.startTime
59
+ }
60
+ }).observe({ type: 'largest-contentful-paint', buffered: true })
61
+
62
+ new PerformanceObserver((list) => {
63
+ for (const entry of list.getEntries()) {
64
+ if (!entry.hadRecentInput) {
65
+ metrics.cls += entry.value
66
+ }
67
+ }
68
+ }).observe({ type: 'layout-shift', buffered: true })
69
+ })
70
+
71
+ await state.page.reload({ waitUntil: 'domcontentloaded' })
72
+ await waitForPageLoad({ page: state.page, timeout: 10000 })
73
+ await state.page.waitForTimeout(3000)
74
+
75
+ const report = await state.page.evaluate(() => {
76
+ const nav = performance.getEntriesByType('navigation')[0]
77
+ const metrics = globalThis.__pwMetrics
78
+ return {
79
+ ttfb: nav?.responseStart || 0,
80
+ domContentLoaded: nav?.domContentLoadedEventEnd || 0,
81
+ load: nav?.loadEventEnd || 0,
82
+ fcp: metrics?.paints['first-contentful-paint'] || 0,
83
+ lcp: metrics?.lcp || 0,
84
+ cls: metrics?.cls || 0,
85
+ }
86
+ })
87
+
88
+ console.log(JSON.stringify(report, null, 2))
89
+ EOF
90
+ )"
91
+ ```
92
+
93
+ List the heaviest requests with CDP:
94
+
95
+ ```bash
96
+ playwriter -s 1 -e "$(cat <<'EOF'
97
+ const cdp = await getCDPSession({ page: state.page })
98
+ await cdp.send('Network.enable')
99
+ await cdp.send('Network.setCacheDisabled', { cacheDisabled: true })
100
+
101
+ const responses = new Map()
102
+ const finished = new Map()
103
+
104
+ cdp.on('Network.responseReceived', (event) => {
105
+ responses.set(event.requestId, {
106
+ url: event.response.url,
107
+ mimeType: event.response.mimeType,
108
+ })
109
+ })
110
+
111
+ cdp.on('Network.loadingFinished', (event) => {
112
+ finished.set(event.requestId, event.encodedDataLength)
113
+ })
114
+
115
+ await state.page.reload({ waitUntil: 'domcontentloaded' })
116
+ await waitForPageLoad({ page: state.page, timeout: 10000 })
117
+ await state.page.waitForTimeout(3000)
118
+
119
+ const largest = [...responses.entries()]
120
+ .map(([requestId, response]) => ({
121
+ url: response.url,
122
+ mimeType: response.mimeType,
123
+ bytes: finished.get(requestId) || 0,
124
+ }))
125
+ .sort((a, b) => b.bytes - a.bytes)
126
+ .slice(0, 10)
127
+
128
+ console.log(JSON.stringify(largest, null, 2))
129
+ EOF
130
+ )"
131
+ ```
132
+
133
+ Check interactivity blockers:
134
+
135
+ ```bash
136
+ playwriter -s 1 -e "$(cat <<'EOF'
137
+ await state.page.evaluate(() => {
138
+ globalThis.__pwLongTasks = []
139
+ globalThis.__pwEvents = []
140
+
141
+ new PerformanceObserver((list) => {
142
+ globalThis.__pwLongTasks.push(
143
+ ...list.getEntries().map((entry) => ({ startTime: entry.startTime, duration: entry.duration })),
144
+ )
145
+ }).observe({ type: 'longtask', buffered: true })
146
+
147
+ new PerformanceObserver((list) => {
148
+ globalThis.__pwEvents.push(
149
+ ...list.getEntries().map((entry) => ({
150
+ name: entry.name,
151
+ duration: entry.duration,
152
+ interactionId: entry.interactionId,
153
+ })),
154
+ )
155
+ }).observe({ type: 'event', buffered: true, durationThreshold: 16 })
156
+ })
157
+
158
+ await state.page.getByRole('button').first().click()
159
+ await state.page.waitForTimeout(1000)
160
+
161
+ const report = await state.page.evaluate(() => ({
162
+ longTasks: globalThis.__pwLongTasks.filter((entry) => entry.duration >= 50),
163
+ interactions: globalThis.__pwEvents.filter((entry) => entry.interactionId !== 0),
164
+ }))
165
+
166
+ console.log(JSON.stringify(report, null, 2))
167
+ EOF
168
+ )"
169
+ ```
170
+
171
+ ## How to read the results
172
+
173
+ **Fast render, heavy payload**
174
+
175
+ - If **FCP** and **LCP** are good but total bytes are huge, the page probably **looks fast on desktop** but wastes bandwidth on mobile.
176
+ - This often happens with **hero videos**, large poster images, or custom fonts.
177
+
178
+ **Slow first paint**
179
+
180
+ - If **TTFB** is high, fix **server latency** or caching first.
181
+ - If **TTFB** is fine but **FCP** is slow, inspect critical CSS, fonts, and above-the-fold images.
182
+
183
+ **Slow interactivity**
184
+
185
+ - If you see **long tasks over 50ms**, startup JS is the first suspect.
186
+ - Look for large client bundles, hydration-heavy UI, and event handlers doing too much work.
187
+
188
+ **Need deeper CPU answers**
189
+
190
+ - If vitals and request sizes are not enough, record a **`.cpuprofile`** with Playwriter's raw **`Profiler.*`** CDP commands and inspect it with **[profano](https://github.com/remorses/profano)**.
191
+ - A good place to keep your reusable profiling snippets is your dots repo, for example **`~/.config/opencode/`**.
192
+
193
+ **Good load event is not enough**
194
+
195
+ - A page can have a decent `load` time and still feel slow if **LCP** or **long tasks** are bad.
196
+ - Prefer **TTFB + FCP + LCP + CLS + long tasks** over the load event alone.
197
+
198
+ ## Performance checklist
199
+
200
+ **If TTFB is bad**
201
+
202
+ - cache HTML closer to users
203
+ - reduce origin work before response
204
+ - avoid expensive server-side data fetching on the critical route
205
+
206
+ **If FCP or LCP is bad**
207
+
208
+ - trim or defer render-blocking CSS
209
+ - avoid large above-the-fold fonts and images
210
+ - preload only truly critical assets
211
+ - compress hero media harder
212
+
213
+ **If interactivity is bad**
214
+
215
+ - reduce startup JS
216
+ - split large client bundles
217
+ - avoid hydrating UI that is not immediately interactive
218
+ - move optional widgets behind user action or idle time
219
+ - if the culprit is still unclear, switch from vitals to a CPU profile and inspect hot functions with **profano**
220
+
221
+ **If bytes are bad but vitals look good**
222
+
223
+ - optimize for slower devices anyway
224
+ - background videos are the first thing to cut
225
+ - subset fonts and trim non-critical client features
226
+
227
+ ## Examples
228
+
229
+ ```ts
230
+ // Example snippets for profiling website performance with Playwriter and CDP.
231
+
232
+ import { console, getCDPSession, page } from './debugger-examples-types.js'
233
+
234
+ type PerfMetrics = {
235
+ paints: Record<string, number>
236
+ lcp: number
237
+ cls: number
238
+ }
239
+
240
+ type ObservedPerfEntry = {
241
+ name: string
242
+ startTime: number
243
+ duration: number
244
+ hadRecentInput?: boolean
245
+ value?: number
246
+ interactionId?: number
247
+ }
248
+
249
+ type NavigationTimingEntry = {
250
+ responseStart: number
251
+ domContentLoadedEventEnd: number
252
+ loadEventEnd: number
253
+ }
254
+
255
+ type LongTaskEntry = {
256
+ startTime: number
257
+ duration: number
258
+ }
259
+
260
+ type EventTimingEntry = {
261
+ name: string
262
+ duration: number
263
+ interactionId: number
264
+ }
265
+
266
+ // Example: Collect navigation timing and basic web vitals from the current page
267
+ async function collectWebVitals() {
268
+ await page.evaluate(() => {
269
+ const metrics: PerfMetrics = {
270
+ paints: {},
271
+ lcp: 0,
272
+ cls: 0,
273
+ }
274
+
275
+ const perfGlobal = globalThis as typeof globalThis & {
276
+ __pwPerfMetrics?: PerfMetrics
277
+ }
278
+
279
+ perfGlobal.__pwPerfMetrics = metrics
280
+
281
+ new PerformanceObserver((list) => {
282
+ for (const entry of list.getEntries() as ObservedPerfEntry[]) {
283
+ metrics.paints[entry.name] = entry.startTime
284
+ }
285
+ }).observe({ type: 'paint', buffered: true } as never)
286
+
287
+ new PerformanceObserver((list) => {
288
+ const entries = list.getEntries() as ObservedPerfEntry[]
289
+ const lastEntry = entries[entries.length - 1]
290
+ if (!lastEntry) {
291
+ return
292
+ }
293
+ metrics.lcp = lastEntry.startTime
294
+ }).observe({ type: 'largest-contentful-paint', buffered: true } as never)
295
+
296
+ new PerformanceObserver((list) => {
297
+ for (const entry of list.getEntries() as ObservedPerfEntry[]) {
298
+ if (entry.hadRecentInput) {
299
+ continue
300
+ }
301
+ metrics.cls += entry.value || 0
302
+ }
303
+ }).observe({ type: 'layout-shift', buffered: true } as never)
304
+ })
305
+
306
+ await page.reload({ waitUntil: 'domcontentloaded' })
307
+
308
+ const report = await page.evaluate(() => {
309
+ const perfGlobal = globalThis as typeof globalThis & {
310
+ __pwPerfMetrics?: PerfMetrics
311
+ }
312
+ const nav = performance.getEntriesByType('navigation' as never)[0] as unknown as
313
+ | NavigationTimingEntry
314
+ | undefined
315
+ const metrics = perfGlobal.__pwPerfMetrics
316
+
317
+ return {
318
+ ttfb: nav?.responseStart || 0,
319
+ domContentLoaded: nav?.domContentLoadedEventEnd || 0,
320
+ load: nav?.loadEventEnd || 0,
321
+ fcp: metrics?.paints['first-contentful-paint'] || 0,
322
+ lcp: metrics?.lcp || 0,
323
+ cls: metrics?.cls || 0,
324
+ }
325
+ })
326
+
327
+ console.log(report)
328
+ }
329
+
330
+ // Example: Measure the biggest transferred requests with raw CDP network events
331
+ async function collectHeaviestRequests() {
332
+ const cdp = await getCDPSession({ page })
333
+ await cdp.send('Network.enable')
334
+ await cdp.send('Network.setCacheDisabled', { cacheDisabled: true })
335
+
336
+ const responses = new Map<string, { url: string; mimeType: string }>()
337
+ const finished = new Map<string, number>()
338
+
339
+ cdp.on('Network.responseReceived', (event) => {
340
+ responses.set(event.requestId, {
341
+ url: event.response.url,
342
+ mimeType: event.response.mimeType,
343
+ })
344
+ })
345
+
346
+ cdp.on('Network.loadingFinished', (event) => {
347
+ finished.set(event.requestId, event.encodedDataLength)
348
+ })
349
+
350
+ await page.reload({ waitUntil: 'domcontentloaded' })
351
+
352
+ const largest = [...responses.entries()]
353
+ .map(([requestId, response]) => {
354
+ return {
355
+ url: response.url,
356
+ mimeType: response.mimeType,
357
+ bytes: finished.get(requestId) || 0,
358
+ }
359
+ })
360
+ .sort((a, b) => b.bytes - a.bytes)
361
+ .slice(0, 10)
362
+
363
+ console.log(largest)
364
+ }
365
+
366
+ // Example: Check whether interactivity is blocked by long tasks or slow events
367
+ async function measureInteractivity() {
368
+ await page.evaluate(() => {
369
+ const perfGlobal = globalThis as typeof globalThis & {
370
+ __pwLongTasks?: LongTaskEntry[]
371
+ __pwEventTimings?: EventTimingEntry[]
372
+ }
373
+
374
+ perfGlobal.__pwLongTasks = []
375
+ perfGlobal.__pwEventTimings = []
376
+
377
+ new PerformanceObserver((list) => {
378
+ perfGlobal.__pwLongTasks?.push(
379
+ ...(list.getEntries() as ObservedPerfEntry[]).map((entry) => ({
380
+ startTime: entry.startTime,
381
+ duration: entry.duration,
382
+ })),
383
+ )
384
+ }).observe({ type: 'longtask', buffered: true } as never)
385
+
386
+ new PerformanceObserver((list) => {
387
+ perfGlobal.__pwEventTimings?.push(
388
+ ...(list.getEntries() as ObservedPerfEntry[]).map((entry) => ({
389
+ name: entry.name,
390
+ duration: entry.duration,
391
+ interactionId: entry.interactionId || 0,
392
+ })),
393
+ )
394
+ }).observe({ type: 'event', buffered: true, durationThreshold: 16 } as never)
395
+ })
396
+
397
+ const button = page.getByRole('button').first()
398
+ await button.click()
399
+
400
+ const report = await page.evaluate(() => {
401
+ const perfGlobal = globalThis as typeof globalThis & {
402
+ __pwLongTasks?: LongTaskEntry[]
403
+ __pwEventTimings?: EventTimingEntry[]
404
+ }
405
+
406
+ return {
407
+ longTasks: (perfGlobal.__pwLongTasks || []).filter((entry) => entry.duration >= 50),
408
+ events: (perfGlobal.__pwEventTimings || []).filter((entry) => entry.interactionId !== 0),
409
+ }
410
+ })
411
+
412
+ console.log(report)
413
+ }
414
+
415
+ export { collectWebVitals, collectHeaviestRequests, measureInteractivity }
416
+
417
+ ```
package/dist/prompt.md CHANGED
@@ -57,6 +57,7 @@ You can collaborate with the user - they can help with captchas, difficult eleme
57
57
  - **Wait for load**: use `state.page.waitForLoadState('domcontentloaded')` not `state.page.waitForEvent('load')` - waitForEvent times out if already loaded
58
58
  - **Minimize timeouts**: prefer proper waits (`waitForSelector`, `waitForPageLoad`) over `state.page.waitForTimeout()`. Short timeouts (1-2s) are acceptable for non-deterministic events like animations, tab opens, or async UI updates where no specific selector is available
59
59
  - **Snapshot before screenshot**: always use `snapshot()` first to understand page state (text-based, fast, cheap). Only use `screenshot` when you specifically need visual/spatial information. Never take a screenshot just to check if a page loaded or to read text content — snapshot gives you that instantly without burning image tokens
60
+ - **Always use absolute file paths for Playwright artifact APIs**: for `page.screenshot({ path })`, `locator.screenshot({ path })`, `elementHandle.screenshot({ path })`, `page.pdf({ path })`, `download.saveAs(path)`, and `video.saveAs(path)`, always pass an absolute path. Relative paths are resolved by Playwright client internals, not the sandboxed `fs`, so they may use the relay server cwd instead of your session cwd.
60
61
  - **Snapshot replaces page.evaluate() for inspection**: do NOT write `page.evaluate()` calls to manually query class names, bounding boxes, child counts, or visibility flags. `snapshot()` already shows every interactive element with its text, role, and a ready-to-use locator. If you catch yourself writing `document.querySelector` or `getBoundingClientRect` inside evaluate — stop and use `snapshot()` instead. Reserve `page.evaluate()` for actions that modify page state (e.g., `localStorage.clear()`, scroll manipulation) or extract non-DOM data (e.g., `window.__CONFIG__`)
61
62
 
62
63
  ## interaction feedback loop
@@ -449,7 +450,7 @@ Instead, use simpler alternatives (single download via `a.click()`, store data i
449
450
 
450
451
  ```js
451
452
  const [download] = await Promise.all([state.page.waitForEvent('download'), state.page.click('button.download')])
452
- await download.saveAs(`/tmp/${download.suggestedFilename()}`)
453
+ await download.saveAs(`/absolute/path/${download.suggestedFilename()}`)
453
454
  ```
454
455
 
455
456
  **iFrames** - two approaches depending on what you need:
@@ -602,6 +603,19 @@ const source = await getReactSource({ locator: state.page.locator('[data-testid=
602
603
  // => { fileName, lineNumber, columnNumber, componentName }
603
604
  ```
604
605
 
606
+ **getReactComponentInfo** - get best-effort React component info for an element. Returns `null` for non-React elements and never throws just because an element was not rendered by React. Source locations are usually only available in React dev builds. Props are sanitized and truncated so functions, DOM nodes, circular refs, and huge objects do not flood the output.
607
+
608
+ ```js
609
+ const info = await getReactComponentInfo({ locator: state.page.locator('[data-testid="submit-btn"]') })
610
+ // => { componentName, source, hierarchy, props } | null
611
+ ```
612
+
613
+ **inspectPinnedElement** - inspect a Playwriter pinned element and print the element `outerHTML` plus React component info when available. Used by the in-page toolbar and right-click copy flow.
614
+
615
+ ```js
616
+ await inspectPinnedElement('https://example.com', 'globalThis.playwriterPinnedElem1')
617
+ ```
618
+
605
619
  **getStylesForLocator** - inspect CSS styles applied to an element, like browser DevTools "Styles" panel. Useful for debugging styling issues, finding where a CSS property is defined (file:line), and checking inherited styles. Returns selector, source location, and declarations for each matching rule. ALWAYS fetch `https://playwriter.dev/resources/styles-api.md` first with curl or webfetch tool.
606
620
 
607
621
  ```js
@@ -654,7 +668,7 @@ await screenshotWithAccessibilityLabels({ page: state.page })
654
668
 
655
669
  Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs.
656
670
 
657
- **resizeImageForAgent** - shrink an image so it consumes fewer tokens when read back into context. The resized image is automatically included in the response (visible to the LLM). `await resizeImageForAgent({ input: './screenshot.png' })`. Also accepts `width`, `height`, `maxDimension`, `quality`, `format` (default: `'png'`), `output`. Alias: `resizeImage`.
671
+ **resizeImageForAgent** - shrink an image so it consumes fewer tokens when read back into context. The resized image is automatically included in the response (visible to the LLM). `await resizeImageForAgent({ input: '/absolute/path/to/screenshot.png' })`. Also accepts `width`, `height`, `maxDimension`, `quality`, `format` (default: `'png'`), `output`. Alias: `resizeImage`.
658
672
 
659
673
  **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`).
660
674
 
@@ -663,7 +677,7 @@ For demos, use interaction methods (`locator.click()`, `page.mouse.move()`) inst
663
677
  ```js
664
678
  await recording.start({
665
679
  page: state.page,
666
- outputPath: './recording.mp4',
680
+ outputPath: '/absolute/path/to/recording.mp4',
667
681
  frameRate: 30, // default
668
682
  audio: false, // default (tab audio)
669
683
  videoBitsPerSecond: 2500000,
@@ -681,11 +695,11 @@ state.recordingResult = await recording.stop({ page: state.page })
681
695
  // Other: recording.isRecording({ page }), recording.cancel({ page })
682
696
  ```
683
697
 
684
- **ghostCursor.show / ghostCursor.hide** - show/hide cursor overlay for screenshots and demos:
698
+ **ghostCursor.show / ghostCursor.hide** - the ghost cursor overlay is always on: the extension injects it on every Playwriter-attached tab and it stays visible at the last spot Playwright clicked or moved. These methods only matter if you want to change the cursor style or temporarily hide it:
685
699
 
686
700
  ```js
687
- await ghostCursor.show({ page: state.page, style: 'minimal' }) // 'minimal', 'dot', 'screenstudio'
688
- await ghostCursor.hide({ page: state.page })
701
+ await ghostCursor.show({ page: state.page, style: 'screenstudio' }) // 'minimal' (default), 'dot', 'screenstudio'
702
+ await ghostCursor.hide({ page: state.page }) // hide until next show() or hard navigation
689
703
  ```
690
704
 
691
705
  **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.
@@ -717,7 +731,7 @@ await el.click()
717
731
  Always use `scale: 'css'` to avoid 2-4x larger images on high-DPI displays:
718
732
 
719
733
  ```js
720
- await state.page.screenshot({ path: 'shot.png', scale: 'css' })
734
+ await state.page.screenshot({ path: '/absolute/path/to/shot.png', scale: 'css' })
721
735
  ```
722
736
 
723
737
  If you want to read back the image file into context, resize it first so it consumes fewer tokens:
@@ -896,7 +910,7 @@ await state.page.setViewportSize({ width: 1280, height: 720 })
896
910
  ### region screenshot (zoom equivalent)
897
911
 
898
912
  ```js
899
- await state.page.screenshot({ path: 'region.png', scale: 'css', clip: { x: 100, y: 200, width: 400, height: 300 } })
913
+ await state.page.screenshot({ path: '/absolute/path/to/region.png', scale: 'css', clip: { x: 100, y: 200, width: 400, height: 300 } })
900
914
  ```
901
915
 
902
916
  Prefer locator-based actions over coordinates — locators are stable across scroll/resize, auto-wait for elements, and don't require screenshot round-trips that burn ~800 image tokens per cycle.
@@ -6,8 +6,52 @@ export interface ReactSourceLocation {
6
6
  columnNumber: number | null;
7
7
  componentName: string | null;
8
8
  }
9
+ export type ReactSerializedProp = string | number | boolean | null | ReactSerializedProp[] | {
10
+ [key: string]: ReactSerializedProp;
11
+ };
12
+ export interface ReactComponentHierarchyItem {
13
+ componentName: string | null;
14
+ source: Omit<ReactSourceLocation, 'componentName'> | null;
15
+ props: ReactSerializedProp;
16
+ }
17
+ export interface ReactComponentInfo {
18
+ componentName: string | null;
19
+ source: Omit<ReactSourceLocation, 'componentName'> | null;
20
+ hierarchy: ReactComponentHierarchyItem[];
21
+ props: ReactSerializedProp;
22
+ }
23
+ type ReactInspectableValue = string | number | boolean | bigint | symbol | null | undefined | object | ((...args: never[]) => ReactInspectableValue);
24
+ type BrowserElement = object;
25
+ interface BippySourceFrame {
26
+ fileName?: string | null;
27
+ lineNumber?: number | null;
28
+ columnNumber?: number | null;
29
+ functionName?: string | null;
30
+ }
31
+ interface BippyFiber {
32
+ return?: BippyFiber | null;
33
+ type?: ReactInspectableValue;
34
+ memoizedProps?: ReactInspectableValue;
35
+ }
36
+ interface BippyRuntime {
37
+ getFiberFromHostInstance(el: BrowserElement): BippyFiber | null;
38
+ getSource(fiber: BippyFiber): Promise<BippySourceFrame | null>;
39
+ getOwnerStack(fiber: BippyFiber): Promise<BippySourceFrame[]>;
40
+ getDisplayName(type: ReactInspectableValue): string | null;
41
+ isCompositeFiber(fiber: BippyFiber): boolean;
42
+ normalizeFileName(fileName: string): string;
43
+ isSourceFile(fileName: string): boolean;
44
+ }
45
+ declare global {
46
+ var __bippy: BippyRuntime | undefined;
47
+ }
9
48
  export declare function getReactSource({ locator, cdp: cdpSession, }: {
10
49
  locator: Locator | ElementHandle;
11
50
  cdp: ICDPSession;
12
51
  }): Promise<ReactSourceLocation | null>;
52
+ export declare function getReactComponentInfo({ locator, cdp: cdpSession, }: {
53
+ locator: Locator | ElementHandle;
54
+ cdp: ICDPSession;
55
+ }): Promise<ReactComponentInfo | null>;
56
+ export {};
13
57
  //# sourceMappingURL=react-source.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react-source.d.ts","sourceRoot":"","sources":["../src/react-source.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAQ,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAEnD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAcD,wBAAsB,cAAc,CAAC,EACnC,OAAO,EACP,GAAG,EAAE,UAAU,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,GAAG,aAAa,CAAA;IAChC,GAAG,EAAE,WAAW,CAAA;CACjB,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CA6DtC"}
1
+ {"version":3,"file":"react-source.d.ts","sourceRoot":"","sources":["../src/react-source.ts"],"names":[],"mappings":"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"}