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.
Files changed (39) hide show
  1. package/ThirdPartyNotices.txt +3 -3
  2. package/browsers.json +2 -2
  3. package/lib/client/browser.js +3 -5
  4. package/lib/client/browserType.js +2 -2
  5. package/lib/client/fetch.js +2 -4
  6. package/lib/client/page.js +4 -3
  7. package/lib/mcpBundleImpl/index.js +29 -29
  8. package/lib/protocol/serializers.js +5 -0
  9. package/lib/protocol/validator.js +26 -8
  10. package/lib/server/agent/actionRunner.js +33 -2
  11. package/lib/server/agent/agent.js +28 -12
  12. package/lib/server/agent/backend.js +2 -5
  13. package/lib/server/agent/codegen.js +83 -0
  14. package/lib/server/agent/context.js +43 -11
  15. package/lib/server/agent/tools.js +67 -5
  16. package/lib/server/artifact.js +1 -1
  17. package/lib/server/chromium/crPage.js +5 -5
  18. package/lib/server/codegen/javascript.js +6 -29
  19. package/lib/server/dispatchers/dispatcher.js +5 -8
  20. package/lib/server/dispatchers/pageDispatcher.js +3 -2
  21. package/lib/server/firefox/ffBrowser.js +1 -1
  22. package/lib/server/firefox/ffPage.js +1 -1
  23. package/lib/server/instrumentation.js +3 -0
  24. package/lib/server/page.js +2 -2
  25. package/lib/server/progress.js +2 -0
  26. package/lib/server/screencast.js +24 -25
  27. package/lib/server/videoRecorder.js +20 -11
  28. package/lib/server/webkit/wkBrowser.js +1 -1
  29. package/lib/server/webkit/wkPage.js +7 -7
  30. package/lib/utils/isomorphic/stringUtils.js +29 -0
  31. package/lib/vite/htmlReport/index.html +1 -1
  32. package/lib/vite/traceViewer/index.BVu7tZDe.css +1 -0
  33. package/lib/vite/traceViewer/index.html +2 -2
  34. package/lib/vite/traceViewer/index.zFV_GQE-.js +2 -0
  35. package/lib/vite/traceViewer/sw.bundle.js +3 -1
  36. package/package.json +1 -1
  37. package/types/types.d.ts +27 -2
  38. package/lib/vite/traceViewer/index.C4Y3Aw8n.css +0 -1
  39. 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(sdkObject));
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(sdkObject);
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(sdkObject), e.browserLogMessage());
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
- return { result: await (0, import_agent.pageExtract)(progress, this._page, params) };
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() };
@@ -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.closeReason = options.reason;
580
+ this._closeReason = options.reason;
581
581
  const runBeforeUnload = !!options.runBeforeUnload;
582
582
  if (this._closedState !== "closing") {
583
583
  if (!runBeforeUnload)
@@ -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 };
@@ -40,11 +40,12 @@ var import_registry = require("./registry");
40
40
  class Screencast {
41
41
  constructor(page) {
42
42
  this._videoRecorder = null;
43
- this._screencastId = null;
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
- async initializeVideoRecorder() {
64
+ launchVideoRecorder() {
64
65
  const recordVideo = this._page.browserContext._options.recordVideo;
65
66
  if (!recordVideo)
66
67
  return void 0;
67
- const screencastId = (0, import_utils.createGuid)();
68
- const outputFile = import_path.default.join(recordVideo.dir, screencastId + ".webm");
69
- const screencastOptions = {
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
- await (0, import_utils.mkdirIfNeeded)(import_path.default.join(recordVideo.dir, "dummy"));
75
- await this._createVideoRecorder(screencastId, screencastOptions);
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 screencastOptions;
84
+ return videoOptions;
82
85
  }
83
86
  async startVideoRecording(options) {
84
- const screencastId = this._screencastId;
85
- (0, import_utils.assert)(screencastId);
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, screencastId, options.outputFile, this._page.waitForInitializedOrError());
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._screencastId)
102
+ if (!this._videoId)
100
103
  return;
101
- const screencastId = this._screencastId;
102
- this._screencastId = null;
103
- const recorder = this._videoRecorder;
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(recorder);
106
- await recorder.stop().catch(() => {
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(page, ffmpegPath) {
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
- const recorder = new VideoRecorder(page, ffmpegPath);
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.writeFrame(Buffer.from([]), this._lastFrame.timestamp + addTime);
111
+ this._writeFrame(Buffer.from([]), this._lastFrame.timestamp + addTime);
107
112
  this._isStopped = true;
108
- await this._lastWritePromise;
109
- await this._gracefullyClose();
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 screencastOptions = await this._page.screencast.initializeVideoRecorder();
754
- if (screencastOptions)
755
- await this._page.screencast.startVideoRecording(screencastOptions);
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,