playwright-core 1.58.0-alpha-2025-12-10 → 1.58.0-alpha-2025-12-11
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/ThirdPartyNotices.txt +3 -3
- package/browsers.json +2 -2
- package/lib/client/browser.js +3 -5
- package/lib/client/browserType.js +2 -2
- package/lib/client/fetch.js +2 -4
- package/lib/mcpBundleImpl/index.js +29 -29
- package/lib/protocol/serializers.js +5 -0
- package/lib/server/agent/actionRunner.js +33 -2
- package/lib/server/agent/agent.js +11 -8
- package/lib/server/agent/context.js +26 -11
- package/lib/server/agent/tools.js +67 -5
- package/lib/server/artifact.js +1 -1
- package/lib/server/chromium/crPage.js +5 -5
- package/lib/server/dispatchers/dispatcher.js +5 -8
- package/lib/server/firefox/ffBrowser.js +1 -1
- package/lib/server/firefox/ffPage.js +1 -1
- package/lib/server/instrumentation.js +3 -0
- package/lib/server/page.js +2 -2
- package/lib/server/progress.js +2 -0
- package/lib/server/screencast.js +24 -25
- package/lib/server/videoRecorder.js +20 -11
- package/lib/server/webkit/wkBrowser.js +1 -1
- package/lib/server/webkit/wkPage.js +7 -7
- package/lib/vite/traceViewer/index.BVu7tZDe.css +1 -0
- package/lib/vite/traceViewer/index.html +2 -2
- package/lib/vite/traceViewer/index.zFV_GQE-.js +2 -0
- package/lib/vite/traceViewer/sw.bundle.js +3 -1
- package/package.json +1 -1
- package/lib/vite/traceViewer/index.C4Y3Aw8n.css +0 -1
- package/lib/vite/traceViewer/index.YskCIlQ-.js +0 -2
|
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var serializers_exports = {};
|
|
20
20
|
__export(serializers_exports, {
|
|
21
21
|
parseSerializedValue: () => parseSerializedValue,
|
|
22
|
+
serializePlainValue: () => serializePlainValue,
|
|
22
23
|
serializeValue: () => serializeValue
|
|
23
24
|
});
|
|
24
25
|
module.exports = __toCommonJS(serializers_exports);
|
|
@@ -90,6 +91,9 @@ function innerParseSerializedValue(value, handles, refs, accessChain) {
|
|
|
90
91
|
function serializeValue(value, handleSerializer) {
|
|
91
92
|
return innerSerializeValue(value, handleSerializer, { lastId: 0, visited: /* @__PURE__ */ new Map() }, []);
|
|
92
93
|
}
|
|
94
|
+
function serializePlainValue(arg) {
|
|
95
|
+
return serializeValue(arg, (value) => ({ fallThrough: value }));
|
|
96
|
+
}
|
|
93
97
|
function innerSerializeValue(value, handleSerializer, visitorInfo, accessChain) {
|
|
94
98
|
const handle = handleSerializer(value);
|
|
95
99
|
if ("fallThrough" in handle)
|
|
@@ -188,5 +192,6 @@ const constructorToTypedArrayKind = new Map(Object.entries(typedArrayKindToConst
|
|
|
188
192
|
// Annotate the CommonJS export names for ESM import in node:
|
|
189
193
|
0 && (module.exports = {
|
|
190
194
|
parseSerializedValue,
|
|
195
|
+
serializePlainValue,
|
|
191
196
|
serializeValue
|
|
192
197
|
});
|
|
@@ -18,9 +18,12 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var actionRunner_exports = {};
|
|
20
20
|
__export(actionRunner_exports, {
|
|
21
|
-
runAction: () => runAction
|
|
21
|
+
runAction: () => runAction,
|
|
22
|
+
serializeArgument: () => serializeArgument
|
|
22
23
|
});
|
|
23
24
|
module.exports = __toCommonJS(actionRunner_exports);
|
|
25
|
+
var import_expectUtils = require("../utils/expectUtils");
|
|
26
|
+
var import_serializers = require("../../protocol/serializers");
|
|
24
27
|
async function runAction(progress, page, action, secrets) {
|
|
25
28
|
const frame = page.mainFrame();
|
|
26
29
|
switch (action.method) {
|
|
@@ -59,10 +62,38 @@ async function runAction(progress, page, action, secrets) {
|
|
|
59
62
|
else
|
|
60
63
|
await frame.uncheck(progress, action.selector, { ...strictTrue });
|
|
61
64
|
break;
|
|
65
|
+
case "expectVisible": {
|
|
66
|
+
const result = await frame.expect(progress, action.selector, { expression: "to.be.visible", isNot: false }, 5e3);
|
|
67
|
+
if (result.errorMessage)
|
|
68
|
+
throw new Error(result.errorMessage);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "expectValue": {
|
|
72
|
+
let result;
|
|
73
|
+
if (action.type === "textbox" || action.type === "combobox" || action.type === "slider") {
|
|
74
|
+
const expectedText = (0, import_expectUtils.serializeExpectedTextValues)([action.value]);
|
|
75
|
+
result = await frame.expect(progress, action.selector, { expression: "to.have.value", expectedText, isNot: false }, 5e3);
|
|
76
|
+
} else if (action.type === "checkbox" || action.type === "radio") {
|
|
77
|
+
const expectedValue = serializeArgument({ checked: true });
|
|
78
|
+
result = await frame.expect(progress, action.selector, { expression: "to.be.checked", expectedValue, isNot: false }, 5e3);
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error(`Unsupported element type: ${action.type}`);
|
|
81
|
+
}
|
|
82
|
+
if (result.errorMessage)
|
|
83
|
+
throw new Error(result.errorMessage);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
62
86
|
}
|
|
63
87
|
}
|
|
88
|
+
function serializeArgument(arg) {
|
|
89
|
+
return {
|
|
90
|
+
value: (0, import_serializers.serializePlainValue)(arg),
|
|
91
|
+
handles: []
|
|
92
|
+
};
|
|
93
|
+
}
|
|
64
94
|
const strictTrue = { strict: true };
|
|
65
95
|
// Annotate the CommonJS export names for ESM import in node:
|
|
66
96
|
0 && (module.exports = {
|
|
67
|
-
runAction
|
|
97
|
+
runAction,
|
|
98
|
+
serializeArgument
|
|
68
99
|
});
|
|
@@ -42,9 +42,7 @@ async function pagePerform(progress, page, options) {
|
|
|
42
42
|
const context = new import_context.Context(progress, page);
|
|
43
43
|
if (await cachedPerform(context, options))
|
|
44
44
|
return;
|
|
45
|
-
await perform(context, options.task,
|
|
46
|
-
error: import_mcpBundle.z.string().optional().describe("An error message if the task could not be completed successfully")
|
|
47
|
-
})), options);
|
|
45
|
+
await perform(context, options.task, void 0, options);
|
|
48
46
|
await updateCache(context, options);
|
|
49
47
|
}
|
|
50
48
|
async function pageExtract(progress, page, options) {
|
|
@@ -70,6 +68,13 @@ async function perform(context, userTask, resultSchema, options = {}) {
|
|
|
70
68
|
debug: import_utilsBundle.debug,
|
|
71
69
|
callTool,
|
|
72
70
|
tools,
|
|
71
|
+
beforeTurn: (params) => {
|
|
72
|
+
const lastReply = params.conversation.messages.findLast((m) => m.role === "assistant");
|
|
73
|
+
const toolCall = lastReply?.content.find((c) => c.type === "tool_call");
|
|
74
|
+
if (!resultSchema && toolCall && toolCall.arguments.thatShouldBeIt)
|
|
75
|
+
return "break";
|
|
76
|
+
return "continue";
|
|
77
|
+
},
|
|
73
78
|
...options
|
|
74
79
|
});
|
|
75
80
|
const task = `${userTask}
|
|
@@ -77,9 +82,8 @@ async function perform(context, userTask, resultSchema, options = {}) {
|
|
|
77
82
|
### Page snapshot
|
|
78
83
|
${full}
|
|
79
84
|
`;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
85
|
+
const { result } = await loop.run(task, { resultSchema });
|
|
86
|
+
return result;
|
|
83
87
|
}
|
|
84
88
|
const allCaches = /* @__PURE__ */ new Map();
|
|
85
89
|
async function cachedPerform(context, options) {
|
|
@@ -112,8 +116,7 @@ async function updateCache(context, options) {
|
|
|
112
116
|
async function cachedActions(cacheFile) {
|
|
113
117
|
let cache = allCaches.get(cacheFile);
|
|
114
118
|
if (!cache) {
|
|
115
|
-
|
|
116
|
-
cache = JSON.parse(text);
|
|
119
|
+
cache = await import_fs.default.promises.readFile(cacheFile, "utf-8").then((text) => JSON.parse(text)).catch(() => ({}));
|
|
117
120
|
allCaches.set(cacheFile, cache);
|
|
118
121
|
}
|
|
119
122
|
return cache;
|
|
@@ -34,13 +34,17 @@ class Context {
|
|
|
34
34
|
return await this.runActionsAndWait([action]);
|
|
35
35
|
}
|
|
36
36
|
async runActionsAndWait(action) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
try {
|
|
38
|
+
await this.waitForCompletion(async () => {
|
|
39
|
+
for (const a of action) {
|
|
40
|
+
await (0, import_actionRunner.runAction)(this.progress, this.page, a, this.options?.secrets ?? []);
|
|
41
|
+
this.actions.push(a);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return await this.snapshotResult();
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return await this.snapshotResult(e);
|
|
47
|
+
}
|
|
44
48
|
}
|
|
45
49
|
async waitForCompletion(callback) {
|
|
46
50
|
const requests = [];
|
|
@@ -73,17 +77,28 @@ class Context {
|
|
|
73
77
|
await this.progress.wait(500);
|
|
74
78
|
return result;
|
|
75
79
|
}
|
|
76
|
-
async snapshotResult() {
|
|
80
|
+
async snapshotResult(error) {
|
|
77
81
|
let { full } = await this.page.snapshotForAI(this.progress);
|
|
78
82
|
full = this._redactText(full);
|
|
79
|
-
const text = [
|
|
80
|
-
|
|
83
|
+
const text = [];
|
|
84
|
+
if (error)
|
|
85
|
+
text.push(`# Error
|
|
86
|
+
${error.message}`);
|
|
87
|
+
else
|
|
88
|
+
text.push(`# Success`);
|
|
89
|
+
text.push(`# Page snapshot
|
|
90
|
+
${full}`);
|
|
81
91
|
return {
|
|
82
92
|
_meta: {
|
|
83
93
|
"dev.lowire/state": {
|
|
84
94
|
"Page snapshot": full
|
|
85
|
-
}
|
|
95
|
+
},
|
|
96
|
+
"dev.lowire/history": error ? [{
|
|
97
|
+
category: "error",
|
|
98
|
+
content: error.message
|
|
99
|
+
}] : []
|
|
86
100
|
},
|
|
101
|
+
isError: !!error,
|
|
87
102
|
content: [{ type: "text", text: text.join("\n\n") }]
|
|
88
103
|
};
|
|
89
104
|
}
|
|
@@ -22,21 +22,25 @@ __export(tools_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(tools_exports);
|
|
24
24
|
var import_mcpBundle = require("../../mcpBundle");
|
|
25
|
+
var import_locatorUtils = require("../../utils/isomorphic/locatorUtils");
|
|
25
26
|
function defineTool(tool) {
|
|
26
27
|
return tool;
|
|
27
28
|
}
|
|
29
|
+
const baseSchema = import_mcpBundle.z.object({
|
|
30
|
+
thatShouldBeIt: import_mcpBundle.z.boolean().describe("Indicates that this tool call is sufficient to complete the task. If false, the task will continue with the next tool call")
|
|
31
|
+
});
|
|
28
32
|
const snapshot = defineTool({
|
|
29
33
|
schema: {
|
|
30
34
|
name: "browser_snapshot",
|
|
31
35
|
title: "Page snapshot",
|
|
32
36
|
description: "Capture accessibility snapshot of the current page, this is better than screenshot",
|
|
33
|
-
inputSchema:
|
|
37
|
+
inputSchema: baseSchema
|
|
34
38
|
},
|
|
35
39
|
handle: async (context, params) => {
|
|
36
40
|
return await context.snapshotResult();
|
|
37
41
|
}
|
|
38
42
|
});
|
|
39
|
-
const elementSchema =
|
|
43
|
+
const elementSchema = baseSchema.extend({
|
|
40
44
|
element: import_mcpBundle.z.string().describe("Human-readable element description used to obtain permission to interact with the element"),
|
|
41
45
|
ref: import_mcpBundle.z.string().describe("Exact target element reference from the page snapshot")
|
|
42
46
|
});
|
|
@@ -70,7 +74,7 @@ const drag = defineTool({
|
|
|
70
74
|
name: "browser_drag",
|
|
71
75
|
title: "Drag mouse",
|
|
72
76
|
description: "Perform drag and drop between two elements",
|
|
73
|
-
inputSchema:
|
|
77
|
+
inputSchema: baseSchema.extend({
|
|
74
78
|
startElement: import_mcpBundle.z.string().describe("Human-readable source element description used to obtain the permission to interact with the element"),
|
|
75
79
|
startRef: import_mcpBundle.z.string().describe("Exact source element reference from the page snapshot"),
|
|
76
80
|
endElement: import_mcpBundle.z.string().describe("Human-readable target element description used to obtain the permission to interact with the element"),
|
|
@@ -181,7 +185,7 @@ const fillForm = defineTool({
|
|
|
181
185
|
name: "browser_fill_form",
|
|
182
186
|
title: "Fill form",
|
|
183
187
|
description: "Fill multiple form fields",
|
|
184
|
-
inputSchema:
|
|
188
|
+
inputSchema: baseSchema.extend({
|
|
185
189
|
fields: import_mcpBundle.z.array(import_mcpBundle.z.object({
|
|
186
190
|
name: import_mcpBundle.z.string().describe("Human-readable field name"),
|
|
187
191
|
type: import_mcpBundle.z.enum(["textbox", "checkbox", "radio", "combobox", "slider"]).describe("Type of the field"),
|
|
@@ -217,6 +221,61 @@ const fillForm = defineTool({
|
|
|
217
221
|
return await context.runActionsAndWait(actions);
|
|
218
222
|
}
|
|
219
223
|
});
|
|
224
|
+
const expectVisible = defineTool({
|
|
225
|
+
schema: {
|
|
226
|
+
name: "browser_expect_visible",
|
|
227
|
+
title: "Expect element visible",
|
|
228
|
+
description: "Expect element is visible on the page",
|
|
229
|
+
inputSchema: baseSchema.extend({
|
|
230
|
+
role: import_mcpBundle.z.string().describe('ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":`'),
|
|
231
|
+
accessibleName: import_mcpBundle.z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"`')
|
|
232
|
+
})
|
|
233
|
+
},
|
|
234
|
+
handle: async (context, params) => {
|
|
235
|
+
return await context.runActionAndWait({
|
|
236
|
+
method: "expectVisible",
|
|
237
|
+
selector: (0, import_locatorUtils.getByRoleSelector)(params.role, { name: params.accessibleName })
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const expectVisibleText = defineTool({
|
|
242
|
+
schema: {
|
|
243
|
+
name: "browser_expect_visible_text",
|
|
244
|
+
title: "Expect text visible",
|
|
245
|
+
description: `Expect text is visible on the page. Prefer ${expectVisible.schema.name} if possible.`,
|
|
246
|
+
inputSchema: baseSchema.extend({
|
|
247
|
+
text: import_mcpBundle.z.string().describe('TEXT to expect. Can be found in the snapshot like this: `- role "Accessible Name": {TEXT}` or like this: `- text: {TEXT}`')
|
|
248
|
+
})
|
|
249
|
+
},
|
|
250
|
+
handle: async (context, params) => {
|
|
251
|
+
return await context.runActionAndWait({
|
|
252
|
+
method: "expectVisible",
|
|
253
|
+
selector: (0, import_locatorUtils.getByTextSelector)(params.text)
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
const expectValue = defineTool({
|
|
258
|
+
schema: {
|
|
259
|
+
name: "browser_expect_value",
|
|
260
|
+
title: "Expect value",
|
|
261
|
+
description: "Expect element value",
|
|
262
|
+
inputSchema: baseSchema.extend({
|
|
263
|
+
type: import_mcpBundle.z.enum(["textbox", "checkbox", "radio", "combobox", "slider"]).describe("Type of the element"),
|
|
264
|
+
element: import_mcpBundle.z.string().describe("Human-readable element description"),
|
|
265
|
+
ref: import_mcpBundle.z.string().describe("Exact target element reference from the page snapshot"),
|
|
266
|
+
value: import_mcpBundle.z.string().describe('Value to expect. For checkbox, use "true" or "false".')
|
|
267
|
+
})
|
|
268
|
+
},
|
|
269
|
+
handle: async (context, params) => {
|
|
270
|
+
const [selector] = await context.refSelectors([{ ref: params.ref, element: params.element }]);
|
|
271
|
+
return await context.runActionAndWait({
|
|
272
|
+
method: "expectValue",
|
|
273
|
+
selector,
|
|
274
|
+
type: params.type,
|
|
275
|
+
value: params.value
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
220
279
|
var tools_default = [
|
|
221
280
|
snapshot,
|
|
222
281
|
click,
|
|
@@ -225,5 +284,8 @@ var tools_default = [
|
|
|
225
284
|
selectOption,
|
|
226
285
|
pressKey,
|
|
227
286
|
type,
|
|
228
|
-
fillForm
|
|
287
|
+
fillForm,
|
|
288
|
+
expectVisible,
|
|
289
|
+
expectVisibleText,
|
|
290
|
+
expectValue
|
|
229
291
|
];
|
package/lib/server/artifact.js
CHANGED
|
@@ -103,7 +103,7 @@ class Artifact extends import_instrumentation.SdkObject {
|
|
|
103
103
|
if (!this._unaccessibleErrorMessage)
|
|
104
104
|
await import_fs.default.promises.unlink(this._localPath).catch((e) => {
|
|
105
105
|
});
|
|
106
|
-
await this.reportFinished(new import_errors.TargetClosedError());
|
|
106
|
+
await this.reportFinished(new import_errors.TargetClosedError(this.closeReason()));
|
|
107
107
|
}
|
|
108
108
|
async reportFinished(error) {
|
|
109
109
|
if (this._finished)
|
|
@@ -357,9 +357,9 @@ class FrameSession {
|
|
|
357
357
|
const { windowId } = await this._client.send("Browser.getWindowForTarget");
|
|
358
358
|
this._windowId = windowId;
|
|
359
359
|
}
|
|
360
|
-
let
|
|
360
|
+
let videoOptions;
|
|
361
361
|
if (!this._page.isStorageStatePage && this._isMainFrame() && hasUIWindow)
|
|
362
|
-
|
|
362
|
+
videoOptions = this._crPage._page.screencast.launchVideoRecorder();
|
|
363
363
|
let lifecycleEventsEnabled;
|
|
364
364
|
if (!this._isMainFrame())
|
|
365
365
|
this._addRendererListeners();
|
|
@@ -439,15 +439,15 @@ class FrameSession {
|
|
|
439
439
|
true
|
|
440
440
|
/* runImmediately */
|
|
441
441
|
));
|
|
442
|
-
if (
|
|
443
|
-
promises.push(this._crPage._page.screencast.startVideoRecording(
|
|
442
|
+
if (videoOptions)
|
|
443
|
+
promises.push(this._crPage._page.screencast.startVideoRecording(videoOptions));
|
|
444
444
|
}
|
|
445
445
|
promises.push(this._client.send("Runtime.runIfWaitingForDebugger"));
|
|
446
446
|
promises.push(this._firstNonInitialNavigationCommittedPromise);
|
|
447
447
|
await Promise.all(promises);
|
|
448
448
|
}
|
|
449
449
|
dispose() {
|
|
450
|
-
this._firstNonInitialNavigationCommittedReject(new import_errors.TargetClosedError());
|
|
450
|
+
this._firstNonInitialNavigationCommittedReject(new import_errors.TargetClosedError(this._page.closeReason()));
|
|
451
451
|
for (const childSession of this._childSessions)
|
|
452
452
|
childSession.dispose();
|
|
453
453
|
if (this._parentSession)
|
|
@@ -106,7 +106,7 @@ class Dispatcher extends import_events.EventEmitter {
|
|
|
106
106
|
this.connection.sendEvent(this, method, params);
|
|
107
107
|
}
|
|
108
108
|
_dispose(reason) {
|
|
109
|
-
this._disposeRecursively(new import_errors.TargetClosedError());
|
|
109
|
+
this._disposeRecursively(new import_errors.TargetClosedError(this._object.closeReason()));
|
|
110
110
|
this.connection.sendDispose(this, reason);
|
|
111
111
|
}
|
|
112
112
|
_onDispose() {
|
|
@@ -257,7 +257,7 @@ class DispatcherConnection {
|
|
|
257
257
|
const { id, guid, method, params, metadata } = message;
|
|
258
258
|
const dispatcher = this._dispatcherByGuid.get(guid);
|
|
259
259
|
if (!dispatcher) {
|
|
260
|
-
this.onmessage({ id, error: (0, import_errors.serializeError)(new import_errors.TargetClosedError()) });
|
|
260
|
+
this.onmessage({ id, error: (0, import_errors.serializeError)(new import_errors.TargetClosedError(void 0)) });
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
let validParams;
|
|
@@ -325,19 +325,19 @@ class DispatcherConnection {
|
|
|
325
325
|
const response = { id };
|
|
326
326
|
try {
|
|
327
327
|
if (this._dispatcherByGuid.get(guid) !== dispatcher)
|
|
328
|
-
throw new import_errors.TargetClosedError(closeReason(
|
|
328
|
+
throw new import_errors.TargetClosedError(sdkObject.closeReason());
|
|
329
329
|
const result = await dispatcher._runCommand(callMetadata, method, validParams);
|
|
330
330
|
const validator = (0, import_validator.findValidator)(dispatcher._type, method, "Result");
|
|
331
331
|
response.result = validator(result, "", this._validatorToWireContext());
|
|
332
332
|
callMetadata.result = result;
|
|
333
333
|
} catch (e) {
|
|
334
334
|
if ((0, import_errors.isTargetClosedError)(e)) {
|
|
335
|
-
const reason = closeReason(
|
|
335
|
+
const reason = sdkObject.closeReason();
|
|
336
336
|
if (reason)
|
|
337
337
|
(0, import_utils.rewriteErrorMessage)(e, reason);
|
|
338
338
|
} else if ((0, import_protocolError.isProtocolError)(e)) {
|
|
339
339
|
if (e.type === "closed")
|
|
340
|
-
e = new import_errors.TargetClosedError(closeReason(
|
|
340
|
+
e = new import_errors.TargetClosedError(sdkObject.closeReason(), e.browserLogMessage());
|
|
341
341
|
else if (e.type === "crashed")
|
|
342
342
|
(0, import_utils.rewriteErrorMessage)(e, "Target crashed " + e.browserLogMessage());
|
|
343
343
|
}
|
|
@@ -359,9 +359,6 @@ class DispatcherConnection {
|
|
|
359
359
|
await new Promise((f) => setTimeout(f, slowMo));
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
|
-
function closeReason(sdkObject) {
|
|
363
|
-
return sdkObject.attribution.page?.closeReason || sdkObject.attribution.context?._closeReason || sdkObject.attribution.browser?._closeReason;
|
|
364
|
-
}
|
|
365
362
|
// Annotate the CommonJS export names for ESM import in node:
|
|
366
363
|
0 && (module.exports = {
|
|
367
364
|
Dispatcher,
|
|
@@ -146,7 +146,7 @@ class FFBrowser extends import_browser.Browser {
|
|
|
146
146
|
}
|
|
147
147
|
_onDisconnect() {
|
|
148
148
|
for (const video of this._idToVideo.values())
|
|
149
|
-
video.artifact.reportFinished(new import_errors.TargetClosedError());
|
|
149
|
+
video.artifact.reportFinished(new import_errors.TargetClosedError(this.closeReason()));
|
|
150
150
|
this._idToVideo.clear();
|
|
151
151
|
for (const ffPage of this._ffPages.values())
|
|
152
152
|
ffPage.didClose();
|
|
@@ -276,7 +276,7 @@ class FFPage {
|
|
|
276
276
|
this._browserContext._browser._videoStarted(this._browserContext, event.screencastId, event.file, this._page.waitForInitializedOrError());
|
|
277
277
|
}
|
|
278
278
|
didClose() {
|
|
279
|
-
this._markAsError(new import_errors.TargetClosedError());
|
|
279
|
+
this._markAsError(new import_errors.TargetClosedError(this._page.closeReason()));
|
|
280
280
|
this._session.dispose();
|
|
281
281
|
import_eventsHelper.eventsHelper.removeEventListeners(this._eventListeners);
|
|
282
282
|
this._networkManager.dispose();
|
|
@@ -33,6 +33,9 @@ class SdkObject extends import_events.EventEmitter {
|
|
|
33
33
|
this.attribution = { ...parent.attribution };
|
|
34
34
|
this.instrumentation = parent.instrumentation;
|
|
35
35
|
}
|
|
36
|
+
closeReason() {
|
|
37
|
+
return this.attribution.page?._closeReason || this.attribution.context?._closeReason || this.attribution.browser?._closeReason;
|
|
38
|
+
}
|
|
36
39
|
}
|
|
37
40
|
function createRootSdkObject() {
|
|
38
41
|
const fakeParent = { attribution: {}, instrumentation: createInstrumentation() };
|
package/lib/server/page.js
CHANGED
|
@@ -163,7 +163,7 @@ class Page extends import_instrumentation.SdkObject {
|
|
|
163
163
|
this.emit(Page.Events.Close);
|
|
164
164
|
this._closedPromise.resolve();
|
|
165
165
|
this.instrumentation.onPageClose(this);
|
|
166
|
-
this.openScope.close(new import_errors.TargetClosedError());
|
|
166
|
+
this.openScope.close(new import_errors.TargetClosedError(this.closeReason()));
|
|
167
167
|
}
|
|
168
168
|
_didCrash() {
|
|
169
169
|
this.frameManager.dispose();
|
|
@@ -577,7 +577,7 @@ class Page extends import_instrumentation.SdkObject {
|
|
|
577
577
|
if (this._closedState === "closed")
|
|
578
578
|
return;
|
|
579
579
|
if (options.reason)
|
|
580
|
-
this.
|
|
580
|
+
this._closeReason = options.reason;
|
|
581
581
|
const runBeforeUnload = !!options.runBeforeUnload;
|
|
582
582
|
if (this._closedState !== "closing") {
|
|
583
583
|
if (!runBeforeUnload)
|
package/lib/server/progress.js
CHANGED
|
@@ -68,6 +68,8 @@ class ProgressController {
|
|
|
68
68
|
if (timeout) {
|
|
69
69
|
const timeoutError = new import_errors.TimeoutError(`Timeout ${timeout}ms exceeded.`);
|
|
70
70
|
timer = setTimeout(() => {
|
|
71
|
+
if (this.metadata.pauseStartTime && !this.metadata.pauseEndTime)
|
|
72
|
+
return;
|
|
71
73
|
if (this._state === "running") {
|
|
72
74
|
timeoutError[kAbortErrorSymbol] = true;
|
|
73
75
|
this._state = { error: timeoutError };
|
package/lib/server/screencast.js
CHANGED
|
@@ -40,11 +40,12 @@ var import_registry = require("./registry");
|
|
|
40
40
|
class Screencast {
|
|
41
41
|
constructor(page) {
|
|
42
42
|
this._videoRecorder = null;
|
|
43
|
-
this.
|
|
43
|
+
this._videoId = null;
|
|
44
44
|
this._screencastClients = /* @__PURE__ */ new Set();
|
|
45
45
|
// Aiming at 25 fps by default - each frame is 40ms, but we give some slack with 35ms.
|
|
46
46
|
// When throttling for tracing, 200ms between frames, except for 10 frames around the action.
|
|
47
47
|
this._frameThrottler = new FrameThrottler(10, 35, 200);
|
|
48
|
+
this._frameListener = null;
|
|
48
49
|
this._page = page;
|
|
49
50
|
}
|
|
50
51
|
stopFrameThrottler() {
|
|
@@ -60,29 +61,31 @@ class Screencast {
|
|
|
60
61
|
temporarilyDisableThrottling() {
|
|
61
62
|
this._frameThrottler.recharge();
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
+
launchVideoRecorder() {
|
|
64
65
|
const recordVideo = this._page.browserContext._options.recordVideo;
|
|
65
66
|
if (!recordVideo)
|
|
66
67
|
return void 0;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
68
|
+
(0, import_utils.assert)(!this._videoId);
|
|
69
|
+
this._videoId = (0, import_utils.createGuid)();
|
|
70
|
+
const outputFile = import_path.default.join(recordVideo.dir, this._videoId + ".webm");
|
|
71
|
+
const videoOptions = {
|
|
70
72
|
// validateBrowserContextOptions ensures correct video size.
|
|
71
73
|
...recordVideo.size,
|
|
72
74
|
outputFile
|
|
73
75
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
const ffmpegPath = import_registry.registry.findExecutable("ffmpeg").executablePathOrDie(this._page.browserContext._browser.sdkLanguage());
|
|
77
|
+
this._videoRecorder = new import_videoRecorder.VideoRecorder(ffmpegPath, videoOptions);
|
|
78
|
+
this._frameListener = import_utils.eventsHelper.addEventListener(this._page, import_page.Page.Events.ScreencastFrame, (frame) => this._videoRecorder.writeFrame(frame.buffer, frame.frameSwapWallTime / 1e3));
|
|
76
79
|
this._page.waitForInitializedOrError().then((p) => {
|
|
77
80
|
if (p instanceof Error)
|
|
78
81
|
this.stopVideoRecording().catch(() => {
|
|
79
82
|
});
|
|
80
83
|
});
|
|
81
|
-
return
|
|
84
|
+
return videoOptions;
|
|
82
85
|
}
|
|
83
86
|
async startVideoRecording(options) {
|
|
84
|
-
const
|
|
85
|
-
(0, import_utils.assert)(
|
|
87
|
+
const videoId = this._videoId;
|
|
88
|
+
(0, import_utils.assert)(videoId);
|
|
86
89
|
this._page.once(import_page.Page.Events.Close, () => this.stopVideoRecording().catch(() => {
|
|
87
90
|
}));
|
|
88
91
|
const gotFirstFrame = new Promise((f) => this._page.once(import_page.Page.Events.ScreencastFrame, f));
|
|
@@ -92,20 +95,22 @@ class Screencast {
|
|
|
92
95
|
height: options.height
|
|
93
96
|
});
|
|
94
97
|
gotFirstFrame.then(() => {
|
|
95
|
-
this._page.browserContext._browser._videoStarted(this._page.browserContext,
|
|
98
|
+
this._page.browserContext._browser._videoStarted(this._page.browserContext, videoId, options.outputFile, this._page.waitForInitializedOrError());
|
|
96
99
|
});
|
|
97
100
|
}
|
|
98
101
|
async stopVideoRecording() {
|
|
99
|
-
if (!this.
|
|
102
|
+
if (!this._videoId)
|
|
100
103
|
return;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
if (this._frameListener)
|
|
105
|
+
import_utils.eventsHelper.removeEventListeners([this._frameListener]);
|
|
106
|
+
this._frameListener = null;
|
|
107
|
+
const videoId = this._videoId;
|
|
108
|
+
this._videoId = null;
|
|
109
|
+
const videoRecorder = this._videoRecorder;
|
|
104
110
|
this._videoRecorder = null;
|
|
105
|
-
await this._stopScreencast(
|
|
106
|
-
await
|
|
107
|
-
|
|
108
|
-
const video = this._page.browserContext._browser._takeVideo(screencastId);
|
|
111
|
+
await this._stopScreencast(videoRecorder);
|
|
112
|
+
await videoRecorder.stop();
|
|
113
|
+
const video = this._page.browserContext._browser._takeVideo(videoId);
|
|
109
114
|
video?.reportFinished();
|
|
110
115
|
}
|
|
111
116
|
async _setOptions(options) {
|
|
@@ -129,12 +134,6 @@ class Screencast {
|
|
|
129
134
|
if (!this._screencastClients.size)
|
|
130
135
|
await this._page.delegate.stopScreencast();
|
|
131
136
|
}
|
|
132
|
-
async _createVideoRecorder(screencastId, options) {
|
|
133
|
-
(0, import_utils.assert)(!this._screencastId);
|
|
134
|
-
const ffmpegPath = import_registry.registry.findExecutable("ffmpeg").executablePathOrDie(this._page.browserContext._browser.sdkLanguage());
|
|
135
|
-
this._videoRecorder = await import_videoRecorder.VideoRecorder.launch(this._page, ffmpegPath, options);
|
|
136
|
-
this._screencastId = screencastId;
|
|
137
|
-
}
|
|
138
137
|
}
|
|
139
138
|
class FrameThrottler {
|
|
140
139
|
constructor(nonThrottledFrames, defaultInterval, throttlingInterval) {
|
|
@@ -22,11 +22,10 @@ __export(videoRecorder_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(videoRecorder_exports);
|
|
24
24
|
var import_utils = require("../utils");
|
|
25
|
-
var import_page = require("./page");
|
|
26
25
|
var import_processLauncher = require("./utils/processLauncher");
|
|
27
26
|
const fps = 25;
|
|
28
27
|
class VideoRecorder {
|
|
29
|
-
constructor(
|
|
28
|
+
constructor(ffmpegPath, options) {
|
|
30
29
|
this._process = null;
|
|
31
30
|
this._gracefullyClose = null;
|
|
32
31
|
this._lastWritePromise = Promise.resolve();
|
|
@@ -36,16 +35,12 @@ class VideoRecorder {
|
|
|
36
35
|
this._frameQueue = [];
|
|
37
36
|
this._isStopped = false;
|
|
38
37
|
this._ffmpegPath = ffmpegPath;
|
|
39
|
-
page.on(import_page.Page.Events.ScreencastFrame, (frame) => this.writeFrame(frame.buffer, frame.frameSwapWallTime / 1e3));
|
|
40
|
-
}
|
|
41
|
-
static async launch(page, ffmpegPath, options) {
|
|
42
38
|
if (!options.outputFile.endsWith(".webm"))
|
|
43
39
|
throw new Error("File must have .webm extension");
|
|
44
|
-
|
|
45
|
-
await recorder._launch(options);
|
|
46
|
-
return recorder;
|
|
40
|
+
this._launchPromise = this._launch(options).catch((e) => e);
|
|
47
41
|
}
|
|
48
42
|
async _launch(options) {
|
|
43
|
+
await (0, import_utils.mkdirIfNeeded)(options.outputFile);
|
|
49
44
|
const w = options.width;
|
|
50
45
|
const h = options.height;
|
|
51
46
|
const args = `-loglevel error -f image2pipe -avioflags direct -fpsprobesize 0 -probesize 32 -analyzeduration 0 -c:v mjpeg -i pipe:0 -y -an -r ${fps} -c:v vp8 -qmin 0 -qmax 50 -crf 8 -deadline realtime -speed 8 -b:v 1M -threads 1 -vf pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0`.split(" ");
|
|
@@ -74,6 +69,13 @@ class VideoRecorder {
|
|
|
74
69
|
this._gracefullyClose = gracefullyClose;
|
|
75
70
|
}
|
|
76
71
|
writeFrame(frame, timestamp) {
|
|
72
|
+
this._launchPromise.then((error) => {
|
|
73
|
+
if (error)
|
|
74
|
+
return;
|
|
75
|
+
this._writeFrame(frame, timestamp);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
_writeFrame(frame, timestamp) {
|
|
77
79
|
(0, import_utils.assert)(this._process);
|
|
78
80
|
if (this._isStopped)
|
|
79
81
|
return;
|
|
@@ -100,13 +102,20 @@ class VideoRecorder {
|
|
|
100
102
|
});
|
|
101
103
|
}
|
|
102
104
|
async stop() {
|
|
105
|
+
const error = await this._launchPromise;
|
|
106
|
+
if (error)
|
|
107
|
+
throw error;
|
|
103
108
|
if (this._isStopped || !this._lastFrame)
|
|
104
109
|
return;
|
|
105
110
|
const addTime = Math.max(((0, import_utils.monotonicTime)() - this._lastWriteNodeTime) / 1e3, 1);
|
|
106
|
-
this.
|
|
111
|
+
this._writeFrame(Buffer.from([]), this._lastFrame.timestamp + addTime);
|
|
107
112
|
this._isStopped = true;
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
try {
|
|
114
|
+
await this._lastWritePromise;
|
|
115
|
+
await this._gracefullyClose();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
import_utils.debugLogger.log("error", `ffmpeg failed to stop: ${String(e)}`);
|
|
118
|
+
}
|
|
110
119
|
}
|
|
111
120
|
}
|
|
112
121
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -79,7 +79,7 @@ class WKBrowser extends import_browser.Browser {
|
|
|
79
79
|
wkPage.didClose();
|
|
80
80
|
this._wkPages.clear();
|
|
81
81
|
for (const video of this._idToVideo.values())
|
|
82
|
-
video.artifact.reportFinished(new import_errors.TargetClosedError());
|
|
82
|
+
video.artifact.reportFinished(new import_errors.TargetClosedError(this.closeReason()));
|
|
83
83
|
this._idToVideo.clear();
|
|
84
84
|
this._didClose();
|
|
85
85
|
}
|