playwriter 0.0.63 → 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 +41 -3
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +134 -55
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/aria-snapshot.test.js +5 -2
- package/dist/aria-snapshot.test.js.map +1 -1
- package/dist/aria-snapshot.unit.test.js +83 -41
- package/dist/aria-snapshot.unit.test.js.map +1 -1
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
- package/dist/bippy.js +1 -1
- package/dist/cdp-log.d.ts +1 -1
- package/dist/cdp-log.d.ts.map +1 -1
- package/dist/cdp-log.js +1 -1
- package/dist/cdp-log.js.map +1 -1
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +492 -298
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cdp-session.d.ts.map +1 -1
- package/dist/cdp-session.js.map +1 -1
- package/dist/cdp-types.d.ts.map +1 -1
- package/dist/cdp-types.js +7 -7
- package/dist/cdp-types.js.map +1 -1
- package/dist/clean-html.d.ts.map +1 -1
- package/dist/clean-html.js +4 -5
- package/dist/clean-html.js.map +1 -1
- package/dist/cli.js +45 -27
- package/dist/cli.js.map +1 -1
- package/dist/create-logger.d.ts.map +1 -1
- package/dist/create-logger.js +3 -1
- package/dist/create-logger.js.map +1 -1
- package/dist/debugger-examples-types.d.ts.map +1 -1
- package/dist/debugger.d.ts.map +1 -1
- package/dist/debugger.js +1 -3
- package/dist/debugger.js.map +1 -1
- package/dist/diff-utils.d.ts.map +1 -1
- package/dist/diff-utils.js +1 -4
- package/dist/diff-utils.js.map +1 -1
- package/dist/editor-api.md +12 -2
- package/dist/editor-examples.d.ts +1 -1
- package/dist/editor-examples.d.ts.map +1 -1
- package/dist/editor-examples.js +1 -1
- package/dist/editor-examples.js.map +1 -1
- package/dist/editor.d.ts +1 -1
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/executor.d.ts +26 -3
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +297 -64
- package/dist/executor.js.map +1 -1
- package/dist/executor.unit.test.js +38 -1
- package/dist/executor.unit.test.js.map +1 -1
- package/dist/extension-connection.test.js +139 -36
- package/dist/extension-connection.test.js.map +1 -1
- package/dist/ffmpeg.d.ts +148 -0
- package/dist/ffmpeg.d.ts.map +1 -0
- package/dist/ffmpeg.js +523 -0
- package/dist/ffmpeg.js.map +1 -0
- package/dist/ghost-browser.d.ts.map +1 -1
- package/dist/ghost-browser.js.map +1 -1
- package/dist/ghost-cursor-client.js +287 -0
- package/dist/ghost-cursor.d.ts +27 -0
- package/dist/ghost-cursor.d.ts.map +1 -0
- package/dist/ghost-cursor.js +63 -0
- package/dist/ghost-cursor.js.map +1 -0
- package/dist/htmlrewrite.d.ts.map +1 -1
- package/dist/htmlrewrite.js +17 -55
- package/dist/htmlrewrite.js.map +1 -1
- package/dist/htmlrewrite.test.js.map +1 -1
- package/dist/kill-port.d.ts.map +1 -1
- package/dist/kill-port.js +1 -3
- package/dist/kill-port.js.map +1 -1
- package/dist/locator-selector.test.d.ts +2 -0
- package/dist/locator-selector.test.d.ts.map +1 -0
- package/dist/locator-selector.test.js +96 -0
- package/dist/locator-selector.test.js.map +1 -0
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +8 -3
- package/dist/mcp.js.map +1 -1
- package/dist/on-mouse-action.test.d.ts +2 -0
- package/dist/on-mouse-action.test.d.ts.map +1 -0
- package/dist/on-mouse-action.test.js +155 -0
- package/dist/on-mouse-action.test.js.map +1 -0
- package/dist/page-markdown.js +4 -4
- package/dist/page-markdown.js.map +1 -1
- package/dist/prompt.md +450 -377
- package/dist/protocol.d.ts +4 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/readability.js +16 -2
- package/dist/recording-ghost-cursor.d.ts +41 -0
- package/dist/recording-ghost-cursor.d.ts.map +1 -0
- package/dist/recording-ghost-cursor.js +79 -0
- package/dist/recording-ghost-cursor.js.map +1 -0
- package/dist/recording-relay.d.ts.map +1 -1
- package/dist/recording-relay.js +8 -8
- package/dist/recording-relay.js.map +1 -1
- package/dist/relay-client.d.ts +17 -4
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +45 -11
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +515 -26
- 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 +169 -31
- package/dist/relay-navigation.test.js.map +1 -1
- package/dist/relay-session.test.d.ts.map +1 -1
- package/dist/relay-session.test.js +113 -65
- package/dist/relay-session.test.js.map +1 -1
- package/dist/relay-state.d.ts +158 -0
- package/dist/relay-state.d.ts.map +1 -0
- package/dist/relay-state.js +306 -0
- package/dist/relay-state.js.map +1 -0
- package/dist/relay-state.test.d.ts +2 -0
- package/dist/relay-state.test.d.ts.map +1 -0
- package/dist/relay-state.test.js +472 -0
- package/dist/relay-state.test.js.map +1 -0
- package/dist/scoped-fs.d.ts.map +1 -1
- package/dist/scoped-fs.js.map +1 -1
- package/dist/screen-recording.d.ts +66 -4
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +150 -13
- 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/dist/snapshot-tools.test.js +71 -28
- package/dist/snapshot-tools.test.js.map +1 -1
- package/dist/start-relay-server.d.ts +1 -1
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +1 -1
- package/dist/start-relay-server.js.map +1 -1
- package/dist/styles-api.md +8 -1
- package/dist/styles-examples.d.ts +1 -1
- package/dist/styles-examples.d.ts.map +1 -1
- package/dist/styles-examples.js +1 -1
- package/dist/styles-examples.js.map +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +1 -3
- package/dist/styles.js.map +1 -1
- package/dist/test-declarations.d.ts.map +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +7 -5
- package/dist/test-utils.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts.map +1 -1
- package/dist/wait-for-page-load.js +1 -1
- package/dist/wait-for-page-load.js.map +1 -1
- package/package.json +4 -3
- package/src/a11y-client.ts +5 -4
- package/src/aria-snapshot.test.ts +5 -2
- package/src/aria-snapshot.ts +306 -117
- package/src/aria-snapshot.unit.test.ts +199 -141
- package/src/aria-snapshots/github-interactive.txt +2 -0
- package/src/aria-snapshots/github-raw.txt +5 -1
- package/src/aria-snapshots/hackernews-interactive.txt +238 -241
- package/src/aria-snapshots/hackernews-raw.txt +265 -269
- package/src/assets/aria-labels-example.png +0 -0
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/assets/aria-labels-old-reddit.png +0 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
- package/src/cdp-log.ts +4 -1
- package/src/cdp-relay.ts +1059 -737
- package/src/cdp-session.ts +12 -3
- package/src/cdp-types.ts +51 -51
- package/src/clean-html.ts +4 -5
- package/src/cli.ts +82 -55
- package/src/create-logger.ts +5 -3
- package/src/debugger-examples-types.ts +4 -1
- package/src/debugger.ts +1 -5
- package/src/diff-utils.ts +2 -5
- package/src/editor-examples.ts +11 -1
- package/src/editor.ts +10 -2
- package/src/executor.ts +374 -73
- package/src/executor.unit.test.ts +48 -1
- package/src/extension-connection.test.ts +612 -488
- package/src/ffmpeg.ts +769 -0
- package/src/ghost-browser.ts +4 -6
- package/src/ghost-cursor-client.ts +369 -0
- package/src/ghost-cursor.ts +110 -0
- package/src/htmlrewrite.test.ts +6 -2
- package/src/htmlrewrite.ts +348 -386
- package/src/kill-port.ts +1 -3
- package/src/locator-selector.test.ts +115 -0
- package/src/mcp-client.ts +1 -1
- package/src/mcp.ts +21 -15
- package/src/on-mouse-action.test.ts +196 -0
- package/src/page-markdown.ts +7 -7
- package/src/protocol.ts +73 -57
- package/src/recording-ghost-cursor.ts +113 -0
- package/src/recording-relay.ts +20 -12
- package/src/relay-client.ts +85 -18
- package/src/relay-core.test.ts +1117 -578
- package/src/relay-navigation.test.ts +648 -483
- package/src/relay-session.test.ts +984 -929
- package/src/relay-state.test.ts +570 -0
- package/src/relay-state.ts +497 -0
- package/src/resource.md +21 -49
- package/src/scoped-fs.ts +9 -3
- package/src/screen-recording.test.ts +111 -0
- package/src/screen-recording.ts +256 -31
- package/src/skill.md +476 -396
- package/src/snapshot-tools.test.ts +580 -528
- package/src/snapshots/shadcn-ui-accessibility-full.md +8 -8
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +8 -8
- package/src/start-relay-server.ts +14 -11
- package/src/styles-examples.ts +8 -1
- package/src/styles.ts +20 -21
- package/src/test-declarations.ts +6 -6
- package/src/test-utils.ts +104 -91
- package/src/utils.ts +2 -1
- package/src/wait-for-page-load.ts +6 -1
package/src/aria-snapshot.ts
CHANGED
|
@@ -11,11 +11,14 @@ import { Sema } from 'async-sema'
|
|
|
11
11
|
import type { ICDPSession } from './cdp-session.js'
|
|
12
12
|
import { getCDPSessionForPage } from './cdp-session.js'
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
// Import sharp at module level - resolves to null if not available
|
|
16
15
|
const sharpPromise = import('sharp')
|
|
17
|
-
.then((m) => {
|
|
18
|
-
|
|
16
|
+
.then((m) => {
|
|
17
|
+
return m.default
|
|
18
|
+
})
|
|
19
|
+
.catch(() => {
|
|
20
|
+
return null
|
|
21
|
+
})
|
|
19
22
|
|
|
20
23
|
// ============================================================================
|
|
21
24
|
// Snapshot Format Types
|
|
@@ -81,6 +84,113 @@ export interface ScreenshotResult {
|
|
|
81
84
|
labelCount: number
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Image Resize Utility
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* LLM-optimal max dimension. Claude auto-resizes images larger than 1568px
|
|
93
|
+
* on any edge, adding latency. Token cost: (width * height) / 750.
|
|
94
|
+
*/
|
|
95
|
+
export const LLM_MAX_DIMENSION = 1568
|
|
96
|
+
|
|
97
|
+
export interface ResizeImageOptions {
|
|
98
|
+
/** Input: file path or Buffer */
|
|
99
|
+
input: string | Buffer
|
|
100
|
+
/** Max pixels on longest edge. Default 1568 (Claude-optimal).
|
|
101
|
+
* Ignored if explicit width/height provided. */
|
|
102
|
+
maxDimension?: number
|
|
103
|
+
/** Explicit target width in px (aspect ratio preserved unless both width+height set) */
|
|
104
|
+
width?: number
|
|
105
|
+
/** Explicit target height in px */
|
|
106
|
+
height?: number
|
|
107
|
+
/** How to fit when both width+height specified. Default 'inside' (preserve aspect ratio, no crop) */
|
|
108
|
+
fit?: 'inside' | 'cover' | 'contain' | 'fill'
|
|
109
|
+
/** JPEG quality 1-100. Default 80 */
|
|
110
|
+
quality?: number
|
|
111
|
+
/** Output file path. Defaults to overwriting the input file (when input is a path) */
|
|
112
|
+
output?: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ResizeImageResult {
|
|
116
|
+
buffer: Buffer
|
|
117
|
+
mimeType: 'image/jpeg'
|
|
118
|
+
/** Only set if output path was provided */
|
|
119
|
+
path?: string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resize an image using sharp. Useful for shrinking screenshots before reading
|
|
124
|
+
* them back into context so they consume fewer tokens.
|
|
125
|
+
*
|
|
126
|
+
* Default behavior (no width/height): fits within 1568×1568px, preserving
|
|
127
|
+
* aspect ratio, never upscales. This is optimal for Claude vision.
|
|
128
|
+
*
|
|
129
|
+
* Explicit width/height: resizes to those dimensions using the fit strategy.
|
|
130
|
+
*/
|
|
131
|
+
export async function resizeImage(options: ResizeImageOptions): Promise<ResizeImageResult> {
|
|
132
|
+
const sharp = await sharpPromise
|
|
133
|
+
if (!sharp) {
|
|
134
|
+
throw new Error('sharp is not installed — install it with: pnpm add sharp')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const inputBuffer = (() => {
|
|
138
|
+
if (Buffer.isBuffer(options.input)) {
|
|
139
|
+
return options.input
|
|
140
|
+
}
|
|
141
|
+
return fs.readFileSync(options.input)
|
|
142
|
+
})()
|
|
143
|
+
|
|
144
|
+
const quality = options.quality ?? 80
|
|
145
|
+
const hasExplicitDimensions = options.width !== undefined || options.height !== undefined
|
|
146
|
+
|
|
147
|
+
const fit = options.fit ?? 'inside'
|
|
148
|
+
const resizeOpts = (() => {
|
|
149
|
+
if (hasExplicitDimensions) {
|
|
150
|
+
return {
|
|
151
|
+
width: options.width,
|
|
152
|
+
height: options.height,
|
|
153
|
+
fit,
|
|
154
|
+
withoutEnlargement: false,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const max = options.maxDimension ?? LLM_MAX_DIMENSION
|
|
158
|
+
return {
|
|
159
|
+
width: max,
|
|
160
|
+
height: max,
|
|
161
|
+
fit,
|
|
162
|
+
withoutEnlargement: true,
|
|
163
|
+
}
|
|
164
|
+
})()
|
|
165
|
+
|
|
166
|
+
const buffer = await sharp(inputBuffer)
|
|
167
|
+
.resize(resizeOpts)
|
|
168
|
+
.jpeg({ quality })
|
|
169
|
+
.toBuffer()
|
|
170
|
+
|
|
171
|
+
// Default: overwrite input file. When input is a Buffer, no file is written
|
|
172
|
+
// unless output is explicitly set.
|
|
173
|
+
const outputPath = (() => {
|
|
174
|
+
if (options.output) {
|
|
175
|
+
return options.output
|
|
176
|
+
}
|
|
177
|
+
if (typeof options.input === 'string') {
|
|
178
|
+
return options.input
|
|
179
|
+
}
|
|
180
|
+
return undefined
|
|
181
|
+
})()
|
|
182
|
+
|
|
183
|
+
if (outputPath) {
|
|
184
|
+
fs.writeFileSync(outputPath, buffer)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
buffer,
|
|
189
|
+
mimeType: 'image/jpeg',
|
|
190
|
+
...(outputPath ? { path: outputPath } : {}),
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
84
194
|
export interface AriaSnapshotResult {
|
|
85
195
|
snapshot: string
|
|
86
196
|
tree: AriaSnapshotNode[]
|
|
@@ -142,9 +252,7 @@ const INTERACTIVE_ROLES = new Set([
|
|
|
142
252
|
'audio',
|
|
143
253
|
])
|
|
144
254
|
|
|
145
|
-
const LABEL_ROLES = new Set([
|
|
146
|
-
'labeltext',
|
|
147
|
-
])
|
|
255
|
+
const LABEL_ROLES = new Set(['labeltext'])
|
|
148
256
|
|
|
149
257
|
const MAX_LABEL_POSITION_CONCURRENCY = 24
|
|
150
258
|
const BOX_MODEL_TIMEOUT_MS = 5000
|
|
@@ -165,12 +273,7 @@ const CONTEXT_ROLES = new Set([
|
|
|
165
273
|
'cell',
|
|
166
274
|
])
|
|
167
275
|
|
|
168
|
-
const SKIP_WRAPPER_ROLES = new Set([
|
|
169
|
-
'generic',
|
|
170
|
-
'group',
|
|
171
|
-
'none',
|
|
172
|
-
'presentation',
|
|
173
|
-
])
|
|
276
|
+
const SKIP_WRAPPER_ROLES = new Set(['generic', 'group', 'none', 'presentation'])
|
|
174
277
|
|
|
175
278
|
const TEST_ID_ATTRS = [
|
|
176
279
|
'data-testid',
|
|
@@ -231,7 +334,15 @@ function buildLocatorFromStable(stable: { value: string; attr: string }): string
|
|
|
231
334
|
return `[${stable.attr}="${escaped}"]`
|
|
232
335
|
}
|
|
233
336
|
|
|
234
|
-
function buildBaseLocator({
|
|
337
|
+
function buildBaseLocator({
|
|
338
|
+
role,
|
|
339
|
+
name,
|
|
340
|
+
stable,
|
|
341
|
+
}: {
|
|
342
|
+
role: string
|
|
343
|
+
name: string
|
|
344
|
+
stable: { value: string; attr: string } | null
|
|
345
|
+
}): string {
|
|
235
346
|
if (stable) {
|
|
236
347
|
return buildLocatorFromStable(stable)
|
|
237
348
|
}
|
|
@@ -243,7 +354,6 @@ function buildBaseLocator({ role, name, stable }: { role: string; name: string;
|
|
|
243
354
|
return `role=${role}`
|
|
244
355
|
}
|
|
245
356
|
|
|
246
|
-
|
|
247
357
|
function getAxValueString(value?: Protocol.Accessibility.AXValue): string {
|
|
248
358
|
if (!value) {
|
|
249
359
|
return ''
|
|
@@ -283,7 +393,13 @@ export type SnapshotNode = {
|
|
|
283
393
|
children: SnapshotNode[]
|
|
284
394
|
}
|
|
285
395
|
|
|
286
|
-
function buildSnapshotLine({
|
|
396
|
+
function buildSnapshotLine({
|
|
397
|
+
role,
|
|
398
|
+
name,
|
|
399
|
+
baseLocator,
|
|
400
|
+
indent,
|
|
401
|
+
hasChildren,
|
|
402
|
+
}: {
|
|
287
403
|
role: string
|
|
288
404
|
name: string
|
|
289
405
|
baseLocator?: string
|
|
@@ -308,15 +424,16 @@ function buildTextLine(text: string, indent: number): SnapshotLine {
|
|
|
308
424
|
export function buildSnapshotLines(nodes: SnapshotNode[], indent = 0): SnapshotLine[] {
|
|
309
425
|
return nodes.flatMap((node) => {
|
|
310
426
|
const nodeIndent = indent + (node.indentOffset ?? 0)
|
|
311
|
-
const line =
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
427
|
+
const line =
|
|
428
|
+
node.role === 'text'
|
|
429
|
+
? buildTextLine(node.name, nodeIndent)
|
|
430
|
+
: buildSnapshotLine({
|
|
431
|
+
role: node.role,
|
|
432
|
+
name: node.name,
|
|
433
|
+
baseLocator: node.baseLocator,
|
|
434
|
+
indent: nodeIndent,
|
|
435
|
+
hasChildren: node.children.length > 0,
|
|
436
|
+
})
|
|
320
437
|
return [line, ...buildSnapshotLines(node.children, nodeIndent + 1)]
|
|
321
438
|
})
|
|
322
439
|
}
|
|
@@ -339,13 +456,15 @@ export function buildRawSnapshotTree(options: {
|
|
|
339
456
|
|
|
340
457
|
const role = getAxRole(node)
|
|
341
458
|
const name = getAxValueString(node.name).trim()
|
|
342
|
-
const children = (node.childIds ?? [])
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
459
|
+
const children = (node.childIds ?? [])
|
|
460
|
+
.map((childId) => {
|
|
461
|
+
return buildRawSnapshotTree({
|
|
462
|
+
nodeId: childId,
|
|
463
|
+
axById: options.axById,
|
|
464
|
+
isNodeInScope: options.isNodeInScope,
|
|
465
|
+
})
|
|
347
466
|
})
|
|
348
|
-
|
|
467
|
+
.filter(isTruthy)
|
|
349
468
|
|
|
350
469
|
const inScope = options.isNodeInScope(node) || children.length > 0
|
|
351
470
|
if (!inScope) {
|
|
@@ -367,7 +486,11 @@ export function filterInteractiveSnapshotTree(options: {
|
|
|
367
486
|
labelContext: boolean
|
|
368
487
|
refFilter?: (entry: { role: string; name: string }) => boolean
|
|
369
488
|
domByBackendId: Map<Protocol.DOM.BackendNodeId, DomNodeInfo>
|
|
370
|
-
createRefForNode: (options: {
|
|
489
|
+
createRefForNode: (options: {
|
|
490
|
+
backendNodeId?: Protocol.DOM.BackendNodeId
|
|
491
|
+
role: string
|
|
492
|
+
name: string
|
|
493
|
+
}) => string | null
|
|
371
494
|
}): { nodes: SnapshotNode[]; names: Set<string> } {
|
|
372
495
|
const role = options.node.role
|
|
373
496
|
const name = options.node.name
|
|
@@ -476,7 +599,11 @@ export function filterFullSnapshotTree(options: {
|
|
|
476
599
|
ancestorNames: string[]
|
|
477
600
|
refFilter?: (entry: { role: string; name: string }) => boolean
|
|
478
601
|
domByBackendId: Map<Protocol.DOM.BackendNodeId, DomNodeInfo>
|
|
479
|
-
createRefForNode: (options: {
|
|
602
|
+
createRefForNode: (options: {
|
|
603
|
+
backendNodeId?: Protocol.DOM.BackendNodeId
|
|
604
|
+
role: string
|
|
605
|
+
name: string
|
|
606
|
+
}) => string | null
|
|
480
607
|
}): { nodes: SnapshotNode[]; names: Set<string> } {
|
|
481
608
|
const role = options.node.role
|
|
482
609
|
const name = options.node.name
|
|
@@ -613,18 +740,20 @@ export function finalizeSnapshotOutput(
|
|
|
613
740
|
}, [])
|
|
614
741
|
|
|
615
742
|
let lineLocatorIndex = 0
|
|
616
|
-
const snapshot = lines
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
743
|
+
const snapshot = lines
|
|
744
|
+
.map((line) => {
|
|
745
|
+
let text = line.text
|
|
746
|
+
if (line.baseLocator) {
|
|
747
|
+
const locator = locatorSequence[lineLocatorIndex]
|
|
748
|
+
lineLocatorIndex += 1
|
|
749
|
+
text = buildLocatorLineText({ line, locator })
|
|
750
|
+
}
|
|
751
|
+
if (line.hasChildren) {
|
|
752
|
+
text += ':'
|
|
753
|
+
}
|
|
754
|
+
return text
|
|
755
|
+
})
|
|
756
|
+
.join('\n')
|
|
628
757
|
|
|
629
758
|
let nodeLocatorIndex = 0
|
|
630
759
|
const applyLocators = (items: SnapshotNode[]): AriaSnapshotNode[] => {
|
|
@@ -676,7 +805,11 @@ function buildDomIndex(nodes: Protocol.DOM.Node[]): {
|
|
|
676
805
|
return { domById, domByBackendId, childrenByParent }
|
|
677
806
|
}
|
|
678
807
|
|
|
679
|
-
function findScopeRootNodeId(
|
|
808
|
+
function findScopeRootNodeId(
|
|
809
|
+
nodes: Protocol.DOM.Node[],
|
|
810
|
+
attrName: string,
|
|
811
|
+
attrValue: string,
|
|
812
|
+
): Protocol.DOM.NodeId | null {
|
|
680
813
|
for (const node of nodes) {
|
|
681
814
|
if (!node.attributes) {
|
|
682
815
|
continue
|
|
@@ -692,7 +825,11 @@ function findScopeRootNodeId(nodes: Protocol.DOM.Node[], attrName: string, attrV
|
|
|
692
825
|
return null
|
|
693
826
|
}
|
|
694
827
|
|
|
695
|
-
function buildBackendIdSet(
|
|
828
|
+
function buildBackendIdSet(
|
|
829
|
+
rootNodeId: Protocol.DOM.NodeId,
|
|
830
|
+
childrenByParent: Map<Protocol.DOM.NodeId, Protocol.DOM.NodeId[]>,
|
|
831
|
+
domById: Map<Protocol.DOM.NodeId, DomNodeInfo>,
|
|
832
|
+
): Set<Protocol.DOM.BackendNodeId> {
|
|
696
833
|
const result = new Set<Protocol.DOM.BackendNodeId>()
|
|
697
834
|
const stack: Protocol.DOM.NodeId[] = [rootNodeId]
|
|
698
835
|
while (stack.length > 0) {
|
|
@@ -786,7 +923,14 @@ async function resolveFrame({ frame, page }: { frame?: Frame | FrameLocator; pag
|
|
|
786
923
|
* await page.locator(selector).click()
|
|
787
924
|
* ```
|
|
788
925
|
*/
|
|
789
|
-
export async function getAriaSnapshot({
|
|
926
|
+
export async function getAriaSnapshot({
|
|
927
|
+
page,
|
|
928
|
+
frame,
|
|
929
|
+
locator,
|
|
930
|
+
refFilter,
|
|
931
|
+
interactiveOnly = false,
|
|
932
|
+
cdp,
|
|
933
|
+
}: {
|
|
790
934
|
page: Page
|
|
791
935
|
frame?: Frame | FrameLocator
|
|
792
936
|
locator?: Locator
|
|
@@ -794,29 +938,29 @@ export async function getAriaSnapshot({ page, frame, locator, refFilter, interac
|
|
|
794
938
|
interactiveOnly?: boolean
|
|
795
939
|
cdp?: ICDPSession
|
|
796
940
|
}): Promise<AriaSnapshotResult> {
|
|
797
|
-
const session = cdp || await getCDPSessionForPage({ page })
|
|
941
|
+
const session = cdp || (await getCDPSessionForPage({ page }))
|
|
798
942
|
|
|
799
943
|
// Resolve FrameLocator to an actual Frame. FrameLocator (from locator.contentFrame())
|
|
800
944
|
// is a scoping helper without CDP access. We need the real Frame from page.frames()
|
|
801
945
|
// which has frameId() for OOPIF session attachment.
|
|
802
946
|
const resolvedFrame = await resolveFrame({ frame, page })
|
|
803
|
-
|
|
947
|
+
|
|
804
948
|
// For cross-origin iframes (OOPIFs), we need to attach to the iframe's target
|
|
805
949
|
// to get a separate CDP session. Same-origin iframes can use frameId directly.
|
|
806
950
|
let oopifSessionId: string | null = null
|
|
807
951
|
const frameId = resolvedFrame?.frameId() ?? null
|
|
808
|
-
|
|
952
|
+
|
|
809
953
|
if (frameId) {
|
|
810
|
-
const { targetInfos } = await session.send('Target.getTargets') as Protocol.Target.GetTargetsResponse
|
|
954
|
+
const { targetInfos } = (await session.send('Target.getTargets')) as Protocol.Target.GetTargetsResponse
|
|
811
955
|
const frameUrl = resolvedFrame!.url()
|
|
812
956
|
const iframeTarget = targetInfos.find((t) => {
|
|
813
957
|
return t.type === 'iframe' && t.url === frameUrl
|
|
814
958
|
})
|
|
815
959
|
if (iframeTarget) {
|
|
816
|
-
const { sessionId } = await session.send('Target.attachToTarget', {
|
|
960
|
+
const { sessionId } = (await session.send('Target.attachToTarget', {
|
|
817
961
|
targetId: iframeTarget.targetId,
|
|
818
962
|
flatten: true,
|
|
819
|
-
}) as Protocol.Target.AttachToTargetResponse
|
|
963
|
+
})) as Protocol.Target.AttachToTargetResponse
|
|
820
964
|
oopifSessionId = sessionId
|
|
821
965
|
await session.send('Runtime.runIfWaitingForDebugger', undefined, oopifSessionId)
|
|
822
966
|
}
|
|
@@ -831,13 +975,20 @@ export async function getAriaSnapshot({ page, frame, locator, refFilter, interac
|
|
|
831
975
|
|
|
832
976
|
try {
|
|
833
977
|
if (scopeLocator) {
|
|
834
|
-
await scopeLocator.evaluate(
|
|
835
|
-
element
|
|
836
|
-
|
|
978
|
+
await scopeLocator.evaluate(
|
|
979
|
+
(element, data) => {
|
|
980
|
+
element.setAttribute(data.attr, data.value)
|
|
981
|
+
},
|
|
982
|
+
{ attr: scopeAttr, value: scopeValue },
|
|
983
|
+
)
|
|
837
984
|
scopeApplied = true
|
|
838
985
|
}
|
|
839
986
|
|
|
840
|
-
const { nodes: domNodes } = await session.send(
|
|
987
|
+
const { nodes: domNodes } = (await session.send(
|
|
988
|
+
'DOM.getFlattenedDocument',
|
|
989
|
+
{ depth: -1, pierce: true },
|
|
990
|
+
oopifSessionId,
|
|
991
|
+
)) as Protocol.DOM.GetFlattenedDocumentResponse
|
|
841
992
|
const { domById, domByBackendId, childrenByParent } = buildDomIndex(domNodes)
|
|
842
993
|
|
|
843
994
|
let scopeRootNodeId: Protocol.DOM.NodeId | null = null
|
|
@@ -852,12 +1003,14 @@ export async function getAriaSnapshot({ page, frame, locator, refFilter, interac
|
|
|
852
1003
|
}
|
|
853
1004
|
}
|
|
854
1005
|
|
|
855
|
-
const allowedBackendIds = scopeRootNodeId
|
|
856
|
-
? buildBackendIdSet(scopeRootNodeId, childrenByParent, domById)
|
|
857
|
-
: null
|
|
1006
|
+
const allowedBackendIds = scopeRootNodeId ? buildBackendIdSet(scopeRootNodeId, childrenByParent, domById) : null
|
|
858
1007
|
|
|
859
1008
|
const axParams = !oopifSessionId && frameId ? { frameId } : undefined
|
|
860
|
-
const { nodes: axNodes } = await session.send(
|
|
1009
|
+
const { nodes: axNodes } = (await session.send(
|
|
1010
|
+
'Accessibility.getFullAXTree',
|
|
1011
|
+
axParams,
|
|
1012
|
+
oopifSessionId,
|
|
1013
|
+
)) as Protocol.Accessibility.GetFullAXTreeResponse
|
|
861
1014
|
|
|
862
1015
|
const axById = new Map<Protocol.Accessibility.AXNodeId, Protocol.Accessibility.AXNode>()
|
|
863
1016
|
for (const node of axNodes) {
|
|
@@ -937,16 +1090,18 @@ export async function getAriaSnapshot({ page, frame, locator, refFilter, interac
|
|
|
937
1090
|
return allowedBackendIds.has(node.backendDOMNodeId)
|
|
938
1091
|
}
|
|
939
1092
|
|
|
940
|
-
|
|
941
1093
|
let snapshotNodes: SnapshotNode[] = []
|
|
942
1094
|
if (rootAxNodeId) {
|
|
943
1095
|
const rootNode = axById.get(rootAxNodeId)
|
|
944
1096
|
const rootRole = rootNode ? getAxRole(rootNode) : ''
|
|
945
|
-
const rawRoots =
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1097
|
+
const rawRoots =
|
|
1098
|
+
rootNode && (rootRole === 'rootwebarea' || rootRole === 'webarea') && rootNode.childIds
|
|
1099
|
+
? rootNode.childIds
|
|
1100
|
+
.map((childId) => {
|
|
1101
|
+
return buildRawSnapshotTree({ nodeId: childId, axById, isNodeInScope })
|
|
1102
|
+
})
|
|
1103
|
+
.filter(isTruthy)
|
|
1104
|
+
: [buildRawSnapshotTree({ nodeId: rootAxNodeId, axById, isNodeInScope })].filter(isTruthy)
|
|
950
1105
|
|
|
951
1106
|
const filtered = rawRoots.flatMap((rawNode) => {
|
|
952
1107
|
if (interactiveOnly) {
|
|
@@ -1027,7 +1182,7 @@ export async function getAriaSnapshot({ page, frame, locator, refFilter, interac
|
|
|
1027
1182
|
} catch {
|
|
1028
1183
|
return null
|
|
1029
1184
|
}
|
|
1030
|
-
})
|
|
1185
|
+
}),
|
|
1031
1186
|
)
|
|
1032
1187
|
|
|
1033
1188
|
const matchingRefs = await page.evaluate(
|
|
@@ -1037,7 +1192,16 @@ export async function getAriaSnapshot({ page, frame, locator, refFilter, interac
|
|
|
1037
1192
|
return null
|
|
1038
1193
|
}
|
|
1039
1194
|
|
|
1040
|
-
const testIdAttrs = [
|
|
1195
|
+
const testIdAttrs = [
|
|
1196
|
+
'data-testid',
|
|
1197
|
+
'data-test-id',
|
|
1198
|
+
'data-test',
|
|
1199
|
+
'data-cy',
|
|
1200
|
+
'data-pw',
|
|
1201
|
+
'data-qa',
|
|
1202
|
+
'data-e2e',
|
|
1203
|
+
'data-automation-id',
|
|
1204
|
+
]
|
|
1041
1205
|
for (const attr of testIdAttrs) {
|
|
1042
1206
|
const value = target.getAttribute(attr)
|
|
1043
1207
|
if (value) {
|
|
@@ -1066,7 +1230,7 @@ export async function getAriaSnapshot({ page, frame, locator, refFilter, interac
|
|
|
1066
1230
|
{
|
|
1067
1231
|
targets: targetHandles,
|
|
1068
1232
|
refData: result.refs,
|
|
1069
|
-
}
|
|
1233
|
+
},
|
|
1070
1234
|
)
|
|
1071
1235
|
|
|
1072
1236
|
return matchingRefs.map((ref) => {
|
|
@@ -1096,7 +1260,9 @@ export async function getAriaSnapshot({ page, frame, locator, refFilter, interac
|
|
|
1096
1260
|
}, scopeAttr)
|
|
1097
1261
|
}
|
|
1098
1262
|
if (oopifSessionId) {
|
|
1099
|
-
await session.send('Target.detachFromTarget', { sessionId: oopifSessionId }).catch(() => {
|
|
1263
|
+
await session.send('Target.detachFromTarget', { sessionId: oopifSessionId }).catch((e) => {
|
|
1264
|
+
console.error('[aria-snapshot] Failed to detach OOPIF session:', oopifSessionId, e)
|
|
1265
|
+
})
|
|
1100
1266
|
}
|
|
1101
1267
|
if (!cdp) {
|
|
1102
1268
|
await session.detach()
|
|
@@ -1140,7 +1306,7 @@ async function getLabelBoxesForRefs({
|
|
|
1140
1306
|
cdp?: ICDPSession
|
|
1141
1307
|
}): Promise<AriaLabel[]> {
|
|
1142
1308
|
const log = logger?.info ?? logger?.error ?? console.error
|
|
1143
|
-
const session = cdp || await getCDPSessionForPage({ page })
|
|
1309
|
+
const session = cdp || (await getCDPSessionForPage({ page }))
|
|
1144
1310
|
const sema = new Sema(maxConcurrency)
|
|
1145
1311
|
const labelRefs = refs.filter((ref) => {
|
|
1146
1312
|
return Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role)
|
|
@@ -1161,14 +1327,20 @@ async function getLabelBoxesForRefs({
|
|
|
1161
1327
|
await sema.acquire()
|
|
1162
1328
|
try {
|
|
1163
1329
|
const response = await Promise.race([
|
|
1164
|
-
session.send('DOM.getBoxModel', {
|
|
1330
|
+
session.send('DOM.getBoxModel', {
|
|
1331
|
+
backendNodeId: ref.backendNodeId,
|
|
1332
|
+
}) as Promise<Protocol.DOM.GetBoxModelResponse>,
|
|
1165
1333
|
new Promise<null>((resolve) => {
|
|
1166
|
-
setTimeout(() => {
|
|
1334
|
+
setTimeout(() => {
|
|
1335
|
+
resolve(null)
|
|
1336
|
+
}, BOX_MODEL_TIMEOUT_MS)
|
|
1167
1337
|
}),
|
|
1168
1338
|
])
|
|
1169
1339
|
completed++
|
|
1170
1340
|
if (completed % 50 === 0 || completed === labelRefs.length) {
|
|
1171
|
-
log(
|
|
1341
|
+
log(
|
|
1342
|
+
`[getLabelBoxesForRefs] progress: ${completed}/${labelRefs.length} (${timedOut} timeouts, ${failed} errors) - ${Date.now() - startTime}ms`,
|
|
1343
|
+
)
|
|
1172
1344
|
}
|
|
1173
1345
|
if (!response) {
|
|
1174
1346
|
timedOut++
|
|
@@ -1186,9 +1358,11 @@ async function getLabelBoxesForRefs({
|
|
|
1186
1358
|
} finally {
|
|
1187
1359
|
sema.release()
|
|
1188
1360
|
}
|
|
1189
|
-
})
|
|
1361
|
+
}),
|
|
1362
|
+
)
|
|
1363
|
+
log(
|
|
1364
|
+
`[getLabelBoxesForRefs] done: ${completed} completed, ${timedOut} timeouts, ${failed} errors - ${Date.now() - startTime}ms`,
|
|
1190
1365
|
)
|
|
1191
|
-
log(`[getLabelBoxesForRefs] done: ${completed} completed, ${timedOut} timeouts, ${failed} errors - ${Date.now() - startTime}ms`)
|
|
1192
1366
|
return labels.filter(isTruthy)
|
|
1193
1367
|
} finally {
|
|
1194
1368
|
if (!cdp) {
|
|
@@ -1217,7 +1391,12 @@ async function getLabelBoxesForRefs({
|
|
|
1217
1391
|
* await page.locator('[data-testid="submit-btn"]').click()
|
|
1218
1392
|
* ```
|
|
1219
1393
|
*/
|
|
1220
|
-
export async function showAriaRefLabels({
|
|
1394
|
+
export async function showAriaRefLabels({
|
|
1395
|
+
page,
|
|
1396
|
+
locator,
|
|
1397
|
+
interactiveOnly = true,
|
|
1398
|
+
logger,
|
|
1399
|
+
}: {
|
|
1221
1400
|
page: Page
|
|
1222
1401
|
locator?: Locator
|
|
1223
1402
|
interactiveOnly?: boolean
|
|
@@ -1240,11 +1419,15 @@ export async function showAriaRefLabels({ page, locator, interactiveOnly = true,
|
|
|
1240
1419
|
try {
|
|
1241
1420
|
const snapshotStart = Date.now()
|
|
1242
1421
|
const { snapshot, refs } = await getAriaSnapshot({ page, locator, interactiveOnly, cdp })
|
|
1243
|
-
const shortRefMap = new Map(
|
|
1244
|
-
|
|
1245
|
-
|
|
1422
|
+
const shortRefMap = new Map(
|
|
1423
|
+
refs.map((entry) => {
|
|
1424
|
+
return [entry.ref, entry.shortRef]
|
|
1425
|
+
}),
|
|
1426
|
+
)
|
|
1246
1427
|
const interactiveRefs = refs.filter((ref) => Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role))
|
|
1247
|
-
log(
|
|
1428
|
+
log(
|
|
1429
|
+
`[showAriaRefLabels] getAriaSnapshot: ${Date.now() - snapshotStart}ms (${refs.length} refs, ${interactiveRefs.length} interactive)`,
|
|
1430
|
+
)
|
|
1248
1431
|
|
|
1249
1432
|
const rootHandle = locator ? await locator.elementHandle() : null
|
|
1250
1433
|
|
|
@@ -1259,22 +1442,30 @@ export async function showAriaRefLabels({ page, locator, interactiveOnly = true,
|
|
|
1259
1442
|
log(`[showAriaRefLabels] getLabelBoxesForRefs: ${Date.now() - labelsStart}ms (${labels.length} boxes)`)
|
|
1260
1443
|
|
|
1261
1444
|
const renderStart = Date.now()
|
|
1262
|
-
const labelCount = await page.evaluate(
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1445
|
+
const labelCount = await page.evaluate(
|
|
1446
|
+
({ entries, root, interactiveOnly: intOnly }) => {
|
|
1447
|
+
const a11y = (
|
|
1448
|
+
globalThis as {
|
|
1449
|
+
__a11y?: {
|
|
1450
|
+
renderA11yLabels?: (labels: typeof entries) => number
|
|
1451
|
+
computeA11ySnapshot?: (options: { root: unknown; interactiveOnly: boolean; renderLabels: boolean }) => {
|
|
1452
|
+
labelCount: number
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
).__a11y
|
|
1457
|
+
if (a11y?.renderA11yLabels) {
|
|
1458
|
+
return a11y.renderA11yLabels(entries)
|
|
1267
1459
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
}, { entries: shortLabels, root: rootHandle, interactiveOnly })
|
|
1460
|
+
if (a11y?.computeA11ySnapshot) {
|
|
1461
|
+
const rootElement = root || document.body
|
|
1462
|
+
return a11y.computeA11ySnapshot({ root: rootElement, interactiveOnly: intOnly, renderLabels: true })
|
|
1463
|
+
.labelCount
|
|
1464
|
+
}
|
|
1465
|
+
throw new Error('a11y client not loaded')
|
|
1466
|
+
},
|
|
1467
|
+
{ entries: shortLabels, root: rootHandle, interactiveOnly },
|
|
1468
|
+
)
|
|
1278
1469
|
|
|
1279
1470
|
log(`[showAriaRefLabels] renderA11yLabels: ${Date.now() - renderStart}ms (${labelCount} labels)`)
|
|
1280
1471
|
log(`[showAriaRefLabels] total: ${Date.now() - startTime}ms`)
|
|
@@ -1324,7 +1515,13 @@ export async function hideAriaRefLabels({ page }: { page: Page }): Promise<void>
|
|
|
1324
1515
|
* await page.locator('[data-testid="submit-btn"]').click()
|
|
1325
1516
|
* ```
|
|
1326
1517
|
*/
|
|
1327
|
-
export async function screenshotWithAccessibilityLabels({
|
|
1518
|
+
export async function screenshotWithAccessibilityLabels({
|
|
1519
|
+
page,
|
|
1520
|
+
locator,
|
|
1521
|
+
interactiveOnly = true,
|
|
1522
|
+
collector,
|
|
1523
|
+
logger,
|
|
1524
|
+
}: {
|
|
1328
1525
|
page: Page
|
|
1329
1526
|
locator?: Locator
|
|
1330
1527
|
interactiveOnly?: boolean
|
|
@@ -1351,18 +1548,17 @@ export async function screenshotWithAccessibilityLabels({ page, locator, interac
|
|
|
1351
1548
|
const screenshotPath = path.join(tmpDir, filename)
|
|
1352
1549
|
|
|
1353
1550
|
// Get viewport size to clip screenshot to visible area
|
|
1354
|
-
const viewport = await page.evaluate('({ width: window.innerWidth, height: window.innerHeight })') as {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
const MAX_DIMENSION = 1568
|
|
1551
|
+
const viewport = (await page.evaluate('({ width: window.innerWidth, height: window.innerHeight })')) as {
|
|
1552
|
+
width: number
|
|
1553
|
+
height: number
|
|
1554
|
+
}
|
|
1359
1555
|
|
|
1360
1556
|
// Check if sharp is available for resizing
|
|
1361
1557
|
const sharp = await sharpPromise
|
|
1362
1558
|
|
|
1363
|
-
// Clip dimensions: if sharp unavailable, limit capture area to
|
|
1364
|
-
const clipWidth = sharp ? viewport.width : Math.min(viewport.width,
|
|
1365
|
-
const clipHeight = sharp ? viewport.height : Math.min(viewport.height,
|
|
1559
|
+
// Clip dimensions: if sharp unavailable, limit capture area to LLM_MAX_DIMENSION
|
|
1560
|
+
const clipWidth = sharp ? viewport.width : Math.min(viewport.width, LLM_MAX_DIMENSION)
|
|
1561
|
+
const clipHeight = sharp ? viewport.height : Math.min(viewport.height, LLM_MAX_DIMENSION)
|
|
1366
1562
|
|
|
1367
1563
|
// Take viewport screenshot with scale: 'css' to ignore device pixel ratio
|
|
1368
1564
|
const screenshotStart = Date.now()
|
|
@@ -1376,23 +1572,16 @@ export async function screenshotWithAccessibilityLabels({ page, locator, interac
|
|
|
1376
1572
|
log(`page.screenshot: ${Date.now() - screenshotStart}ms`)
|
|
1377
1573
|
}
|
|
1378
1574
|
|
|
1379
|
-
// Resize with
|
|
1575
|
+
// Resize with resizeImage if sharp available, otherwise use clipped raw buffer
|
|
1380
1576
|
const resizeStart = Date.now()
|
|
1381
1577
|
const buffer = await (async () => {
|
|
1382
1578
|
if (!sharp) {
|
|
1383
|
-
logger?.error?.('[playwriter] sharp not available, using clipped screenshot (max',
|
|
1579
|
+
logger?.error?.('[playwriter] sharp not available, using clipped screenshot (max', LLM_MAX_DIMENSION, 'px)')
|
|
1384
1580
|
return rawBuffer
|
|
1385
1581
|
}
|
|
1386
1582
|
try {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
width: MAX_DIMENSION,
|
|
1390
|
-
height: MAX_DIMENSION,
|
|
1391
|
-
fit: 'inside', // Scale down to fit, preserving aspect ratio
|
|
1392
|
-
withoutEnlargement: true, // Don't upscale small images
|
|
1393
|
-
})
|
|
1394
|
-
.jpeg({ quality: 80 })
|
|
1395
|
-
.toBuffer()
|
|
1583
|
+
const result = await resizeImage({ input: rawBuffer })
|
|
1584
|
+
return result.buffer
|
|
1396
1585
|
} catch (err) {
|
|
1397
1586
|
logger?.error?.('[playwriter] sharp resize failed, using raw buffer:', err)
|
|
1398
1587
|
return rawBuffer
|