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.
- package/dist/bippy.js +5 -5
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +17 -5
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cli-help.test.d.ts +2 -0
- package/dist/cli-help.test.d.ts.map +1 -0
- package/dist/cli-help.test.js +53 -0
- package/dist/cli-help.test.js.map +1 -0
- package/dist/cli.js +74 -25
- package/dist/cli.js.map +1 -1
- package/dist/executor.d.ts +1 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +55 -12
- package/dist/executor.js.map +1 -1
- package/dist/extension/background.js +675 -27
- package/dist/extension/manifest.json +1 -1
- package/dist/ghost-cursor-client.js +170 -83
- package/dist/{recording-ghost-cursor.d.ts → ghost-cursor-controller.d.ts} +15 -10
- package/dist/ghost-cursor-controller.d.ts.map +1 -0
- package/dist/ghost-cursor-controller.js +98 -0
- package/dist/ghost-cursor-controller.js.map +1 -0
- package/dist/ghost-cursor.d.ts.map +1 -1
- package/dist/ghost-cursor.js +42 -26
- package/dist/ghost-cursor.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +6 -1
- package/dist/mcp.js.map +1 -1
- package/dist/on-mouse-action.test.js +25 -0
- package/dist/on-mouse-action.test.js.map +1 -1
- package/dist/performance-examples.d.ts +5 -0
- package/dist/performance-examples.d.ts.map +1 -0
- package/dist/performance-examples.js +112 -0
- package/dist/performance-examples.js.map +1 -0
- package/dist/performance-profiling.md +417 -0
- package/dist/prompt.md +22 -8
- package/dist/react-source.d.ts +44 -0
- package/dist/react-source.d.ts.map +1 -1
- package/dist/react-source.js +207 -20
- package/dist/react-source.js.map +1 -1
- package/dist/readability.js +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +101 -1
- package/dist/relay-core.test.js.map +1 -1
- package/dist/relay-session.test.js +34 -6
- package/dist/relay-session.test.js.map +1 -1
- package/dist/screen-recording.d.ts +2 -2
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +19 -7
- package/dist/screen-recording.js.map +1 -1
- package/dist/selector-generator.js +1 -1
- package/package.json +7 -7
- package/src/aria-snapshots/github-interactive.txt +5 -3
- package/src/aria-snapshots/github-raw.txt +8 -5
- package/src/aria-snapshots/hackernews-interactive.txt +241 -238
- package/src/aria-snapshots/hackernews-raw.txt +269 -265
- package/src/aria-snapshots/prosemirror-interactive.txt +3 -1
- package/src/aria-snapshots/prosemirror-raw.txt +4 -1
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/assets/aria-labels-old-reddit.png +0 -0
- package/src/cdp-relay.ts +17 -5
- package/src/cli-help.test.ts +63 -0
- package/src/cli.ts +80 -28
- package/src/executor.ts +65 -15
- package/src/ghost-cursor-client.ts +221 -96
- package/src/{recording-ghost-cursor.ts → ghost-cursor-controller.ts} +50 -34
- package/src/ghost-cursor.ts +54 -41
- package/src/mcp.ts +6 -1
- package/src/on-mouse-action.test.ts +30 -0
- package/src/performance-examples.ts +186 -0
- package/src/react-source.ts +310 -24
- package/src/relay-core.test.ts +117 -0
- package/src/relay-session.test.ts +36 -10
- package/src/screen-recording.ts +23 -10
- package/src/skill.md +33 -9
- package/src/snapshots/shadcn-ui-accessibility-full.md +6 -3
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +2 -0
- package/dist/recording-ghost-cursor.d.ts.map +0 -1
- package/dist/recording-ghost-cursor.js +0 -79
- package/dist/recording-ghost-cursor.js.map +0 -1
package/src/ghost-cursor.ts
CHANGED
|
@@ -56,55 +56,68 @@ export async function enableGhostCursor(options: {
|
|
|
56
56
|
page: Page
|
|
57
57
|
cursorOptions?: GhostCursorClientOptions
|
|
58
58
|
}): Promise<void> {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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 }
|