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
@@ -56,55 +56,68 @@ export async function enableGhostCursor(options: {
56
56
  page: Page
57
57
  cursorOptions?: GhostCursorClientOptions
58
58
  }): Promise<void> {
59
- const { page, cursorOptions } = options
60
- await ensureGhostCursorInjected({ page })
61
-
62
- await page.evaluate(
63
- ({ optionsFromNode }) => {
64
- const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
65
- api?.enable(optionsFromNode)
66
- },
67
- { optionsFromNode: cursorOptions },
68
- )
59
+ try {
60
+ const { page, cursorOptions } = options
61
+ await ensureGhostCursorInjected({ page })
62
+
63
+ await page.evaluate(
64
+ ({ optionsFromNode }) => {
65
+ const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
66
+ api?.enable(optionsFromNode)
67
+ },
68
+ { optionsFromNode: cursorOptions },
69
+ )
70
+ } catch {
71
+ // Non-fatal — page may be closed or navigating.
72
+ }
69
73
  }
70
74
 
71
75
  export async function disableGhostCursor(options: { page: Page }): Promise<void> {
72
- const { page } = options
73
- await page.evaluate(() => {
74
- const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
75
- api?.disable()
76
- })
76
+ try {
77
+ const { page } = options
78
+ await page.evaluate(() => {
79
+ const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
80
+ api?.disable()
81
+ })
82
+ } catch {
83
+ // Non-fatal — page may be closed or navigating.
84
+ }
77
85
  }
78
86
 
79
87
  export async function applyGhostCursorMouseAction(options: {
80
88
  page: Page
81
89
  event: MouseActionEvent
82
90
  }): Promise<void> {
83
- const { page, event } = options
84
-
85
- const applied = await page.evaluate(
86
- ({ serializedEvent }) => {
87
- const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
88
- if (!api) {
89
- return false
90
- }
91
-
92
- api.applyMouseAction(serializedEvent)
93
- return true
94
- },
95
- { serializedEvent: event },
96
- )
97
-
98
- if (applied) {
99
- return
91
+ // Never throw the cursor is cosmetic and must not break the caller's action.
92
+ try {
93
+ const { page, event } = options
94
+
95
+ const applied = await page.evaluate(
96
+ ({ serializedEvent }) => {
97
+ const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
98
+ if (!api) {
99
+ return false
100
+ }
101
+
102
+ api.applyMouseAction(serializedEvent)
103
+ return true
104
+ },
105
+ { serializedEvent: event },
106
+ )
107
+
108
+ if (applied) {
109
+ return
110
+ }
111
+
112
+ await ensureGhostCursorInjected({ page })
113
+ await page.evaluate(
114
+ ({ serializedEvent }) => {
115
+ const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
116
+ api?.applyMouseAction(serializedEvent)
117
+ },
118
+ { serializedEvent: event },
119
+ )
120
+ } catch {
121
+ // Swallow — page may be closed, navigating, or debugger detached.
100
122
  }
101
-
102
- await ensureGhostCursorInjected({ page })
103
- await page.evaluate(
104
- ({ serializedEvent }) => {
105
- const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
106
- api?.applyMouseAction(serializedEvent)
107
- },
108
- { serializedEvent: event },
109
- )
110
123
  }
package/src/mcp.ts CHANGED
@@ -56,9 +56,14 @@ function getLogServerUrl(): string {
56
56
 
57
57
  async function sendLogToRelayServer(level: string, ...args: any[]) {
58
58
  try {
59
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
60
+ const token = process.env.PLAYWRITER_TOKEN
61
+ if (token) {
62
+ headers['Authorization'] = `Bearer ${token}`
63
+ }
59
64
  await fetch(getLogServerUrl(), {
60
65
  method: 'POST',
61
- headers: { 'Content-Type': 'application/json' },
66
+ headers,
62
67
  body: JSON.stringify({ level, args }),
63
68
  signal: AbortSignal.timeout(1000),
64
69
  })
@@ -193,4 +193,34 @@ describe('onMouseAction callback', () => {
193
193
 
194
194
  await safeCloseCDPBrowser(directBrowser)
195
195
  }, 30000)
196
+
197
+ // Always-on ghost cursor: the Chrome extension injects the ghost-cursor-client.js
198
+ // bundle into MAIN world the moment it attaches a tab (see attachTab in
199
+ // extension/src/background.ts). This test verifies the cursor element exists on
200
+ // a freshly-attached tab WITHOUT any explicit enableGhostCursor call.
201
+ it('should inject ghost cursor into attached tabs without explicit enable', async () => {
202
+ const browserContext = testCtx!.browserContext
203
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
204
+
205
+ const page = await browserContext.newPage()
206
+ await page.goto('data:text/html,<html><body><h1>always-on-cursor</h1></body></html>')
207
+ await page.bringToFront()
208
+
209
+ await serviceWorker.evaluate(async () => {
210
+ await (globalThis as any).toggleExtensionForActiveTab()
211
+ })
212
+ await new Promise((r) => {
213
+ setTimeout(r, 300)
214
+ })
215
+
216
+ const cursorPresent = await page.evaluate(() => {
217
+ return {
218
+ apiPresent: Boolean((globalThis as any).__playwriterGhostCursor),
219
+ elementPresent: Boolean(document.getElementById('__playwriter_ghost_cursor__')),
220
+ }
221
+ })
222
+
223
+ expect(cursorPresent.apiPresent).toBe(true)
224
+ expect(cursorPresent.elementPresent).toBe(true)
225
+ }, 30000)
196
226
  })
@@ -0,0 +1,186 @@
1
+ // Example snippets for profiling website performance with Playwriter and CDP.
2
+
3
+ import { console, getCDPSession, page } from './debugger-examples-types.js'
4
+
5
+ type PerfMetrics = {
6
+ paints: Record<string, number>
7
+ lcp: number
8
+ cls: number
9
+ }
10
+
11
+ type ObservedPerfEntry = {
12
+ name: string
13
+ startTime: number
14
+ duration: number
15
+ hadRecentInput?: boolean
16
+ value?: number
17
+ interactionId?: number
18
+ }
19
+
20
+ type NavigationTimingEntry = {
21
+ responseStart: number
22
+ domContentLoadedEventEnd: number
23
+ loadEventEnd: number
24
+ }
25
+
26
+ type LongTaskEntry = {
27
+ startTime: number
28
+ duration: number
29
+ }
30
+
31
+ type EventTimingEntry = {
32
+ name: string
33
+ duration: number
34
+ interactionId: number
35
+ }
36
+
37
+ // Example: Collect navigation timing and basic web vitals from the current page
38
+ async function collectWebVitals() {
39
+ await page.evaluate(() => {
40
+ const metrics: PerfMetrics = {
41
+ paints: {},
42
+ lcp: 0,
43
+ cls: 0,
44
+ }
45
+
46
+ const perfGlobal = globalThis as typeof globalThis & {
47
+ __pwPerfMetrics?: PerfMetrics
48
+ }
49
+
50
+ perfGlobal.__pwPerfMetrics = metrics
51
+
52
+ new PerformanceObserver((list) => {
53
+ for (const entry of list.getEntries() as ObservedPerfEntry[]) {
54
+ metrics.paints[entry.name] = entry.startTime
55
+ }
56
+ }).observe({ type: 'paint', buffered: true } as never)
57
+
58
+ new PerformanceObserver((list) => {
59
+ const entries = list.getEntries() as ObservedPerfEntry[]
60
+ const lastEntry = entries[entries.length - 1]
61
+ if (!lastEntry) {
62
+ return
63
+ }
64
+ metrics.lcp = lastEntry.startTime
65
+ }).observe({ type: 'largest-contentful-paint', buffered: true } as never)
66
+
67
+ new PerformanceObserver((list) => {
68
+ for (const entry of list.getEntries() as ObservedPerfEntry[]) {
69
+ if (entry.hadRecentInput) {
70
+ continue
71
+ }
72
+ metrics.cls += entry.value || 0
73
+ }
74
+ }).observe({ type: 'layout-shift', buffered: true } as never)
75
+ })
76
+
77
+ await page.reload({ waitUntil: 'domcontentloaded' })
78
+
79
+ const report = await page.evaluate(() => {
80
+ const perfGlobal = globalThis as typeof globalThis & {
81
+ __pwPerfMetrics?: PerfMetrics
82
+ }
83
+ const nav = performance.getEntriesByType('navigation' as never)[0] as unknown as
84
+ | NavigationTimingEntry
85
+ | undefined
86
+ const metrics = perfGlobal.__pwPerfMetrics
87
+
88
+ return {
89
+ ttfb: nav?.responseStart || 0,
90
+ domContentLoaded: nav?.domContentLoadedEventEnd || 0,
91
+ load: nav?.loadEventEnd || 0,
92
+ fcp: metrics?.paints['first-contentful-paint'] || 0,
93
+ lcp: metrics?.lcp || 0,
94
+ cls: metrics?.cls || 0,
95
+ }
96
+ })
97
+
98
+ console.log(report)
99
+ }
100
+
101
+ // Example: Measure the biggest transferred requests with raw CDP network events
102
+ async function collectHeaviestRequests() {
103
+ const cdp = await getCDPSession({ page })
104
+ await cdp.send('Network.enable')
105
+ await cdp.send('Network.setCacheDisabled', { cacheDisabled: true })
106
+
107
+ const responses = new Map<string, { url: string; mimeType: string }>()
108
+ const finished = new Map<string, number>()
109
+
110
+ cdp.on('Network.responseReceived', (event) => {
111
+ responses.set(event.requestId, {
112
+ url: event.response.url,
113
+ mimeType: event.response.mimeType,
114
+ })
115
+ })
116
+
117
+ cdp.on('Network.loadingFinished', (event) => {
118
+ finished.set(event.requestId, event.encodedDataLength)
119
+ })
120
+
121
+ await page.reload({ waitUntil: 'domcontentloaded' })
122
+
123
+ const largest = [...responses.entries()]
124
+ .map(([requestId, response]) => {
125
+ return {
126
+ url: response.url,
127
+ mimeType: response.mimeType,
128
+ bytes: finished.get(requestId) || 0,
129
+ }
130
+ })
131
+ .sort((a, b) => b.bytes - a.bytes)
132
+ .slice(0, 10)
133
+
134
+ console.log(largest)
135
+ }
136
+
137
+ // Example: Check whether interactivity is blocked by long tasks or slow events
138
+ async function measureInteractivity() {
139
+ await page.evaluate(() => {
140
+ const perfGlobal = globalThis as typeof globalThis & {
141
+ __pwLongTasks?: LongTaskEntry[]
142
+ __pwEventTimings?: EventTimingEntry[]
143
+ }
144
+
145
+ perfGlobal.__pwLongTasks = []
146
+ perfGlobal.__pwEventTimings = []
147
+
148
+ new PerformanceObserver((list) => {
149
+ perfGlobal.__pwLongTasks?.push(
150
+ ...(list.getEntries() as ObservedPerfEntry[]).map((entry) => ({
151
+ startTime: entry.startTime,
152
+ duration: entry.duration,
153
+ })),
154
+ )
155
+ }).observe({ type: 'longtask', buffered: true } as never)
156
+
157
+ new PerformanceObserver((list) => {
158
+ perfGlobal.__pwEventTimings?.push(
159
+ ...(list.getEntries() as ObservedPerfEntry[]).map((entry) => ({
160
+ name: entry.name,
161
+ duration: entry.duration,
162
+ interactionId: entry.interactionId || 0,
163
+ })),
164
+ )
165
+ }).observe({ type: 'event', buffered: true, durationThreshold: 16 } as never)
166
+ })
167
+
168
+ const button = page.getByRole('button').first()
169
+ await button.click()
170
+
171
+ const report = await page.evaluate(() => {
172
+ const perfGlobal = globalThis as typeof globalThis & {
173
+ __pwLongTasks?: LongTaskEntry[]
174
+ __pwEventTimings?: EventTimingEntry[]
175
+ }
176
+
177
+ return {
178
+ longTasks: (perfGlobal.__pwLongTasks || []).filter((entry) => entry.duration >= 50),
179
+ events: (perfGlobal.__pwEventTimings || []).filter((entry) => entry.interactionId !== 0),
180
+ }
181
+ })
182
+
183
+ console.log(report)
184
+ }
185
+
186
+ export { collectWebVitals, collectHeaviestRequests, measureInteractivity }