playwright-core 1.58.0-alpha-2025-12-10 → 1.58.0-alpha-2025-12-12
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/client/page.js +4 -3
- package/lib/mcpBundleImpl/index.js +29 -29
- package/lib/protocol/serializers.js +5 -0
- package/lib/protocol/validator.js +26 -8
- package/lib/server/agent/actionRunner.js +33 -2
- package/lib/server/agent/agent.js +28 -12
- package/lib/server/agent/backend.js +2 -5
- package/lib/server/agent/codegen.js +83 -0
- package/lib/server/agent/context.js +43 -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/codegen/javascript.js +6 -29
- package/lib/server/dispatchers/dispatcher.js +5 -8
- package/lib/server/dispatchers/pageDispatcher.js +3 -2
- 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/utils/isomorphic/stringUtils.js +29 -0
- package/lib/vite/htmlReport/index.html +1 -1
- 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/types/types.d.ts +27 -2
- package/lib/vite/traceViewer/index.C4Y3Aw8n.css +0 -1
- package/lib/vite/traceViewer/index.YskCIlQ-.js +0 -2
|
@@ -90,7 +90,7 @@ class JavaScriptLanguageGenerator {
|
|
|
90
90
|
case "fill":
|
|
91
91
|
return `await ${subject}.${this._asLocator(action.selector)}.fill(${quote(action.text)});`;
|
|
92
92
|
case "setInputFiles":
|
|
93
|
-
return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`;
|
|
93
|
+
return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${(0, import_utils.formatObject)(action.files.length === 1 ? action.files[0] : action.files)});`;
|
|
94
94
|
case "press": {
|
|
95
95
|
const modifiers = (0, import_language.toKeyboardModifiers)(action.modifiers);
|
|
96
96
|
const shortcut = [...modifiers, action.key].join("+");
|
|
@@ -99,7 +99,7 @@ class JavaScriptLanguageGenerator {
|
|
|
99
99
|
case "navigate":
|
|
100
100
|
return `await ${subject}.goto(${quote(action.url)});`;
|
|
101
101
|
case "select":
|
|
102
|
-
return `await ${subject}.${this._asLocator(action.selector)}.selectOption(${formatObject(action.options.length === 1 ? action.options[0] : action.options)});`;
|
|
102
|
+
return `await ${subject}.${this._asLocator(action.selector)}.selectOption(${(0, import_utils.formatObject)(action.options.length === 1 ? action.options[0] : action.options)});`;
|
|
103
103
|
case "assertText":
|
|
104
104
|
return `${this._isTest ? "" : "// "}await expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? "toContainText" : "toHaveText"}(${quote(action.text)});`;
|
|
105
105
|
case "assertChecked":
|
|
@@ -151,7 +151,7 @@ ${useText ? "\ntest.use(" + useText + ");\n" : ""}
|
|
|
151
151
|
const { ${options.browserName}${options.deviceName ? ", devices" : ""} } = require('playwright');
|
|
152
152
|
|
|
153
153
|
(async () => {
|
|
154
|
-
const browser = await ${options.browserName}.launch(${formatObjectOrVoid(options.launchOptions)});
|
|
154
|
+
const browser = await ${options.browserName}.launch(${(0, import_utils.formatObjectOrVoid)(options.launchOptions)});
|
|
155
155
|
const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName, false)});`);
|
|
156
156
|
if (options.contextOptions.recordHar)
|
|
157
157
|
formatter.add(` await context.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
|
|
@@ -171,37 +171,14 @@ function formatOptions(value, hasArguments) {
|
|
|
171
171
|
const keys = Object.keys(value).filter((key) => value[key] !== void 0);
|
|
172
172
|
if (!keys.length)
|
|
173
173
|
return "";
|
|
174
|
-
return (hasArguments ? ", " : "") + formatObject(value);
|
|
175
|
-
}
|
|
176
|
-
function formatObject(value, indent = " ") {
|
|
177
|
-
if (typeof value === "string")
|
|
178
|
-
return quote(value);
|
|
179
|
-
if (Array.isArray(value))
|
|
180
|
-
return `[${value.map((o) => formatObject(o)).join(", ")}]`;
|
|
181
|
-
if (typeof value === "object") {
|
|
182
|
-
const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
|
|
183
|
-
if (!keys.length)
|
|
184
|
-
return "{}";
|
|
185
|
-
const tokens = [];
|
|
186
|
-
for (const key of keys)
|
|
187
|
-
tokens.push(`${key}: ${formatObject(value[key])}`);
|
|
188
|
-
return `{
|
|
189
|
-
${indent}${tokens.join(`,
|
|
190
|
-
${indent}`)}
|
|
191
|
-
}`;
|
|
192
|
-
}
|
|
193
|
-
return String(value);
|
|
194
|
-
}
|
|
195
|
-
function formatObjectOrVoid(value, indent = " ") {
|
|
196
|
-
const result = formatObject(value, indent);
|
|
197
|
-
return result === "{}" ? "" : result;
|
|
174
|
+
return (hasArguments ? ", " : "") + (0, import_utils.formatObject)(value);
|
|
198
175
|
}
|
|
199
176
|
function formatContextOptions(options, deviceName, isTest) {
|
|
200
177
|
const device = deviceName && import_deviceDescriptors.deviceDescriptors[deviceName];
|
|
201
178
|
options = { ...options, recordHar: void 0 };
|
|
202
179
|
if (!device)
|
|
203
|
-
return formatObjectOrVoid(options);
|
|
204
|
-
let serializedObject = formatObjectOrVoid((0, import_language.sanitizeDeviceOptions)(device, options));
|
|
180
|
+
return (0, import_utils.formatObjectOrVoid)(options);
|
|
181
|
+
let serializedObject = (0, import_utils.formatObjectOrVoid)((0, import_language.sanitizeDeviceOptions)(device, options));
|
|
205
182
|
if (!serializedObject)
|
|
206
183
|
serializedObject = "{\n}";
|
|
207
184
|
const lines = serializedObject.split("\n");
|
|
@@ -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,
|
|
@@ -260,10 +260,11 @@ class PageDispatcher extends import_dispatcher.Dispatcher {
|
|
|
260
260
|
return { pdf: buffer };
|
|
261
261
|
}
|
|
262
262
|
async perform(params, progress) {
|
|
263
|
-
await (0, import_agent.pagePerform)(progress, this._page, params);
|
|
263
|
+
return await (0, import_agent.pagePerform)(progress, this._page, params);
|
|
264
264
|
}
|
|
265
265
|
async extract(params, progress) {
|
|
266
|
-
|
|
266
|
+
const { result, usage } = await (0, import_agent.pageExtract)(progress, this._page, params);
|
|
267
|
+
return { result, ...usage };
|
|
267
268
|
}
|
|
268
269
|
async requests(params, progress) {
|
|
269
270
|
this._subscriptions.add("request");
|
|
@@ -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
|
}
|
|
@@ -245,7 +245,7 @@ class WKPage {
|
|
|
245
245
|
this._provisionalPage.dispose();
|
|
246
246
|
this._provisionalPage = null;
|
|
247
247
|
}
|
|
248
|
-
this._firstNonInitialNavigationCommittedReject(new import_errors.TargetClosedError());
|
|
248
|
+
this._firstNonInitialNavigationCommittedReject(new import_errors.TargetClosedError(this._page.closeReason()));
|
|
249
249
|
this._page._didClose();
|
|
250
250
|
}
|
|
251
251
|
dispatchMessageToSession(message) {
|
|
@@ -441,7 +441,7 @@ class WKPage {
|
|
|
441
441
|
}
|
|
442
442
|
async navigateFrame(frame, url, referrer) {
|
|
443
443
|
if (this._pageProxySession.isDisposed())
|
|
444
|
-
throw new import_errors.TargetClosedError();
|
|
444
|
+
throw new import_errors.TargetClosedError(this._page.closeReason());
|
|
445
445
|
const pageProxyId = this._pageProxySession.sessionId;
|
|
446
446
|
const result = await this._pageProxySession.connection.browserSession.send("Playwright.navigate", { url, pageProxyId, frameId: frame._id, referrer });
|
|
447
447
|
return { newDocumentId: result.loaderId };
|
|
@@ -750,9 +750,10 @@ class WKPage {
|
|
|
750
750
|
return 0;
|
|
751
751
|
}
|
|
752
752
|
async _initializeVideoRecording() {
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
753
|
+
const screencast = this._page.screencast;
|
|
754
|
+
const videoOptions = screencast.launchVideoRecorder();
|
|
755
|
+
if (videoOptions)
|
|
756
|
+
await screencast.startVideoRecording(videoOptions);
|
|
756
757
|
}
|
|
757
758
|
validateScreenshotDimension(side, omitDeviceScaleFactor) {
|
|
758
759
|
if (process.platform === "darwin")
|
|
@@ -840,8 +841,7 @@ class WKPage {
|
|
|
840
841
|
const buffer = Buffer.from(event.data, "base64");
|
|
841
842
|
this._page.emit(import_page.Page.Events.ScreencastFrame, {
|
|
842
843
|
buffer,
|
|
843
|
-
frameSwapWallTime: event.timestamp * 1e3,
|
|
844
|
-
// timestamp is in seconds, we need to convert to milliseconds.
|
|
844
|
+
frameSwapWallTime: event.timestamp ? event.timestamp * 1e3 : Date.now(),
|
|
845
845
|
width: event.deviceWidth,
|
|
846
846
|
height: event.deviceHeight
|
|
847
847
|
});
|
|
@@ -26,6 +26,8 @@ __export(stringUtils_exports, {
|
|
|
26
26
|
escapeRegExp: () => escapeRegExp,
|
|
27
27
|
escapeTemplateString: () => escapeTemplateString,
|
|
28
28
|
escapeWithQuotes: () => escapeWithQuotes,
|
|
29
|
+
formatObject: () => formatObject,
|
|
30
|
+
formatObjectOrVoid: () => formatObjectOrVoid,
|
|
29
31
|
isString: () => isString,
|
|
30
32
|
longestCommonSubstring: () => longestCommonSubstring,
|
|
31
33
|
normalizeEscapedRegexQuotes: () => normalizeEscapedRegexQuotes,
|
|
@@ -60,6 +62,31 @@ function toTitleCase(name) {
|
|
|
60
62
|
function toSnakeCase(name) {
|
|
61
63
|
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
|
|
62
64
|
}
|
|
65
|
+
function formatObject(value, indent = " ", mode = "multiline") {
|
|
66
|
+
if (typeof value === "string")
|
|
67
|
+
return escapeWithQuotes(value, "'");
|
|
68
|
+
if (Array.isArray(value))
|
|
69
|
+
return `[${value.map((o) => formatObject(o)).join(", ")}]`;
|
|
70
|
+
if (typeof value === "object") {
|
|
71
|
+
const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
|
|
72
|
+
if (!keys.length)
|
|
73
|
+
return "{}";
|
|
74
|
+
const tokens = [];
|
|
75
|
+
for (const key of keys)
|
|
76
|
+
tokens.push(`${key}: ${formatObject(value[key])}`);
|
|
77
|
+
if (mode === "multiline")
|
|
78
|
+
return `{
|
|
79
|
+
${tokens.join(`,
|
|
80
|
+
${indent}`)}
|
|
81
|
+
}`;
|
|
82
|
+
return `{ ${tokens.join(", ")} }`;
|
|
83
|
+
}
|
|
84
|
+
return String(value);
|
|
85
|
+
}
|
|
86
|
+
function formatObjectOrVoid(value, indent = " ") {
|
|
87
|
+
const result = formatObject(value, indent);
|
|
88
|
+
return result === "{}" ? "" : result;
|
|
89
|
+
}
|
|
63
90
|
function quoteCSSAttributeValue(text) {
|
|
64
91
|
return `"${text.replace(/["\\]/g, (char) => "\\" + char)}"`;
|
|
65
92
|
}
|
|
@@ -143,6 +170,8 @@ function longestCommonSubstring(s1, s2) {
|
|
|
143
170
|
escapeRegExp,
|
|
144
171
|
escapeTemplateString,
|
|
145
172
|
escapeWithQuotes,
|
|
173
|
+
formatObject,
|
|
174
|
+
formatObjectOrVoid,
|
|
146
175
|
isString,
|
|
147
176
|
longestCommonSubstring,
|
|
148
177
|
normalizeEscapedRegexQuotes,
|