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/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,18 @@ 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 short for fast agent failure.
|
|
596
|
+
// Navigation timeout (goto, reload) is longer since page loads are slower.
|
|
597
|
+
context.setDefaultTimeout(2000)
|
|
598
|
+
context.setDefaultNavigationTimeout(10000)
|
|
599
|
+
|
|
412
600
|
context.on('page', (page) => {
|
|
413
|
-
this.
|
|
601
|
+
this.setupPageListeners(page)
|
|
414
602
|
})
|
|
415
603
|
|
|
416
|
-
context.pages().forEach((p) => this.
|
|
604
|
+
context.pages().forEach((p) => this.setupPageListeners(p))
|
|
417
605
|
const page = await this.ensurePageForContext({ context, timeout: 10000 })
|
|
418
606
|
|
|
419
|
-
await this.preserveSystemColorScheme(context)
|
|
420
607
|
await this.setDeviceScaleFactorForMacOS(context)
|
|
421
608
|
|
|
422
609
|
this.browser = browser
|
|
@@ -455,10 +642,13 @@ export class PlaywrightExecutor {
|
|
|
455
642
|
|
|
456
643
|
async reset(): Promise<{ page: Page; context: BrowserContext }> {
|
|
457
644
|
if (this.browser) {
|
|
645
|
+
this.suppressPageCloseWarnings = true
|
|
458
646
|
try {
|
|
459
647
|
await this.browser.close()
|
|
460
648
|
} catch (e) {
|
|
461
649
|
this.logger.error('Error closing browser:', e)
|
|
650
|
+
} finally {
|
|
651
|
+
this.suppressPageCloseWarnings = false
|
|
462
652
|
}
|
|
463
653
|
}
|
|
464
654
|
|
|
@@ -484,14 +674,18 @@ export class PlaywrightExecutor {
|
|
|
484
674
|
const contexts = browser.contexts()
|
|
485
675
|
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
|
486
676
|
|
|
677
|
+
// Action timeout (click, fill, hover, etc.) is short for fast agent failure.
|
|
678
|
+
// Navigation timeout (goto, reload) is longer since page loads are slower.
|
|
679
|
+
context.setDefaultTimeout(2000)
|
|
680
|
+
context.setDefaultNavigationTimeout(10000)
|
|
681
|
+
|
|
487
682
|
context.on('page', (page) => {
|
|
488
|
-
this.
|
|
683
|
+
this.setupPageListeners(page)
|
|
489
684
|
})
|
|
490
685
|
|
|
491
|
-
context.pages().forEach((p) => this.
|
|
686
|
+
context.pages().forEach((p) => this.setupPageListeners(p))
|
|
492
687
|
const page = await this.ensurePageForContext({ context, timeout: 10000 })
|
|
493
688
|
|
|
494
|
-
await this.preserveSystemColorScheme(context)
|
|
495
689
|
await this.setDeviceScaleFactorForMacOS(context)
|
|
496
690
|
|
|
497
691
|
this.browser = browser
|
|
@@ -504,6 +698,7 @@ export class PlaywrightExecutor {
|
|
|
504
698
|
|
|
505
699
|
async execute(code: string, timeout = 10000): Promise<ExecuteResult> {
|
|
506
700
|
const consoleLogs: Array<{ method: string; args: any[] }> = []
|
|
701
|
+
const warningScope = this.beginWarningScope()
|
|
507
702
|
|
|
508
703
|
const formatConsoleLogs = (logs: Array<{ method: string; args: any[] }>, prefix = 'Console output') => {
|
|
509
704
|
if (logs.length === 0) {
|
|
@@ -514,7 +709,13 @@ export class PlaywrightExecutor {
|
|
|
514
709
|
const formattedArgs = args
|
|
515
710
|
.map((arg) => {
|
|
516
711
|
if (typeof arg === 'string') return arg
|
|
517
|
-
return util.inspect(arg, {
|
|
712
|
+
return util.inspect(arg, {
|
|
713
|
+
depth: 4,
|
|
714
|
+
colors: false,
|
|
715
|
+
maxArrayLength: 100,
|
|
716
|
+
maxStringLength: 1000,
|
|
717
|
+
breakLength: 80,
|
|
718
|
+
})
|
|
518
719
|
})
|
|
519
720
|
.join(' ')
|
|
520
721
|
text += `[${method}] ${formattedArgs}\n`
|
|
@@ -526,7 +727,6 @@ export class PlaywrightExecutor {
|
|
|
526
727
|
await this.ensureConnection()
|
|
527
728
|
const page = await this.getCurrentPage(timeout)
|
|
528
729
|
const context = this.context || page.context()
|
|
529
|
-
context.setDefaultTimeout(timeout)
|
|
530
730
|
|
|
531
731
|
this.logger.log('Executing code:', code)
|
|
532
732
|
|
|
@@ -548,7 +748,7 @@ export class PlaywrightExecutor {
|
|
|
548
748
|
},
|
|
549
749
|
}
|
|
550
750
|
|
|
551
|
-
const
|
|
751
|
+
const snapshot = async (options: {
|
|
552
752
|
page?: Page
|
|
553
753
|
/** Optional frame to scope the snapshot (e.g. from iframe.contentFrame() or page.frames()) */
|
|
554
754
|
frame?: Frame | FrameLocator
|
|
@@ -561,14 +761,25 @@ export class PlaywrightExecutor {
|
|
|
561
761
|
/** Only include interactive elements (default: true) */
|
|
562
762
|
interactiveOnly?: boolean
|
|
563
763
|
}) => {
|
|
564
|
-
const {
|
|
764
|
+
const {
|
|
765
|
+
page: targetPage,
|
|
766
|
+
frame,
|
|
767
|
+
locator,
|
|
768
|
+
search,
|
|
769
|
+
showDiffSinceLastCall = !search,
|
|
770
|
+
interactiveOnly = false,
|
|
771
|
+
} = options
|
|
565
772
|
const resolvedPage = targetPage || page
|
|
566
773
|
if (!resolvedPage) {
|
|
567
|
-
throw new Error('
|
|
774
|
+
throw new Error('snapshot requires a page')
|
|
568
775
|
}
|
|
569
776
|
|
|
570
777
|
// Use new in-page implementation via getAriaSnapshot
|
|
571
|
-
const {
|
|
778
|
+
const {
|
|
779
|
+
snapshot: rawSnapshot,
|
|
780
|
+
refs,
|
|
781
|
+
getSelectorForRef,
|
|
782
|
+
} = await getAriaSnapshot({
|
|
572
783
|
page: resolvedPage,
|
|
573
784
|
frame,
|
|
574
785
|
locator,
|
|
@@ -586,12 +797,20 @@ export class PlaywrightExecutor {
|
|
|
586
797
|
this.lastRefToLocator.set(resolvedPage, refToLocator)
|
|
587
798
|
|
|
588
799
|
const shouldCacheSnapshot = !frame
|
|
589
|
-
|
|
800
|
+
// Cache keyed by locator selector so full-page and locator-scoped snapshots
|
|
801
|
+
// don't pollute each other's diff baselines
|
|
802
|
+
const snapshotKey = locator ? `locator:${locator.selector()}` : 'page'
|
|
803
|
+
let pageSnapshots = this.lastSnapshots.get(resolvedPage)
|
|
804
|
+
if (!pageSnapshots) {
|
|
805
|
+
pageSnapshots = new Map()
|
|
806
|
+
this.lastSnapshots.set(resolvedPage, pageSnapshots)
|
|
807
|
+
}
|
|
808
|
+
const previousSnapshot = shouldCacheSnapshot ? pageSnapshots.get(snapshotKey) : undefined
|
|
590
809
|
if (shouldCacheSnapshot) {
|
|
591
|
-
|
|
810
|
+
pageSnapshots.set(snapshotKey, snapshotStr)
|
|
592
811
|
}
|
|
593
812
|
|
|
594
|
-
//
|
|
813
|
+
// Diff defaults off when search is provided, but agent can explicitly enable both
|
|
595
814
|
if (showDiffSinceLastCall && previousSnapshot && shouldCacheSnapshot) {
|
|
596
815
|
const diffResult = createSmartDiff({
|
|
597
816
|
oldContent: previousSnapshot,
|
|
@@ -680,7 +899,7 @@ export class PlaywrightExecutor {
|
|
|
680
899
|
|
|
681
900
|
if (filterPage) {
|
|
682
901
|
// Use targetId() if available, fallback to internal _guid for CDP connections
|
|
683
|
-
const targetId = filterPage.targetId() || (filterPage as any)._guid as string | undefined
|
|
902
|
+
const targetId = filterPage.targetId() || ((filterPage as any)._guid as string | undefined)
|
|
684
903
|
if (!targetId) {
|
|
685
904
|
throw new Error('Could not get page targetId')
|
|
686
905
|
}
|
|
@@ -770,18 +989,51 @@ export class PlaywrightExecutor {
|
|
|
770
989
|
// Recording uses chrome.tabCapture which requires activeTab permission.
|
|
771
990
|
// This permission is granted when the user clicks the Playwriter extension icon on a tab.
|
|
772
991
|
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
992
|
const self = this
|
|
993
|
+
const recordingGhostCursor = new RecordingGhostCursorController({
|
|
994
|
+
logger: {
|
|
995
|
+
error: (...args: unknown[]) => {
|
|
996
|
+
self.logger.error(...args)
|
|
997
|
+
},
|
|
998
|
+
},
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
const showGhostCursor = async (options?: ({ page?: Page } & GhostCursorClientOptions)) => {
|
|
1002
|
+
const targetPage = options?.page || page
|
|
1003
|
+
const cursorOptions: GhostCursorClientOptions | undefined = (() => {
|
|
1004
|
+
if (!options) {
|
|
1005
|
+
return undefined
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const { page: _ignoredPage, ...rest } = options
|
|
1009
|
+
return rest
|
|
1010
|
+
})()
|
|
1011
|
+
|
|
1012
|
+
await recordingGhostCursor.show({ page: targetPage, cursorOptions })
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const hideGhostCursor = async (options?: { page?: Page }) => {
|
|
1016
|
+
const targetPage = options?.page || page
|
|
1017
|
+
await recordingGhostCursor.hide({ page: targetPage })
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const recordingApi = createRecordingApi({
|
|
1021
|
+
context,
|
|
1022
|
+
defaultPage: page,
|
|
1023
|
+
relayPort,
|
|
1024
|
+
ghostCursorController: recordingGhostCursor,
|
|
1025
|
+
onStart: () => {
|
|
1026
|
+
self.recordingStartedAt = Date.now()
|
|
1027
|
+
self.executionTimestamps = []
|
|
1028
|
+
},
|
|
1029
|
+
onFinish: () => {
|
|
1030
|
+
self.recordingStartedAt = null
|
|
1031
|
+
self.executionTimestamps = []
|
|
1032
|
+
},
|
|
1033
|
+
getExecutionTimestamps: () => {
|
|
1034
|
+
return self.executionTimestamps
|
|
1035
|
+
},
|
|
1036
|
+
})
|
|
785
1037
|
|
|
786
1038
|
// Ghost Browser API - creates chrome object that mirrors Ghost Browser's APIs
|
|
787
1039
|
// See extension/src/ghost-browser-api.d.ts for full API documentation
|
|
@@ -800,7 +1052,8 @@ export class PlaywrightExecutor {
|
|
|
800
1052
|
context,
|
|
801
1053
|
state: this.userState,
|
|
802
1054
|
console: customConsole,
|
|
803
|
-
|
|
1055
|
+
snapshot,
|
|
1056
|
+
accessibilitySnapshot: snapshot, // backward compat alias
|
|
804
1057
|
refToLocator,
|
|
805
1058
|
getCleanHTML,
|
|
806
1059
|
getPageMarkdown,
|
|
@@ -815,10 +1068,23 @@ export class PlaywrightExecutor {
|
|
|
815
1068
|
formatStylesAsText,
|
|
816
1069
|
getReactSource: getReactSourceFn,
|
|
817
1070
|
screenshotWithAccessibilityLabels: screenshotWithAccessibilityLabelsFn,
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1071
|
+
resizeImage,
|
|
1072
|
+
ghostCursor: {
|
|
1073
|
+
show: showGhostCursor,
|
|
1074
|
+
hide: hideGhostCursor,
|
|
1075
|
+
},
|
|
1076
|
+
recording: {
|
|
1077
|
+
start: recordingApi.start,
|
|
1078
|
+
stop: recordingApi.stop,
|
|
1079
|
+
isRecording: recordingApi.isRecording,
|
|
1080
|
+
cancel: recordingApi.cancel,
|
|
1081
|
+
},
|
|
1082
|
+
// Backward-compatible aliases
|
|
1083
|
+
startRecording: recordingApi.start,
|
|
1084
|
+
stopRecording: recordingApi.stop,
|
|
1085
|
+
isRecording: recordingApi.isRecording,
|
|
1086
|
+
cancelRecording: recordingApi.cancel,
|
|
1087
|
+
createDemoVideo,
|
|
822
1088
|
resetPlaywright: async () => {
|
|
823
1089
|
const { page: newPage, context: newContext } = await self.reset()
|
|
824
1090
|
vmContextObj.page = newPage
|
|
@@ -833,16 +1099,37 @@ export class PlaywrightExecutor {
|
|
|
833
1099
|
}
|
|
834
1100
|
|
|
835
1101
|
const vmContext = vm.createContext(vmContextObj)
|
|
836
|
-
const
|
|
837
|
-
const wrappedCode =
|
|
838
|
-
? `(async () => { return await (${
|
|
1102
|
+
const autoReturnExpr = getAutoReturnExpression(code)
|
|
1103
|
+
const wrappedCode = autoReturnExpr !== null
|
|
1104
|
+
? `(async () => { return await (${autoReturnExpr}) })()`
|
|
839
1105
|
: `(async () => { ${code} })()`
|
|
840
|
-
const hasExplicitReturn =
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1106
|
+
const hasExplicitReturn = autoReturnExpr !== null || /\breturn\b/.test(code)
|
|
1107
|
+
|
|
1108
|
+
// Track execution timestamps relative to recording start (seconds).
|
|
1109
|
+
// Used to identify idle gaps that can be sped up in demo videos.
|
|
1110
|
+
// Captured before execution so we can record timing even if it throws.
|
|
1111
|
+
const recordingStartSnapshot = this.recordingStartedAt
|
|
1112
|
+
const execStartSec = recordingStartSnapshot !== null
|
|
1113
|
+
? (Date.now() - recordingStartSnapshot) / 1000
|
|
1114
|
+
: -1
|
|
1115
|
+
|
|
1116
|
+
const result = await (async () => {
|
|
1117
|
+
try {
|
|
1118
|
+
return await Promise.race([
|
|
1119
|
+
vm.runInContext(wrappedCode, vmContext, { timeout, displayErrors: true }),
|
|
1120
|
+
new Promise((_, reject) => setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout)),
|
|
1121
|
+
])
|
|
1122
|
+
} finally {
|
|
1123
|
+
// Record timestamp even on error — the execution still occupied real time
|
|
1124
|
+
// that should not be sped up in the demo video.
|
|
1125
|
+
// Compare against snapshot to avoid cross-session contamination if
|
|
1126
|
+
// recording was stopped and restarted inside the same execute() call.
|
|
1127
|
+
if (recordingStartSnapshot !== null && execStartSec >= 0 && this.recordingStartedAt === recordingStartSnapshot) {
|
|
1128
|
+
const execEndSec = (Date.now() - recordingStartSnapshot) / 1000
|
|
1129
|
+
this.executionTimestamps.push({ start: execStartSec, end: execEndSec })
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
})()
|
|
846
1133
|
|
|
847
1134
|
let responseText = formatConsoleLogs(consoleLogs)
|
|
848
1135
|
|
|
@@ -853,13 +1140,21 @@ export class PlaywrightExecutor {
|
|
|
853
1140
|
const formatted =
|
|
854
1141
|
typeof resolvedResult === 'string'
|
|
855
1142
|
? resolvedResult
|
|
856
|
-
: util.inspect(resolvedResult, {
|
|
1143
|
+
: util.inspect(resolvedResult, {
|
|
1144
|
+
depth: 4,
|
|
1145
|
+
colors: false,
|
|
1146
|
+
maxArrayLength: 100,
|
|
1147
|
+
maxStringLength: 1000,
|
|
1148
|
+
breakLength: 80,
|
|
1149
|
+
})
|
|
857
1150
|
if (formatted.trim()) {
|
|
858
1151
|
responseText += `[return value] ${formatted}\n`
|
|
859
1152
|
}
|
|
860
1153
|
}
|
|
861
1154
|
}
|
|
862
1155
|
|
|
1156
|
+
responseText += this.flushWarningsForScope(warningScope)
|
|
1157
|
+
|
|
863
1158
|
if (!responseText.trim()) {
|
|
864
1159
|
responseText = 'Code executed successfully (no output)'
|
|
865
1160
|
}
|
|
@@ -883,17 +1178,21 @@ export class PlaywrightExecutor {
|
|
|
883
1178
|
return { text: finalText, images, isError: false }
|
|
884
1179
|
} catch (error: any) {
|
|
885
1180
|
const errorStack = error.stack || error.message
|
|
886
|
-
const isTimeoutError =
|
|
1181
|
+
const isTimeoutError =
|
|
1182
|
+
error instanceof CodeExecutionTimeoutError || error?.name === 'TimeoutError' || error?.name === 'AbortError'
|
|
887
1183
|
|
|
888
1184
|
this.logger.error('Error in execute:', errorStack)
|
|
889
1185
|
|
|
890
1186
|
const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)')
|
|
1187
|
+
const warningText = this.flushWarningsForScope(warningScope)
|
|
891
1188
|
const resetHint = isTimeoutError
|
|
892
1189
|
? ''
|
|
893
1190
|
: '\n\n[HINT: If this is an internal Playwright error, page/browser closed, or connection issue, call reset to reconnect.]'
|
|
894
1191
|
|
|
1192
|
+
// timeout stacks are internal noise (Promise.race / setTimeout); only show the message
|
|
1193
|
+
const errorText = isTimeoutError ? error.message : errorStack
|
|
895
1194
|
return {
|
|
896
|
-
text: `${logsText}\nError executing code: ${
|
|
1195
|
+
text: `${logsText}${warningText}\nError executing code: ${errorText}${resetHint}`,
|
|
897
1196
|
images: [],
|
|
898
1197
|
isError: true,
|
|
899
1198
|
}
|
|
@@ -927,7 +1226,7 @@ export class PlaywrightExecutor {
|
|
|
927
1226
|
}
|
|
928
1227
|
|
|
929
1228
|
const page = await context.newPage()
|
|
930
|
-
this.
|
|
1229
|
+
this.setupPageListeners(page)
|
|
931
1230
|
const pageUrl = page.url()
|
|
932
1231
|
if (pageUrl === 'about:blank') {
|
|
933
1232
|
return page
|