playwriter 0.0.63 → 0.0.80
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/aria-snapshot.d.ts +41 -3
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +131 -54
- 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 +408 -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 +295 -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 +281 -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 +594 -255
- package/dist/protocol.d.ts +4 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/readability.js +1 -1
- 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 +44 -10
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +187 -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 +54 -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 +42 -4
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +88 -13
- package/dist/screen-recording.js.map +1 -1
- 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 +303 -116
- package/src/aria-snapshot.unit.test.ts +199 -141
- package/src/aria-snapshots/github-raw.txt +1 -1
- package/src/aria-snapshots/hackernews-interactive.txt +240 -240
- package/src/aria-snapshots/hackernews-raw.txt +270 -270
- 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 +949 -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 +372 -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 +368 -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 +107 -0
- package/src/recording-relay.ts +20 -12
- package/src/relay-client.ts +84 -17
- package/src/relay-core.test.ts +761 -583
- package/src/relay-navigation.test.ts +517 -484
- 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.ts +175 -31
- package/src/skill.md +619 -271
- package/src/snapshot-tools.test.ts +580 -528
- package/src/snapshots/shadcn-ui-accessibility-full.md +181 -183
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +119 -121
- 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) => {
|
|
@@ -1140,7 +1304,7 @@ async function getLabelBoxesForRefs({
|
|
|
1140
1304
|
cdp?: ICDPSession
|
|
1141
1305
|
}): Promise<AriaLabel[]> {
|
|
1142
1306
|
const log = logger?.info ?? logger?.error ?? console.error
|
|
1143
|
-
const session = cdp || await getCDPSessionForPage({ page })
|
|
1307
|
+
const session = cdp || (await getCDPSessionForPage({ page }))
|
|
1144
1308
|
const sema = new Sema(maxConcurrency)
|
|
1145
1309
|
const labelRefs = refs.filter((ref) => {
|
|
1146
1310
|
return Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role)
|
|
@@ -1161,14 +1325,20 @@ async function getLabelBoxesForRefs({
|
|
|
1161
1325
|
await sema.acquire()
|
|
1162
1326
|
try {
|
|
1163
1327
|
const response = await Promise.race([
|
|
1164
|
-
session.send('DOM.getBoxModel', {
|
|
1328
|
+
session.send('DOM.getBoxModel', {
|
|
1329
|
+
backendNodeId: ref.backendNodeId,
|
|
1330
|
+
}) as Promise<Protocol.DOM.GetBoxModelResponse>,
|
|
1165
1331
|
new Promise<null>((resolve) => {
|
|
1166
|
-
setTimeout(() => {
|
|
1332
|
+
setTimeout(() => {
|
|
1333
|
+
resolve(null)
|
|
1334
|
+
}, BOX_MODEL_TIMEOUT_MS)
|
|
1167
1335
|
}),
|
|
1168
1336
|
])
|
|
1169
1337
|
completed++
|
|
1170
1338
|
if (completed % 50 === 0 || completed === labelRefs.length) {
|
|
1171
|
-
log(
|
|
1339
|
+
log(
|
|
1340
|
+
`[getLabelBoxesForRefs] progress: ${completed}/${labelRefs.length} (${timedOut} timeouts, ${failed} errors) - ${Date.now() - startTime}ms`,
|
|
1341
|
+
)
|
|
1172
1342
|
}
|
|
1173
1343
|
if (!response) {
|
|
1174
1344
|
timedOut++
|
|
@@ -1186,9 +1356,11 @@ async function getLabelBoxesForRefs({
|
|
|
1186
1356
|
} finally {
|
|
1187
1357
|
sema.release()
|
|
1188
1358
|
}
|
|
1189
|
-
})
|
|
1359
|
+
}),
|
|
1360
|
+
)
|
|
1361
|
+
log(
|
|
1362
|
+
`[getLabelBoxesForRefs] done: ${completed} completed, ${timedOut} timeouts, ${failed} errors - ${Date.now() - startTime}ms`,
|
|
1190
1363
|
)
|
|
1191
|
-
log(`[getLabelBoxesForRefs] done: ${completed} completed, ${timedOut} timeouts, ${failed} errors - ${Date.now() - startTime}ms`)
|
|
1192
1364
|
return labels.filter(isTruthy)
|
|
1193
1365
|
} finally {
|
|
1194
1366
|
if (!cdp) {
|
|
@@ -1217,7 +1389,12 @@ async function getLabelBoxesForRefs({
|
|
|
1217
1389
|
* await page.locator('[data-testid="submit-btn"]').click()
|
|
1218
1390
|
* ```
|
|
1219
1391
|
*/
|
|
1220
|
-
export async function showAriaRefLabels({
|
|
1392
|
+
export async function showAriaRefLabels({
|
|
1393
|
+
page,
|
|
1394
|
+
locator,
|
|
1395
|
+
interactiveOnly = true,
|
|
1396
|
+
logger,
|
|
1397
|
+
}: {
|
|
1221
1398
|
page: Page
|
|
1222
1399
|
locator?: Locator
|
|
1223
1400
|
interactiveOnly?: boolean
|
|
@@ -1240,11 +1417,15 @@ export async function showAriaRefLabels({ page, locator, interactiveOnly = true,
|
|
|
1240
1417
|
try {
|
|
1241
1418
|
const snapshotStart = Date.now()
|
|
1242
1419
|
const { snapshot, refs } = await getAriaSnapshot({ page, locator, interactiveOnly, cdp })
|
|
1243
|
-
const shortRefMap = new Map(
|
|
1244
|
-
|
|
1245
|
-
|
|
1420
|
+
const shortRefMap = new Map(
|
|
1421
|
+
refs.map((entry) => {
|
|
1422
|
+
return [entry.ref, entry.shortRef]
|
|
1423
|
+
}),
|
|
1424
|
+
)
|
|
1246
1425
|
const interactiveRefs = refs.filter((ref) => Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role))
|
|
1247
|
-
log(
|
|
1426
|
+
log(
|
|
1427
|
+
`[showAriaRefLabels] getAriaSnapshot: ${Date.now() - snapshotStart}ms (${refs.length} refs, ${interactiveRefs.length} interactive)`,
|
|
1428
|
+
)
|
|
1248
1429
|
|
|
1249
1430
|
const rootHandle = locator ? await locator.elementHandle() : null
|
|
1250
1431
|
|
|
@@ -1259,22 +1440,30 @@ export async function showAriaRefLabels({ page, locator, interactiveOnly = true,
|
|
|
1259
1440
|
log(`[showAriaRefLabels] getLabelBoxesForRefs: ${Date.now() - labelsStart}ms (${labels.length} boxes)`)
|
|
1260
1441
|
|
|
1261
1442
|
const renderStart = Date.now()
|
|
1262
|
-
const labelCount = await page.evaluate(
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1443
|
+
const labelCount = await page.evaluate(
|
|
1444
|
+
({ entries, root, interactiveOnly: intOnly }) => {
|
|
1445
|
+
const a11y = (
|
|
1446
|
+
globalThis as {
|
|
1447
|
+
__a11y?: {
|
|
1448
|
+
renderA11yLabels?: (labels: typeof entries) => number
|
|
1449
|
+
computeA11ySnapshot?: (options: { root: unknown; interactiveOnly: boolean; renderLabels: boolean }) => {
|
|
1450
|
+
labelCount: number
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
).__a11y
|
|
1455
|
+
if (a11y?.renderA11yLabels) {
|
|
1456
|
+
return a11y.renderA11yLabels(entries)
|
|
1267
1457
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
}, { entries: shortLabels, root: rootHandle, interactiveOnly })
|
|
1458
|
+
if (a11y?.computeA11ySnapshot) {
|
|
1459
|
+
const rootElement = root || document.body
|
|
1460
|
+
return a11y.computeA11ySnapshot({ root: rootElement, interactiveOnly: intOnly, renderLabels: true })
|
|
1461
|
+
.labelCount
|
|
1462
|
+
}
|
|
1463
|
+
throw new Error('a11y client not loaded')
|
|
1464
|
+
},
|
|
1465
|
+
{ entries: shortLabels, root: rootHandle, interactiveOnly },
|
|
1466
|
+
)
|
|
1278
1467
|
|
|
1279
1468
|
log(`[showAriaRefLabels] renderA11yLabels: ${Date.now() - renderStart}ms (${labelCount} labels)`)
|
|
1280
1469
|
log(`[showAriaRefLabels] total: ${Date.now() - startTime}ms`)
|
|
@@ -1324,7 +1513,13 @@ export async function hideAriaRefLabels({ page }: { page: Page }): Promise<void>
|
|
|
1324
1513
|
* await page.locator('[data-testid="submit-btn"]').click()
|
|
1325
1514
|
* ```
|
|
1326
1515
|
*/
|
|
1327
|
-
export async function screenshotWithAccessibilityLabels({
|
|
1516
|
+
export async function screenshotWithAccessibilityLabels({
|
|
1517
|
+
page,
|
|
1518
|
+
locator,
|
|
1519
|
+
interactiveOnly = true,
|
|
1520
|
+
collector,
|
|
1521
|
+
logger,
|
|
1522
|
+
}: {
|
|
1328
1523
|
page: Page
|
|
1329
1524
|
locator?: Locator
|
|
1330
1525
|
interactiveOnly?: boolean
|
|
@@ -1351,18 +1546,17 @@ export async function screenshotWithAccessibilityLabels({ page, locator, interac
|
|
|
1351
1546
|
const screenshotPath = path.join(tmpDir, filename)
|
|
1352
1547
|
|
|
1353
1548
|
// 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
|
|
1549
|
+
const viewport = (await page.evaluate('({ width: window.innerWidth, height: window.innerHeight })')) as {
|
|
1550
|
+
width: number
|
|
1551
|
+
height: number
|
|
1552
|
+
}
|
|
1359
1553
|
|
|
1360
1554
|
// Check if sharp is available for resizing
|
|
1361
1555
|
const sharp = await sharpPromise
|
|
1362
1556
|
|
|
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,
|
|
1557
|
+
// Clip dimensions: if sharp unavailable, limit capture area to LLM_MAX_DIMENSION
|
|
1558
|
+
const clipWidth = sharp ? viewport.width : Math.min(viewport.width, LLM_MAX_DIMENSION)
|
|
1559
|
+
const clipHeight = sharp ? viewport.height : Math.min(viewport.height, LLM_MAX_DIMENSION)
|
|
1366
1560
|
|
|
1367
1561
|
// Take viewport screenshot with scale: 'css' to ignore device pixel ratio
|
|
1368
1562
|
const screenshotStart = Date.now()
|
|
@@ -1376,23 +1570,16 @@ export async function screenshotWithAccessibilityLabels({ page, locator, interac
|
|
|
1376
1570
|
log(`page.screenshot: ${Date.now() - screenshotStart}ms`)
|
|
1377
1571
|
}
|
|
1378
1572
|
|
|
1379
|
-
// Resize with
|
|
1573
|
+
// Resize with resizeImage if sharp available, otherwise use clipped raw buffer
|
|
1380
1574
|
const resizeStart = Date.now()
|
|
1381
1575
|
const buffer = await (async () => {
|
|
1382
1576
|
if (!sharp) {
|
|
1383
|
-
logger?.error?.('[playwriter] sharp not available, using clipped screenshot (max',
|
|
1577
|
+
logger?.error?.('[playwriter] sharp not available, using clipped screenshot (max', LLM_MAX_DIMENSION, 'px)')
|
|
1384
1578
|
return rawBuffer
|
|
1385
1579
|
}
|
|
1386
1580
|
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()
|
|
1581
|
+
const result = await resizeImage({ input: rawBuffer })
|
|
1582
|
+
return result.buffer
|
|
1396
1583
|
} catch (err) {
|
|
1397
1584
|
logger?.error?.('[playwriter] sharp resize failed, using raw buffer:', err)
|
|
1398
1585
|
return rawBuffer
|