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/executor.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { ScopedFS } from './scoped-fs.js'
|
|
|
26
26
|
import {
|
|
27
27
|
screenshotWithAccessibilityLabels,
|
|
28
28
|
getAriaSnapshot,
|
|
29
|
+
resizeImage,
|
|
29
30
|
type ScreenshotResult,
|
|
30
31
|
type SnapshotFormat,
|
|
31
32
|
} from './aria-snapshot.js'
|
|
@@ -33,7 +34,10 @@ import { createGhostBrowserChrome, type GhostBrowserCommandResult } from './ghos
|
|
|
33
34
|
export type { SnapshotFormat }
|
|
34
35
|
import { getCleanHTML, type GetCleanHTMLOptions } from './clean-html.js'
|
|
35
36
|
import { getPageMarkdown, type GetPageMarkdownOptions } from './page-markdown.js'
|
|
36
|
-
import {
|
|
37
|
+
import { createRecordingApi } from './screen-recording.js'
|
|
38
|
+
import { createDemoVideo } from './ffmpeg.js'
|
|
39
|
+
import { type GhostCursorClientOptions } from './ghost-cursor.js'
|
|
40
|
+
import { RecordingGhostCursorController } from './recording-ghost-cursor.js'
|
|
37
41
|
|
|
38
42
|
const __filename = fileURLToPath(import.meta.url)
|
|
39
43
|
const __dirname = path.dirname(__filename)
|
|
@@ -65,10 +69,11 @@ const usefulGlobals = {
|
|
|
65
69
|
} as const
|
|
66
70
|
|
|
67
71
|
/**
|
|
68
|
-
*
|
|
69
|
-
* Returns
|
|
72
|
+
* Parse code and check if it's a single expression that should be auto-returned.
|
|
73
|
+
* Returns the exact expression source (without trailing semicolon) using AST
|
|
74
|
+
* node offsets, or null if the code should not be auto-wrapped. See #58.
|
|
70
75
|
*/
|
|
71
|
-
export function
|
|
76
|
+
export function getAutoReturnExpression(code: string): string | null {
|
|
72
77
|
try {
|
|
73
78
|
const ast = acorn.parse(code, {
|
|
74
79
|
ecmaVersion: 'latest',
|
|
@@ -79,19 +84,19 @@ export function shouldAutoReturn(code: string): boolean {
|
|
|
79
84
|
|
|
80
85
|
// Must be exactly one statement
|
|
81
86
|
if (ast.body.length !== 1) {
|
|
82
|
-
return
|
|
87
|
+
return null
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
const stmt = ast.body[0]
|
|
86
91
|
|
|
87
92
|
// If it's already a return statement, don't auto-wrap
|
|
88
93
|
if (stmt.type === 'ReturnStatement') {
|
|
89
|
-
return
|
|
94
|
+
return null
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
// Must be an ExpressionStatement
|
|
93
98
|
if (stmt.type !== 'ExpressionStatement') {
|
|
94
|
-
return
|
|
99
|
+
return null
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
// Don't auto-return side-effect expressions
|
|
@@ -101,24 +106,44 @@ export function shouldAutoReturn(code: string): boolean {
|
|
|
101
106
|
expr.type === 'UpdateExpression' ||
|
|
102
107
|
(expr.type === 'UnaryExpression' && (expr as acorn.UnaryExpression).operator === 'delete')
|
|
103
108
|
) {
|
|
104
|
-
return
|
|
109
|
+
return null
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
// Don't auto-return sequence expressions that contain assignments
|
|
108
113
|
if (expr.type === 'SequenceExpression') {
|
|
109
114
|
const hasAssignment = expr.expressions.some((e: acorn.Expression) => e.type === 'AssignmentExpression')
|
|
110
115
|
if (hasAssignment) {
|
|
111
|
-
return
|
|
116
|
+
return null
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
|
|
120
|
+
// Use the expression node's start/end offsets to extract just the expression
|
|
121
|
+
// source, excluding any trailing semicolon. This is more robust than regex.
|
|
122
|
+
return code.slice(expr.start, expr.end)
|
|
116
123
|
} catch {
|
|
117
124
|
// Parse failed, don't auto-return
|
|
118
|
-
return
|
|
125
|
+
return null
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
/** Backward-compatible helper: returns true if code should be auto-wrapped. */
|
|
130
|
+
export function shouldAutoReturn(code: string): boolean {
|
|
131
|
+
return getAutoReturnExpression(code) !== null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Wraps user code in an async IIFE for vm execution.
|
|
136
|
+
* Uses AST node offsets to extract the expression without trailing semicolons,
|
|
137
|
+
* avoiding SyntaxError when embedding inside `return await (...)`. See #58.
|
|
138
|
+
*/
|
|
139
|
+
export function wrapCode(code: string): string {
|
|
140
|
+
const expr = getAutoReturnExpression(code)
|
|
141
|
+
if (expr !== null) {
|
|
142
|
+
return `(async () => { return await (${expr}) })()`
|
|
143
|
+
}
|
|
144
|
+
return `(async () => { ${code} })()`
|
|
145
|
+
}
|
|
146
|
+
|
|
122
147
|
const EXTENSION_NOT_CONNECTED_ERROR = `The Playwriter Chrome extension is not connected. Make sure you have:
|
|
123
148
|
1. Installed the extension: https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe
|
|
124
149
|
2. Clicked the extension icon on a tab to enable it (or refreshed the page if just installed)`
|
|
@@ -173,6 +198,15 @@ export interface ExecuteResult {
|
|
|
173
198
|
isError: boolean
|
|
174
199
|
}
|
|
175
200
|
|
|
201
|
+
interface WarningEvent {
|
|
202
|
+
id: number
|
|
203
|
+
message: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface WarningScope {
|
|
207
|
+
cursor: number
|
|
208
|
+
}
|
|
209
|
+
|
|
176
210
|
export interface ExecutorLogger {
|
|
177
211
|
log(...args: any[]): void
|
|
178
212
|
error(...args: any[]): void
|
|
@@ -217,8 +251,20 @@ export class PlaywrightExecutor {
|
|
|
217
251
|
|
|
218
252
|
private userState: Record<string, any> = {}
|
|
219
253
|
private browserLogs: Map<string, string[]> = new Map()
|
|
220
|
-
private lastSnapshots: WeakMap<Page, string
|
|
254
|
+
private lastSnapshots: WeakMap<Page, Map<string, string>> = new WeakMap()
|
|
221
255
|
private lastRefToLocator: WeakMap<Page, Map<string, string>> = new WeakMap()
|
|
256
|
+
private warningEvents: WarningEvent[] = []
|
|
257
|
+
private nextWarningEventId = 0
|
|
258
|
+
private lastDeliveredWarningEventId = 0
|
|
259
|
+
|
|
260
|
+
// Recording timestamp tracking: when recording is active, each execute()
|
|
261
|
+
// call pushes {start, end} (seconds relative to recordingStartedAt).
|
|
262
|
+
// Returned by stopRecording() so the model can speed up idle sections.
|
|
263
|
+
private recordingStartedAt: number | null = null
|
|
264
|
+
private executionTimestamps: Array<{ start: number; end: number }> = []
|
|
265
|
+
private activeWarningScopes = new Set<WarningScope>()
|
|
266
|
+
private pagesWithListeners = new WeakSet<Page>()
|
|
267
|
+
private suppressPageCloseWarnings = false
|
|
222
268
|
|
|
223
269
|
private scopedFs: ScopedFS
|
|
224
270
|
private sandboxedRequire: NodeRequire
|
|
@@ -273,16 +319,6 @@ export class PlaywrightExecutor {
|
|
|
273
319
|
options.deviceScaleFactor = 2
|
|
274
320
|
}
|
|
275
321
|
|
|
276
|
-
private async preserveSystemColorScheme(context: BrowserContext): Promise<void> {
|
|
277
|
-
const options = (context as any)._options
|
|
278
|
-
if (!options) {
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
options.colorScheme = 'no-override'
|
|
282
|
-
options.reducedMotion = 'no-override'
|
|
283
|
-
options.forcedColors = 'no-override'
|
|
284
|
-
}
|
|
285
|
-
|
|
286
322
|
private clearUserState() {
|
|
287
323
|
Object.keys(this.userState).forEach((key) => delete this.userState[key])
|
|
288
324
|
}
|
|
@@ -294,6 +330,49 @@ export class PlaywrightExecutor {
|
|
|
294
330
|
this.context = null
|
|
295
331
|
}
|
|
296
332
|
|
|
333
|
+
private enqueueWarning(message: string) {
|
|
334
|
+
this.nextWarningEventId += 1
|
|
335
|
+
this.warningEvents.push({ id: this.nextWarningEventId, message })
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private beginWarningScope(): WarningScope {
|
|
339
|
+
const scope: WarningScope = {
|
|
340
|
+
cursor: this.nextWarningEventId,
|
|
341
|
+
}
|
|
342
|
+
this.activeWarningScopes.add(scope)
|
|
343
|
+
return scope
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private flushWarningsForScope(scope: WarningScope): string {
|
|
347
|
+
const relevantWarnings = this.warningEvents.filter((warning) => {
|
|
348
|
+
return warning.id > scope.cursor
|
|
349
|
+
})
|
|
350
|
+
const latestWarningId = relevantWarnings.at(-1)?.id
|
|
351
|
+
if (latestWarningId && latestWarningId > this.lastDeliveredWarningEventId) {
|
|
352
|
+
this.lastDeliveredWarningEventId = latestWarningId
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this.activeWarningScopes.delete(scope)
|
|
356
|
+
this.pruneDeliveredWarnings()
|
|
357
|
+
|
|
358
|
+
if (relevantWarnings.length === 0) {
|
|
359
|
+
return ''
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return `${relevantWarnings.map((warning) => `[WARNING] ${warning.message}`).join('\n')}\n`
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private pruneDeliveredWarnings() {
|
|
366
|
+
const activeCursors = [...this.activeWarningScopes].map((scope) => {
|
|
367
|
+
return scope.cursor
|
|
368
|
+
})
|
|
369
|
+
const minActiveCursor = activeCursors.length > 0 ? Math.min(...activeCursors) : this.lastDeliveredWarningEventId
|
|
370
|
+
const pruneBeforeOrAt = Math.min(this.lastDeliveredWarningEventId, minActiveCursor)
|
|
371
|
+
this.warningEvents = this.warningEvents.filter((warning) => {
|
|
372
|
+
return warning.id > pruneBeforeOrAt
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
297
376
|
private warnIfExtensionOutdated(playwriterVersion: string | null) {
|
|
298
377
|
if (this.hasWarnedExtensionOutdated) {
|
|
299
378
|
return
|
|
@@ -305,9 +384,96 @@ export class PlaywrightExecutor {
|
|
|
305
384
|
}
|
|
306
385
|
}
|
|
307
386
|
|
|
387
|
+
private setupPageListeners(page: Page) {
|
|
388
|
+
if (this.pagesWithListeners.has(page)) {
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
this.pagesWithListeners.add(page)
|
|
392
|
+
this.setupPageCloseDetection(page)
|
|
393
|
+
this.setupPageConsoleListener(page)
|
|
394
|
+
this.setupPopupDetection(page)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private setupPageCloseDetection(page: Page) {
|
|
398
|
+
page.on('close', () => {
|
|
399
|
+
const stateKeysForClosedPage = Object.entries(this.userState)
|
|
400
|
+
.filter(([, value]) => {
|
|
401
|
+
return value === page
|
|
402
|
+
})
|
|
403
|
+
.map(([key]) => key)
|
|
404
|
+
|
|
405
|
+
const wasCurrentPage = this.page === page
|
|
406
|
+
let replacementPageInfo: { index: string; url: string } | null = null
|
|
407
|
+
|
|
408
|
+
if (wasCurrentPage) {
|
|
409
|
+
this.page = null
|
|
410
|
+
const context = this.context || page.context()
|
|
411
|
+
const openPages = context.pages().filter((candidate) => {
|
|
412
|
+
return !candidate.isClosed()
|
|
413
|
+
})
|
|
414
|
+
if (openPages.length > 0) {
|
|
415
|
+
const replacementPage = openPages[0]
|
|
416
|
+
this.page = replacementPage
|
|
417
|
+
const replacementIndex = context.pages().indexOf(replacementPage)
|
|
418
|
+
replacementPageInfo = {
|
|
419
|
+
index: replacementIndex >= 0 ? String(replacementIndex) : 'unknown',
|
|
420
|
+
url: replacementPage.url() || 'unknown',
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!this.isConnected || this.suppressPageCloseWarnings || stateKeysForClosedPage.length === 0) {
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const stateKeyLabel = stateKeysForClosedPage.map((key) => `state.${key}`).join(', ')
|
|
430
|
+
const closedUrl = page.url() || 'unknown'
|
|
431
|
+
|
|
432
|
+
if (!wasCurrentPage) {
|
|
433
|
+
this.enqueueWarning(
|
|
434
|
+
`Page closed (url: ${closedUrl}) for ${stateKeyLabel}. ` +
|
|
435
|
+
`Assign a new open page to ${stateKeyLabel} before reusing it.`,
|
|
436
|
+
)
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (replacementPageInfo) {
|
|
441
|
+
this.enqueueWarning(
|
|
442
|
+
`The current page in ${stateKeyLabel} was closed (url: ${closedUrl}). ` +
|
|
443
|
+
`Switched active page to index ${replacementPageInfo.index} (url: ${replacementPageInfo.url}). ` +
|
|
444
|
+
`Reassign ${stateKeyLabel} before using it again.`,
|
|
445
|
+
)
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
this.enqueueWarning(
|
|
450
|
+
`The current page in ${stateKeyLabel} was closed (url: ${closedUrl}). ` +
|
|
451
|
+
`No open pages remain. Open a tab with Playwriter enabled, then reassign ${stateKeyLabel}.`,
|
|
452
|
+
)
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private setupPopupDetection(page: Page) {
|
|
457
|
+
// Listen for popup events (window.open, target=_blank) on each page.
|
|
458
|
+
// This is more reliable than checking page.opener() on context 'page' event,
|
|
459
|
+
// which also fires for context.newPage() and CDP reconnection scenarios.
|
|
460
|
+
page.on('popup', (popup) => {
|
|
461
|
+
const context = page.context()
|
|
462
|
+
const pages = context.pages()
|
|
463
|
+
const rawIndex = pages.indexOf(popup)
|
|
464
|
+
const pageIndex = rawIndex >= 0 ? String(rawIndex) : 'unknown'
|
|
465
|
+
const url = popup.url()
|
|
466
|
+
this.enqueueWarning(
|
|
467
|
+
`Popup window detected (page index ${pageIndex}, url: ${url}). ` +
|
|
468
|
+
`Popup windows cannot be controlled by playwriter. ` +
|
|
469
|
+
`Repeat the interaction in a way that does not open a popup, or navigate to the URL directly in a new tab.`,
|
|
470
|
+
)
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
|
|
308
474
|
private setupPageConsoleListener(page: Page) {
|
|
309
475
|
// Use targetId() if available, fallback to internal _guid for CDP connections
|
|
310
|
-
const targetId = page.targetId() || (page as any)._guid as string | undefined
|
|
476
|
+
const targetId = page.targetId() || ((page as any)._guid as string | undefined)
|
|
311
477
|
if (!targetId) {
|
|
312
478
|
return
|
|
313
479
|
}
|
|
@@ -343,7 +509,11 @@ export class PlaywrightExecutor {
|
|
|
343
509
|
})
|
|
344
510
|
}
|
|
345
511
|
|
|
346
|
-
private async checkExtensionStatus(): Promise<{
|
|
512
|
+
private async checkExtensionStatus(): Promise<{
|
|
513
|
+
connected: boolean
|
|
514
|
+
activeTargets: number
|
|
515
|
+
playwriterVersion: string | null
|
|
516
|
+
}> {
|
|
347
517
|
const { host = '127.0.0.1', port = 19988, extensionId } = this.cdpConfig
|
|
348
518
|
const { httpBaseUrl } = parseRelayHost(host, port)
|
|
349
519
|
const notConnected = { connected: false, activeTargets: 0, playwriterVersion: null }
|
|
@@ -359,10 +529,19 @@ export class PlaywrightExecutor {
|
|
|
359
529
|
if (!fallback.ok) {
|
|
360
530
|
return notConnected
|
|
361
531
|
}
|
|
362
|
-
return (await fallback.json()) as {
|
|
532
|
+
return (await fallback.json()) as {
|
|
533
|
+
connected: boolean
|
|
534
|
+
activeTargets: number
|
|
535
|
+
playwriterVersion: string | null
|
|
536
|
+
}
|
|
363
537
|
}
|
|
364
|
-
const data = await response.json() as {
|
|
365
|
-
extensions: Array<{
|
|
538
|
+
const data = (await response.json()) as {
|
|
539
|
+
extensions: Array<{
|
|
540
|
+
extensionId: string
|
|
541
|
+
stableKey?: string
|
|
542
|
+
activeTargets: number
|
|
543
|
+
playwriterVersion?: string | null
|
|
544
|
+
}>
|
|
366
545
|
}
|
|
367
546
|
const extension = data.extensions.find((item) => {
|
|
368
547
|
return item.extensionId === extensionId || item.stableKey === extensionId
|
|
@@ -370,7 +549,11 @@ export class PlaywrightExecutor {
|
|
|
370
549
|
if (!extension) {
|
|
371
550
|
return notConnected
|
|
372
551
|
}
|
|
373
|
-
return {
|
|
552
|
+
return {
|
|
553
|
+
connected: true,
|
|
554
|
+
activeTargets: extension.activeTargets,
|
|
555
|
+
playwriterVersion: extension?.playwriterVersion || null,
|
|
556
|
+
}
|
|
374
557
|
}
|
|
375
558
|
|
|
376
559
|
const response = await fetch(`${httpBaseUrl}/extension/status`, {
|
|
@@ -409,14 +592,19 @@ export class PlaywrightExecutor {
|
|
|
409
592
|
const contexts = browser.contexts()
|
|
410
593
|
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
|
411
594
|
|
|
595
|
+
// Action timeout (click, fill, hover, etc.) is longer to tolerate slower
|
|
596
|
+
// SPA/Turbo navigations and post-click settling on real sites.
|
|
597
|
+
// Navigation timeout (goto, reload) remains separate.
|
|
598
|
+
context.setDefaultTimeout(60000)
|
|
599
|
+
context.setDefaultNavigationTimeout(10000)
|
|
600
|
+
|
|
412
601
|
context.on('page', (page) => {
|
|
413
|
-
this.
|
|
602
|
+
this.setupPageListeners(page)
|
|
414
603
|
})
|
|
415
604
|
|
|
416
|
-
context.pages().forEach((p) => this.
|
|
605
|
+
context.pages().forEach((p) => this.setupPageListeners(p))
|
|
417
606
|
const page = await this.ensurePageForContext({ context, timeout: 10000 })
|
|
418
607
|
|
|
419
|
-
await this.preserveSystemColorScheme(context)
|
|
420
608
|
await this.setDeviceScaleFactorForMacOS(context)
|
|
421
609
|
|
|
422
610
|
this.browser = browser
|
|
@@ -455,10 +643,13 @@ export class PlaywrightExecutor {
|
|
|
455
643
|
|
|
456
644
|
async reset(): Promise<{ page: Page; context: BrowserContext }> {
|
|
457
645
|
if (this.browser) {
|
|
646
|
+
this.suppressPageCloseWarnings = true
|
|
458
647
|
try {
|
|
459
648
|
await this.browser.close()
|
|
460
649
|
} catch (e) {
|
|
461
650
|
this.logger.error('Error closing browser:', e)
|
|
651
|
+
} finally {
|
|
652
|
+
this.suppressPageCloseWarnings = false
|
|
462
653
|
}
|
|
463
654
|
}
|
|
464
655
|
|
|
@@ -484,14 +675,19 @@ export class PlaywrightExecutor {
|
|
|
484
675
|
const contexts = browser.contexts()
|
|
485
676
|
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
|
486
677
|
|
|
678
|
+
// Action timeout (click, fill, hover, etc.) is longer to tolerate slower
|
|
679
|
+
// SPA/Turbo navigations and post-click settling on real sites.
|
|
680
|
+
// Navigation timeout (goto, reload) remains separate.
|
|
681
|
+
context.setDefaultTimeout(60000)
|
|
682
|
+
context.setDefaultNavigationTimeout(10000)
|
|
683
|
+
|
|
487
684
|
context.on('page', (page) => {
|
|
488
|
-
this.
|
|
685
|
+
this.setupPageListeners(page)
|
|
489
686
|
})
|
|
490
687
|
|
|
491
|
-
context.pages().forEach((p) => this.
|
|
688
|
+
context.pages().forEach((p) => this.setupPageListeners(p))
|
|
492
689
|
const page = await this.ensurePageForContext({ context, timeout: 10000 })
|
|
493
690
|
|
|
494
|
-
await this.preserveSystemColorScheme(context)
|
|
495
691
|
await this.setDeviceScaleFactorForMacOS(context)
|
|
496
692
|
|
|
497
693
|
this.browser = browser
|
|
@@ -504,6 +700,7 @@ export class PlaywrightExecutor {
|
|
|
504
700
|
|
|
505
701
|
async execute(code: string, timeout = 10000): Promise<ExecuteResult> {
|
|
506
702
|
const consoleLogs: Array<{ method: string; args: any[] }> = []
|
|
703
|
+
const warningScope = this.beginWarningScope()
|
|
507
704
|
|
|
508
705
|
const formatConsoleLogs = (logs: Array<{ method: string; args: any[] }>, prefix = 'Console output') => {
|
|
509
706
|
if (logs.length === 0) {
|
|
@@ -514,7 +711,13 @@ export class PlaywrightExecutor {
|
|
|
514
711
|
const formattedArgs = args
|
|
515
712
|
.map((arg) => {
|
|
516
713
|
if (typeof arg === 'string') return arg
|
|
517
|
-
return util.inspect(arg, {
|
|
714
|
+
return util.inspect(arg, {
|
|
715
|
+
depth: 4,
|
|
716
|
+
colors: false,
|
|
717
|
+
maxArrayLength: 100,
|
|
718
|
+
maxStringLength: 1000,
|
|
719
|
+
breakLength: 80,
|
|
720
|
+
})
|
|
518
721
|
})
|
|
519
722
|
.join(' ')
|
|
520
723
|
text += `[${method}] ${formattedArgs}\n`
|
|
@@ -526,7 +729,6 @@ export class PlaywrightExecutor {
|
|
|
526
729
|
await this.ensureConnection()
|
|
527
730
|
const page = await this.getCurrentPage(timeout)
|
|
528
731
|
const context = this.context || page.context()
|
|
529
|
-
context.setDefaultTimeout(timeout)
|
|
530
732
|
|
|
531
733
|
this.logger.log('Executing code:', code)
|
|
532
734
|
|
|
@@ -548,7 +750,7 @@ export class PlaywrightExecutor {
|
|
|
548
750
|
},
|
|
549
751
|
}
|
|
550
752
|
|
|
551
|
-
const
|
|
753
|
+
const snapshot = async (options: {
|
|
552
754
|
page?: Page
|
|
553
755
|
/** Optional frame to scope the snapshot (e.g. from iframe.contentFrame() or page.frames()) */
|
|
554
756
|
frame?: Frame | FrameLocator
|
|
@@ -561,14 +763,25 @@ export class PlaywrightExecutor {
|
|
|
561
763
|
/** Only include interactive elements (default: true) */
|
|
562
764
|
interactiveOnly?: boolean
|
|
563
765
|
}) => {
|
|
564
|
-
const {
|
|
766
|
+
const {
|
|
767
|
+
page: targetPage,
|
|
768
|
+
frame,
|
|
769
|
+
locator,
|
|
770
|
+
search,
|
|
771
|
+
showDiffSinceLastCall = !search,
|
|
772
|
+
interactiveOnly = false,
|
|
773
|
+
} = options
|
|
565
774
|
const resolvedPage = targetPage || page
|
|
566
775
|
if (!resolvedPage) {
|
|
567
|
-
throw new Error('
|
|
776
|
+
throw new Error('snapshot requires a page')
|
|
568
777
|
}
|
|
569
778
|
|
|
570
779
|
// Use new in-page implementation via getAriaSnapshot
|
|
571
|
-
const {
|
|
780
|
+
const {
|
|
781
|
+
snapshot: rawSnapshot,
|
|
782
|
+
refs,
|
|
783
|
+
getSelectorForRef,
|
|
784
|
+
} = await getAriaSnapshot({
|
|
572
785
|
page: resolvedPage,
|
|
573
786
|
frame,
|
|
574
787
|
locator,
|
|
@@ -586,12 +799,20 @@ export class PlaywrightExecutor {
|
|
|
586
799
|
this.lastRefToLocator.set(resolvedPage, refToLocator)
|
|
587
800
|
|
|
588
801
|
const shouldCacheSnapshot = !frame
|
|
589
|
-
|
|
802
|
+
// Cache keyed by locator selector so full-page and locator-scoped snapshots
|
|
803
|
+
// don't pollute each other's diff baselines
|
|
804
|
+
const snapshotKey = locator ? `locator:${locator.selector()}` : 'page'
|
|
805
|
+
let pageSnapshots = this.lastSnapshots.get(resolvedPage)
|
|
806
|
+
if (!pageSnapshots) {
|
|
807
|
+
pageSnapshots = new Map()
|
|
808
|
+
this.lastSnapshots.set(resolvedPage, pageSnapshots)
|
|
809
|
+
}
|
|
810
|
+
const previousSnapshot = shouldCacheSnapshot ? pageSnapshots.get(snapshotKey) : undefined
|
|
590
811
|
if (shouldCacheSnapshot) {
|
|
591
|
-
|
|
812
|
+
pageSnapshots.set(snapshotKey, snapshotStr)
|
|
592
813
|
}
|
|
593
814
|
|
|
594
|
-
//
|
|
815
|
+
// Diff defaults off when search is provided, but agent can explicitly enable both
|
|
595
816
|
if (showDiffSinceLastCall && previousSnapshot && shouldCacheSnapshot) {
|
|
596
817
|
const diffResult = createSmartDiff({
|
|
597
818
|
oldContent: previousSnapshot,
|
|
@@ -680,7 +901,7 @@ export class PlaywrightExecutor {
|
|
|
680
901
|
|
|
681
902
|
if (filterPage) {
|
|
682
903
|
// Use targetId() if available, fallback to internal _guid for CDP connections
|
|
683
|
-
const targetId = filterPage.targetId() || (filterPage as any)._guid as string | undefined
|
|
904
|
+
const targetId = filterPage.targetId() || ((filterPage as any)._guid as string | undefined)
|
|
684
905
|
if (!targetId) {
|
|
685
906
|
throw new Error('Could not get page targetId')
|
|
686
907
|
}
|
|
@@ -770,18 +991,51 @@ export class PlaywrightExecutor {
|
|
|
770
991
|
// Recording uses chrome.tabCapture which requires activeTab permission.
|
|
771
992
|
// This permission is granted when the user clicks the Playwriter extension icon on a tab.
|
|
772
993
|
const relayPort = this.cdpConfig.port || 19988
|
|
773
|
-
// Recording will work on any tab where the user has clicked the icon.
|
|
774
|
-
const withRecordingDefaults = <T extends { page?: Page; sessionId?: string }, R>(
|
|
775
|
-
fn: (opts: T & { relayPort: number; sessionId?: string }) => Promise<R>,
|
|
776
|
-
) => {
|
|
777
|
-
return async (options: T = {} as T) => {
|
|
778
|
-
const targetPage = options.page || page
|
|
779
|
-
// Use Playwright's exposed sessionId directly
|
|
780
|
-
const sessionId = options.sessionId || targetPage.sessionId() || undefined
|
|
781
|
-
return fn({ page: targetPage, sessionId, relayPort, ...options })
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
994
|
const self = this
|
|
995
|
+
const recordingGhostCursor = new RecordingGhostCursorController({
|
|
996
|
+
logger: {
|
|
997
|
+
error: (...args: unknown[]) => {
|
|
998
|
+
self.logger.error(...args)
|
|
999
|
+
},
|
|
1000
|
+
},
|
|
1001
|
+
})
|
|
1002
|
+
|
|
1003
|
+
const showGhostCursor = async (options?: ({ page?: Page } & GhostCursorClientOptions)) => {
|
|
1004
|
+
const targetPage = options?.page || page
|
|
1005
|
+
const cursorOptions: GhostCursorClientOptions | undefined = (() => {
|
|
1006
|
+
if (!options) {
|
|
1007
|
+
return undefined
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const { page: _ignoredPage, ...rest } = options
|
|
1011
|
+
return rest
|
|
1012
|
+
})()
|
|
1013
|
+
|
|
1014
|
+
await recordingGhostCursor.show({ page: targetPage, cursorOptions })
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const hideGhostCursor = async (options?: { page?: Page }) => {
|
|
1018
|
+
const targetPage = options?.page || page
|
|
1019
|
+
await recordingGhostCursor.hide({ page: targetPage })
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const recordingApi = createRecordingApi({
|
|
1023
|
+
context,
|
|
1024
|
+
defaultPage: page,
|
|
1025
|
+
relayPort,
|
|
1026
|
+
ghostCursorController: recordingGhostCursor,
|
|
1027
|
+
onStart: () => {
|
|
1028
|
+
self.recordingStartedAt = Date.now()
|
|
1029
|
+
self.executionTimestamps = []
|
|
1030
|
+
},
|
|
1031
|
+
onFinish: () => {
|
|
1032
|
+
self.recordingStartedAt = null
|
|
1033
|
+
self.executionTimestamps = []
|
|
1034
|
+
},
|
|
1035
|
+
getExecutionTimestamps: () => {
|
|
1036
|
+
return self.executionTimestamps
|
|
1037
|
+
},
|
|
1038
|
+
})
|
|
785
1039
|
|
|
786
1040
|
// Ghost Browser API - creates chrome object that mirrors Ghost Browser's APIs
|
|
787
1041
|
// See extension/src/ghost-browser-api.d.ts for full API documentation
|
|
@@ -800,7 +1054,8 @@ export class PlaywrightExecutor {
|
|
|
800
1054
|
context,
|
|
801
1055
|
state: this.userState,
|
|
802
1056
|
console: customConsole,
|
|
803
|
-
|
|
1057
|
+
snapshot,
|
|
1058
|
+
accessibilitySnapshot: snapshot, // backward compat alias
|
|
804
1059
|
refToLocator,
|
|
805
1060
|
getCleanHTML,
|
|
806
1061
|
getPageMarkdown,
|
|
@@ -815,10 +1070,23 @@ export class PlaywrightExecutor {
|
|
|
815
1070
|
formatStylesAsText,
|
|
816
1071
|
getReactSource: getReactSourceFn,
|
|
817
1072
|
screenshotWithAccessibilityLabels: screenshotWithAccessibilityLabelsFn,
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1073
|
+
resizeImage,
|
|
1074
|
+
ghostCursor: {
|
|
1075
|
+
show: showGhostCursor,
|
|
1076
|
+
hide: hideGhostCursor,
|
|
1077
|
+
},
|
|
1078
|
+
recording: {
|
|
1079
|
+
start: recordingApi.start,
|
|
1080
|
+
stop: recordingApi.stop,
|
|
1081
|
+
isRecording: recordingApi.isRecording,
|
|
1082
|
+
cancel: recordingApi.cancel,
|
|
1083
|
+
},
|
|
1084
|
+
// Backward-compatible aliases
|
|
1085
|
+
startRecording: recordingApi.start,
|
|
1086
|
+
stopRecording: recordingApi.stop,
|
|
1087
|
+
isRecording: recordingApi.isRecording,
|
|
1088
|
+
cancelRecording: recordingApi.cancel,
|
|
1089
|
+
createDemoVideo,
|
|
822
1090
|
resetPlaywright: async () => {
|
|
823
1091
|
const { page: newPage, context: newContext } = await self.reset()
|
|
824
1092
|
vmContextObj.page = newPage
|
|
@@ -833,16 +1101,37 @@ export class PlaywrightExecutor {
|
|
|
833
1101
|
}
|
|
834
1102
|
|
|
835
1103
|
const vmContext = vm.createContext(vmContextObj)
|
|
836
|
-
const
|
|
837
|
-
const wrappedCode =
|
|
838
|
-
? `(async () => { return await (${
|
|
1104
|
+
const autoReturnExpr = getAutoReturnExpression(code)
|
|
1105
|
+
const wrappedCode = autoReturnExpr !== null
|
|
1106
|
+
? `(async () => { return await (${autoReturnExpr}) })()`
|
|
839
1107
|
: `(async () => { ${code} })()`
|
|
840
|
-
const hasExplicitReturn =
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1108
|
+
const hasExplicitReturn = autoReturnExpr !== null || /\breturn\b/.test(code)
|
|
1109
|
+
|
|
1110
|
+
// Track execution timestamps relative to recording start (seconds).
|
|
1111
|
+
// Used to identify idle gaps that can be sped up in demo videos.
|
|
1112
|
+
// Captured before execution so we can record timing even if it throws.
|
|
1113
|
+
const recordingStartSnapshot = this.recordingStartedAt
|
|
1114
|
+
const execStartSec = recordingStartSnapshot !== null
|
|
1115
|
+
? (Date.now() - recordingStartSnapshot) / 1000
|
|
1116
|
+
: -1
|
|
1117
|
+
|
|
1118
|
+
const result = await (async () => {
|
|
1119
|
+
try {
|
|
1120
|
+
return await Promise.race([
|
|
1121
|
+
vm.runInContext(wrappedCode, vmContext, { timeout, displayErrors: true }),
|
|
1122
|
+
new Promise((_, reject) => setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout)),
|
|
1123
|
+
])
|
|
1124
|
+
} finally {
|
|
1125
|
+
// Record timestamp even on error — the execution still occupied real time
|
|
1126
|
+
// that should not be sped up in the demo video.
|
|
1127
|
+
// Compare against snapshot to avoid cross-session contamination if
|
|
1128
|
+
// recording was stopped and restarted inside the same execute() call.
|
|
1129
|
+
if (recordingStartSnapshot !== null && execStartSec >= 0 && this.recordingStartedAt === recordingStartSnapshot) {
|
|
1130
|
+
const execEndSec = (Date.now() - recordingStartSnapshot) / 1000
|
|
1131
|
+
this.executionTimestamps.push({ start: execStartSec, end: execEndSec })
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
})()
|
|
846
1135
|
|
|
847
1136
|
let responseText = formatConsoleLogs(consoleLogs)
|
|
848
1137
|
|
|
@@ -853,13 +1142,21 @@ export class PlaywrightExecutor {
|
|
|
853
1142
|
const formatted =
|
|
854
1143
|
typeof resolvedResult === 'string'
|
|
855
1144
|
? resolvedResult
|
|
856
|
-
: util.inspect(resolvedResult, {
|
|
1145
|
+
: util.inspect(resolvedResult, {
|
|
1146
|
+
depth: 4,
|
|
1147
|
+
colors: false,
|
|
1148
|
+
maxArrayLength: 100,
|
|
1149
|
+
maxStringLength: 1000,
|
|
1150
|
+
breakLength: 80,
|
|
1151
|
+
})
|
|
857
1152
|
if (formatted.trim()) {
|
|
858
1153
|
responseText += `[return value] ${formatted}\n`
|
|
859
1154
|
}
|
|
860
1155
|
}
|
|
861
1156
|
}
|
|
862
1157
|
|
|
1158
|
+
responseText += this.flushWarningsForScope(warningScope)
|
|
1159
|
+
|
|
863
1160
|
if (!responseText.trim()) {
|
|
864
1161
|
responseText = 'Code executed successfully (no output)'
|
|
865
1162
|
}
|
|
@@ -883,17 +1180,21 @@ export class PlaywrightExecutor {
|
|
|
883
1180
|
return { text: finalText, images, isError: false }
|
|
884
1181
|
} catch (error: any) {
|
|
885
1182
|
const errorStack = error.stack || error.message
|
|
886
|
-
const isTimeoutError =
|
|
1183
|
+
const isTimeoutError =
|
|
1184
|
+
error instanceof CodeExecutionTimeoutError || error?.name === 'TimeoutError' || error?.name === 'AbortError'
|
|
887
1185
|
|
|
888
1186
|
this.logger.error('Error in execute:', errorStack)
|
|
889
1187
|
|
|
890
1188
|
const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)')
|
|
1189
|
+
const warningText = this.flushWarningsForScope(warningScope)
|
|
891
1190
|
const resetHint = isTimeoutError
|
|
892
1191
|
? ''
|
|
893
1192
|
: '\n\n[HINT: If this is an internal Playwright error, page/browser closed, or connection issue, call reset to reconnect.]'
|
|
894
1193
|
|
|
1194
|
+
// timeout stacks are internal noise (Promise.race / setTimeout); only show the message
|
|
1195
|
+
const errorText = isTimeoutError ? error.message : errorStack
|
|
895
1196
|
return {
|
|
896
|
-
text: `${logsText}\nError executing code: ${
|
|
1197
|
+
text: `${logsText}${warningText}\nError executing code: ${errorText}${resetHint}`,
|
|
897
1198
|
images: [],
|
|
898
1199
|
isError: true,
|
|
899
1200
|
}
|
|
@@ -927,7 +1228,7 @@ export class PlaywrightExecutor {
|
|
|
927
1228
|
}
|
|
928
1229
|
|
|
929
1230
|
const page = await context.newPage()
|
|
930
|
-
this.
|
|
1231
|
+
this.setupPageListeners(page)
|
|
931
1232
|
const pageUrl = page.url()
|
|
932
1233
|
if (pageUrl === 'about:blank') {
|
|
933
1234
|
return page
|