browsertime 26.3.1 → 27.0.0

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 (106) hide show
  1. package/CHANGELOG.md +71 -2
  2. package/README.md +1 -1
  3. package/browserscripts/timings/softNavigations.js +94 -0
  4. package/lib/chrome/har.js +36 -5
  5. package/lib/chrome/settings/chromeAndroidOptions.js +2 -1
  6. package/lib/chrome/webdriver/chromium.js +39 -0
  7. package/lib/chrome/webdriver/setupChromiumOptions.js +86 -31
  8. package/lib/core/engine/collector.js +340 -317
  9. package/lib/core/engine/command/addText.js +93 -12
  10. package/lib/core/engine/command/click.js +239 -139
  11. package/lib/core/engine/command/commandHelper.js +45 -0
  12. package/lib/core/engine/command/cookie.js +85 -0
  13. package/lib/core/engine/command/element.js +37 -2
  14. package/lib/core/engine/command/javaScript.js +14 -24
  15. package/lib/core/engine/command/measure/screenshots.js +114 -0
  16. package/lib/core/engine/command/measure/video.js +85 -0
  17. package/lib/core/engine/command/measure.js +104 -180
  18. package/lib/core/engine/command/mouse/clickAndHold.js +6 -3
  19. package/lib/core/engine/command/mouse/contextClick.js +56 -4
  20. package/lib/core/engine/command/mouse/doubleClick.js +49 -1
  21. package/lib/core/engine/command/mouse/mouseMove.js +50 -1
  22. package/lib/core/engine/command/mouse/singleClick.js +165 -129
  23. package/lib/core/engine/command/navigation.js +6 -6
  24. package/lib/core/engine/command/select.js +95 -3
  25. package/lib/core/engine/command/selectorParser.js +58 -0
  26. package/lib/core/engine/command/set.js +55 -1
  27. package/lib/core/engine/command/wait.js +41 -2
  28. package/lib/core/engine/commands.js +550 -42
  29. package/lib/core/engine/index.js +206 -150
  30. package/lib/core/engine/iteration.js +269 -137
  31. package/lib/firefox/networkManager.js +22 -30
  32. package/lib/firefox/webdriver/builder.js +14 -0
  33. package/lib/firefox/webdriver/firefox.js +1 -1
  34. package/lib/safari/har.js +131 -0
  35. package/lib/safari/native/ios-capture +0 -0
  36. package/lib/safari/native/ios-capture.m +340 -0
  37. package/lib/safari/safariInspectorClient.js +322 -0
  38. package/lib/safari/webdriver/safari.js +303 -13
  39. package/lib/safari/webkitToCdpAdapter.js +241 -0
  40. package/lib/support/cli.js +13 -0
  41. package/lib/support/engineUtils.js +16 -5
  42. package/lib/support/getViewPort.js +5 -0
  43. package/lib/support/har/index.js +41 -3
  44. package/lib/support/usbPower.js +2 -3
  45. package/lib/video/postprocessing/finetune/addTextToVideo.js +4 -0
  46. package/lib/video/postprocessing/finetune/getFont.js +3 -3
  47. package/lib/video/postprocessing/finetune/getTimingMetrics.js +7 -0
  48. package/lib/video/postprocessing/finetune/index.js +26 -9
  49. package/lib/video/screenRecording/ios/iosDeviceRecorder.js +109 -0
  50. package/lib/video/screenRecording/recorder.js +2 -2
  51. package/lib/video/screenRecording/setOrangeBackground.js +3 -0
  52. package/package.json +12 -12
  53. package/types/core/engine/command/addText.d.ts +35 -6
  54. package/types/core/engine/command/addText.d.ts.map +1 -1
  55. package/types/core/engine/command/click.d.ts +87 -16
  56. package/types/core/engine/command/click.d.ts.map +1 -1
  57. package/types/core/engine/command/commandHelper.d.ts +16 -0
  58. package/types/core/engine/command/commandHelper.d.ts.map +1 -0
  59. package/types/core/engine/command/cookie.d.ts +65 -0
  60. package/types/core/engine/command/cookie.d.ts.map +1 -0
  61. package/types/core/engine/command/element.d.ts +19 -1
  62. package/types/core/engine/command/element.d.ts.map +1 -1
  63. package/types/core/engine/command/javaScript.d.ts +4 -0
  64. package/types/core/engine/command/javaScript.d.ts.map +1 -1
  65. package/types/core/engine/command/measure/screenshots.d.ts +15 -0
  66. package/types/core/engine/command/measure/screenshots.d.ts.map +1 -0
  67. package/types/core/engine/command/measure/video.d.ts +26 -0
  68. package/types/core/engine/command/measure/video.d.ts.map +1 -0
  69. package/types/core/engine/command/measure.d.ts +59 -33
  70. package/types/core/engine/command/measure.d.ts.map +1 -1
  71. package/types/core/engine/command/mouse/clickAndHold.d.ts.map +1 -1
  72. package/types/core/engine/command/mouse/contextClick.d.ts +28 -4
  73. package/types/core/engine/command/mouse/contextClick.d.ts.map +1 -1
  74. package/types/core/engine/command/mouse/doubleClick.d.ts +24 -4
  75. package/types/core/engine/command/mouse/doubleClick.d.ts.map +1 -1
  76. package/types/core/engine/command/mouse/mouseMove.d.ts +30 -5
  77. package/types/core/engine/command/mouse/mouseMove.d.ts.map +1 -1
  78. package/types/core/engine/command/mouse/singleClick.d.ts +50 -13
  79. package/types/core/engine/command/mouse/singleClick.d.ts.map +1 -1
  80. package/types/core/engine/command/navigation.d.ts +3 -3
  81. package/types/core/engine/command/select.d.ts +45 -8
  82. package/types/core/engine/command/select.d.ts.map +1 -1
  83. package/types/core/engine/command/selectorParser.d.ts +20 -0
  84. package/types/core/engine/command/selectorParser.d.ts.map +1 -0
  85. package/types/core/engine/command/set.d.ts +34 -7
  86. package/types/core/engine/command/set.d.ts.map +1 -1
  87. package/types/core/engine/command/wait.d.ts +29 -6
  88. package/types/core/engine/command/wait.d.ts.map +1 -1
  89. package/types/core/engine/commands.d.ts +296 -28
  90. package/types/core/engine/commands.d.ts.map +1 -1
  91. package/types/support/engineUtils.d.ts.map +1 -1
  92. package/types/support/getViewPort.d.ts.map +1 -1
  93. package/types/support/usbPower.d.ts.map +1 -1
  94. package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
  95. package/types/video/postprocessing/finetune/getTimingMetrics.d.ts.map +1 -1
  96. package/types/video/postprocessing/finetune/index.d.ts.map +1 -1
  97. package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts +28 -0
  98. package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts.map +1 -0
  99. package/types/video/screenRecording/recorder.d.ts +2 -2
  100. package/types/video/screenRecording/recorder.d.ts.map +1 -1
  101. package/types/video/screenRecording/setOrangeBackground.d.ts.map +1 -1
  102. package/types/video/video.d.ts +1 -1
  103. package/types/video/video.d.ts.map +1 -1
  104. package/visualmetrics/visualmetrics-portable.py +136 -136
  105. package/lib/video/screenRecording/ios/convertToMp4.js +0 -22
  106. package/lib/video/screenRecording/ios/iosRecorder.js +0 -68
@@ -84,16 +84,7 @@ export class Iteration {
84
84
  const engineDelegate = this.engineDelegate;
85
85
 
86
86
  try {
87
- if (options.flushDNS) {
88
- await flushDNS(options);
89
- }
90
- if (options.connectivity && options.connectivity.variance) {
91
- await addConnectivity(options);
92
- }
93
-
94
- await engineDelegate.beforeBrowserStart();
95
- await browser.start();
96
- await engineDelegate.afterBrowserStart(browser);
87
+ await this._startBrowser(browser, engineDelegate, options);
97
88
 
98
89
  // The data we push to all selenium scripts
99
90
  const context = new Context(
@@ -106,35 +97,23 @@ export class Iteration {
106
97
  browser.getDriver()
107
98
  );
108
99
 
109
- const commands = new Commands(
100
+ const commands = new Commands({
110
101
  browser,
111
102
  engineDelegate,
112
103
  index,
113
104
  result,
114
- this.storageManager,
115
- this.pageCompleteCheck,
105
+ storageManager: this.storageManager,
106
+ pageCompleteCheck: this.pageCompleteCheck,
116
107
  context,
117
108
  videos,
118
109
  screenshotManager,
119
- this.scriptsByCategory,
120
- this.asyncScriptsByCategory,
121
- this.postURLScripts,
110
+ scriptsByCategory: this.scriptsByCategory,
111
+ asyncScriptsByCategory: this.asyncScriptsByCategory,
112
+ postURLScripts: this.postURLScripts,
122
113
  options
123
- );
114
+ });
124
115
 
125
- // Safari can't load data:text URLs at startup, so you can either choose
126
- // your own URL or use the blank one
127
- const startURL =
128
- options.safari && options.safari.startURL
129
- ? options.safari.startURL
130
- : 'data:text/html;charset=utf-8,<html><body></body></html>';
131
- if (options.spa) {
132
- await setResourceTimingBufferSize(startURL, browser.getDriver(), 1000);
133
- } else if (!options.processStartTime) {
134
- // Before we needed Chrome to open once to get correct values for Chrome HAR
135
- // but now Firefox first visual change is slower if we remove this ...
136
- await browser.getDriver().get(startURL);
137
- }
116
+ await this._loadStartURL(browser, options);
138
117
 
139
118
  // On slowish Android phones it takes some time for the
140
119
  // browser to get ready
@@ -150,32 +129,14 @@ export class Iteration {
150
129
  await this.video.record(0, index);
151
130
  }
152
131
 
153
- for (const preScript of this.preScripts) {
154
- await preScript(context, commands);
155
- }
156
-
157
- if (options.preURL) {
158
- await preURL(browser, engineDelegate, this.pageCompleteCheck, options);
159
- }
160
-
161
- try {
162
- await navigationScript(context, commands, this.postURLScripts);
163
- } catch (error) {
164
- commands.error(error.message);
165
- commands.markAsFailure(error.message);
166
- throw error;
167
- }
168
- if (commands.measure.areWeMeasuring === true) {
169
- // someone forgot to call stop();
170
- log.info(
171
- 'It looks like you missed to call measure.stop() after calling measure.start(..) (without using a URL). Checkout https://www.sitespeed.io/documentation/sitespeed.io/scripting/#measurestartalias'
172
- );
173
- await commands.measure.stop();
174
- }
175
-
176
- for (const postScript of this.postScripts) {
177
- await postScript(context, commands);
178
- }
132
+ await this._runScripts(
133
+ navigationScript,
134
+ context,
135
+ commands,
136
+ options,
137
+ browser,
138
+ engineDelegate
139
+ );
179
140
 
180
141
  if (recordVideo && options.videoParams.debug) {
181
142
  // Just use the first URL
@@ -184,95 +145,32 @@ export class Iteration {
184
145
  await this.video.stop(url);
185
146
  }
186
147
 
187
- await engineDelegate.beforeBrowserStop(browser, index, result);
188
-
189
- // if we are in debig mode we wait on a continue command
190
- if (options.debug) {
191
- await commands.debug.breakpoint('End-of-iteration');
192
- }
193
-
194
- if (options.browser === 'firefox' && options.debug) {
195
- log.info('Firefox is kept open in debug mode');
196
- } else {
197
- if (isAndroidConfigured(options) && options.ignoreShutdownFailures) {
198
- try {
199
- log.info('Ignoring shutdown failures on android...');
200
- await browser.stop();
201
- } catch {
202
- // Ignore shutdown failures on android
203
- log.info('Shutdown problem hit, ignoring...');
204
- }
205
- } else {
206
- await browser.stop();
207
- }
208
- }
209
- // Give the browsers some time to stop
210
- await delay(2000);
211
- await engineDelegate.afterBrowserStopped();
148
+ await this._stopBrowser(
149
+ browser,
150
+ engineDelegate,
151
+ options,
152
+ result,
153
+ index,
154
+ commands
155
+ );
212
156
 
213
157
  if (isAndroidConfigured(options)) {
214
158
  batteryTemperature.stop = await this.android.getTemperature();
215
159
  await this.android.removeDevtoolsFw();
216
160
  }
217
161
 
218
- if (options.visualMetrics && !options.videoParams.debug) {
219
- let index_ = 0;
220
- for (let myVideo of videos) {
221
- try {
222
- const videoMetrics = await myVideo.postProcessing(
223
- result[index_].browserScripts.timings.pageTimings,
224
- result[index_].browserScripts.pageinfo.visualElements
225
- );
226
-
227
- for (let progress of [
228
- 'VisualProgress',
229
- 'ContentfulSpeedIndexProgress',
230
- 'PerceptualSpeedIndexProgress'
231
- ]) {
232
- if (videoMetrics.visualMetrics[progress]) {
233
- videoMetrics.visualMetrics[progress] = jsonifyVisualProgress(
234
- videoMetrics.visualMetrics[progress]
235
- );
236
- }
237
- }
238
- if (videoMetrics.visualMetrics['KeyColorFrames']) {
239
- videoMetrics.visualMetrics['KeyColorFrames'] =
240
- jsonifyKeyColorFrames(
241
- videoMetrics.visualMetrics['KeyColorFrames']
242
- );
243
- }
244
- result[index_].videoRecordingStart =
245
- videoMetrics.videoRecordingStart;
246
- result[index_].visualMetrics = videoMetrics.visualMetrics;
247
- index_++;
248
- } catch (error) {
249
- // If one of the Visual Metrics runs fail, try the next one
250
- log.error('Visual Metrics failed to analyse the video', error);
251
- if (result[index_]) {
252
- result[index_].error.push(error.message);
253
- }
254
- }
255
- }
256
- }
257
-
258
- result.screenshots = screenshotManager.getSaved();
162
+ await this._processVisualMetrics(videos, result, options);
259
163
 
260
- if (engineDelegate.postWork) {
261
- await engineDelegate.postWork(index, result);
262
- }
263
-
264
- if (isAndroidConfigured(options)) {
265
- result.batteryTemperature = batteryTemperature;
266
- }
267
-
268
- if (commands.meta.description) {
269
- result.description = commands.meta.description;
270
- }
271
- if (commands.meta.title) {
272
- result.title = commands.meta.title;
273
- }
164
+ this._collectResults(
165
+ result,
166
+ screenshotManager,
167
+ engineDelegate,
168
+ index,
169
+ options,
170
+ commands,
171
+ batteryTemperature
172
+ );
274
173
 
275
- result.processesAtStart = this.processesAtStart;
276
174
  return result;
277
175
  } catch (error) {
278
176
  log.error(error);
@@ -306,6 +204,240 @@ export class Iteration {
306
204
  } catch {
307
205
  // Most cases the browser been stopped already
308
206
  }
207
+ // Ensure delegate cleanup runs even on failure paths so that
208
+ // long-lived helpers (e.g. ios-capture, safaridriver) are released.
209
+ try {
210
+ await engineDelegate.afterBrowserStopped();
211
+ } catch {
212
+ // Idempotent — may have already run via _stopBrowser
213
+ }
214
+ }
215
+ }
216
+
217
+ async _startBrowser(browser, engineDelegate, options) {
218
+ if (options.flushDNS) {
219
+ await flushDNS(options);
220
+ }
221
+ if (options.connectivity && options.connectivity.variance) {
222
+ await addConnectivity(options);
223
+ }
224
+
225
+ await engineDelegate.beforeBrowserStart();
226
+ await browser.start();
227
+ // The runner deep-merges options into a fresh object before passing them
228
+ // to the browser-specific builder, so anything the builder records there
229
+ // never reaches the engine. Copy it back to the engine's options object
230
+ // so result.info.browser can pick it up. Issue #1622.
231
+ if (browser.options && browser.options.recordedBrowserSettings) {
232
+ this.options.recordedBrowserSettings =
233
+ browser.options.recordedBrowserSettings;
234
+ }
235
+ await engineDelegate.afterBrowserStart(browser);
236
+ }
237
+
238
+ async _loadStartURL(browser, options) {
239
+ // Safari can't load data:text URLs at startup, so you can either choose
240
+ // your own URL or use the blank one
241
+ const startURL =
242
+ options.safari && options.safari.startURL
243
+ ? options.safari.startURL
244
+ : 'data:text/html;charset=utf-8,<html><body></body></html>';
245
+ if (options.spa) {
246
+ await setResourceTimingBufferSize(startURL, browser.getDriver(), 1000);
247
+ } else if (!options.processStartTime) {
248
+ // Before we needed Chrome to open once to get correct values for Chrome HAR
249
+ // but now Firefox first visual change is slower if we remove this ...
250
+ await browser.getDriver().get(startURL);
251
+ }
252
+ }
253
+
254
+ async _runScripts(
255
+ navigationScript,
256
+ context,
257
+ commands,
258
+ options,
259
+ browser,
260
+ engineDelegate
261
+ ) {
262
+ for (const preScript of this.preScripts) {
263
+ await preScript(context, commands);
264
+ }
265
+
266
+ if (options.preURL) {
267
+ await preURL(browser, engineDelegate, this.pageCompleteCheck, options);
268
+ }
269
+
270
+ try {
271
+ await navigationScript(context, commands, this.postURLScripts);
272
+ } catch (error) {
273
+ commands.error(error.message);
274
+ commands.markAsFailure(error.message);
275
+ throw error;
276
+ }
277
+ if (commands.measure.areWeMeasuring === true) {
278
+ // someone forgot to call stop();
279
+ log.info(
280
+ 'It looks like you missed to call measure.stop() after calling measure.start(..) (without using a URL). Checkout https://www.sitespeed.io/documentation/sitespeed.io/scripting/#measurestartalias'
281
+ );
282
+ await commands.measure.stop();
283
+ }
284
+
285
+ for (const postScript of this.postScripts) {
286
+ await postScript(context, commands);
287
+ }
288
+ }
289
+
290
+ async _stopBrowser(
291
+ browser,
292
+ engineDelegate,
293
+ options,
294
+ result,
295
+ index,
296
+ commands
297
+ ) {
298
+ await engineDelegate.beforeBrowserStop(browser, index, result);
299
+
300
+ // if we are in debig mode we wait on a continue command
301
+ if (options.debug) {
302
+ await commands.debug.breakpoint('End-of-iteration');
303
+ }
304
+
305
+ if (options.browser === 'firefox' && options.debug) {
306
+ log.info('Firefox is kept open in debug mode');
307
+ } else {
308
+ if (isAndroidConfigured(options) && options.ignoreShutdownFailures) {
309
+ try {
310
+ log.info('Ignoring shutdown failures on android...');
311
+ await browser.stop();
312
+ } catch {
313
+ // Ignore shutdown failures on android
314
+ log.info('Shutdown problem hit, ignoring...');
315
+ }
316
+ } else {
317
+ await browser.stop();
318
+ }
319
+ }
320
+ // Give the browsers some time to stop
321
+ await delay(2000);
322
+ await engineDelegate.afterBrowserStopped();
323
+ }
324
+
325
+ async _processVisualMetrics(videos, result, options) {
326
+ if (options.visualMetrics && !options.videoParams.debug) {
327
+ let index_ = 0;
328
+ for (let myVideo of videos) {
329
+ try {
330
+ const videoMetrics = await myVideo.postProcessing(
331
+ result[index_].browserScripts.timings.pageTimings,
332
+ result[index_].browserScripts.pageinfo.visualElements
333
+ );
334
+
335
+ for (let progress of [
336
+ 'VisualProgress',
337
+ 'ContentfulSpeedIndexProgress',
338
+ 'PerceptualSpeedIndexProgress'
339
+ ]) {
340
+ if (videoMetrics.visualMetrics[progress]) {
341
+ videoMetrics.visualMetrics[progress] = jsonifyVisualProgress(
342
+ videoMetrics.visualMetrics[progress]
343
+ );
344
+ }
345
+ }
346
+ if (videoMetrics.visualMetrics['KeyColorFrames']) {
347
+ videoMetrics.visualMetrics['KeyColorFrames'] =
348
+ jsonifyKeyColorFrames(
349
+ videoMetrics.visualMetrics['KeyColorFrames']
350
+ );
351
+ }
352
+ result[index_].videoRecordingStart = videoMetrics.videoRecordingStart;
353
+ result[index_].visualMetrics = videoMetrics.visualMetrics;
354
+
355
+ // iOS real device video has USB mirroring latency (~1s).
356
+ // Sync visual metrics to FCP so firstVisualChange matches
357
+ // the browser-reported first contentful paint.
358
+ if (options.safari?.ios) {
359
+ syncVisualMetricsToFCP(result[index_]);
360
+ }
361
+
362
+ index_++;
363
+ } catch (error) {
364
+ // If one of the Visual Metrics runs fail, try the next one
365
+ log.error('Visual Metrics failed to analyse the video', error);
366
+ if (result[index_]) {
367
+ result[index_].error.push(error.message);
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ _collectResults(
375
+ result,
376
+ screenshotManager,
377
+ engineDelegate,
378
+ index,
379
+ options,
380
+ commands,
381
+ batteryTemperature
382
+ ) {
383
+ result.screenshots = screenshotManager.getSaved();
384
+
385
+ if (engineDelegate.postWork) {
386
+ engineDelegate.postWork(index, result);
387
+ }
388
+
389
+ if (isAndroidConfigured(options)) {
390
+ result.batteryTemperature = batteryTemperature;
391
+ }
392
+
393
+ if (commands.meta.description) {
394
+ result.description = commands.meta.description;
395
+ }
396
+ if (commands.meta.title) {
397
+ result.title = commands.meta.title;
398
+ }
399
+
400
+ result.processesAtStart = this.processesAtStart;
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Compensate for USB screen mirroring latency on iOS real devices.
406
+ * The video stream is delayed ~1s compared to what happens on the device.
407
+ * We align visual metrics to the browser-reported FCP (First Contentful Paint)
408
+ * since both measure the same event — when content first appears on screen.
409
+ */
410
+ function syncVisualMetricsToFCP(resultEntry) {
411
+ const vm = resultEntry.visualMetrics;
412
+ if (!vm) return;
413
+
414
+ const fcp =
415
+ resultEntry.browserScripts?.timings?.paintTiming?.[
416
+ 'first-contentful-paint'
417
+ ];
418
+ if (!fcp || fcp <= 0) return;
419
+ if (!vm.FirstVisualChange || vm.FirstVisualChange <= fcp) return;
420
+
421
+ const offset = vm.FirstVisualChange - fcp;
422
+
423
+ for (const key of [
424
+ 'FirstVisualChange',
425
+ 'LastVisualChange',
426
+ 'SpeedIndex',
427
+ 'VisualComplete85',
428
+ 'VisualComplete95',
429
+ 'VisualComplete99'
430
+ ]) {
431
+ if (vm[key] > offset) {
432
+ vm[key] -= offset;
433
+ }
434
+ }
435
+
436
+ if (vm.VisualProgress) {
437
+ for (const entry of vm.VisualProgress) {
438
+ if (entry.timestamp > offset) {
439
+ entry.timestamp -= offset;
440
+ }
309
441
  }
310
442
  }
311
443
  }
@@ -14,6 +14,21 @@ export class NetworkManager {
14
14
  this.idleTime = getProperty(options, 'timeouts.networkIdle', 5000);
15
15
  }
16
16
 
17
+ async _cleanup(messageHandler) {
18
+ if (messageHandler && this.ws) {
19
+ this.ws.removeListener('message', messageHandler);
20
+ }
21
+ await this.bidi.unsubscribe(
22
+ 'network.beforeRequestSent',
23
+ this.browsingContextIds
24
+ );
25
+ await this.bidi.unsubscribe(
26
+ 'network.responseCompleted',
27
+ this.browsingContextIds
28
+ );
29
+ await this.bidi.unsubscribe('network.fetchError', this.browsingContextIds);
30
+ }
31
+
17
32
  async waitForNetworkIdle() {
18
33
  await this.bidi.subscribe(
19
34
  'network.beforeRequestSent',
@@ -26,10 +41,10 @@ export class NetworkManager {
26
41
  await this.bidi.subscribe('network.fetchError', this.browsingContextIds);
27
42
 
28
43
  let inflight = 0;
29
- let lastRequestTimestamp;
30
- let lastResponseTimestamp;
44
+ let lastRequestTimestamp = Date.now();
45
+ let lastResponseTimestamp = Date.now();
31
46
  this.ws = await this.bidi.socket;
32
- this.ws.on('message', function (event) {
47
+ const messageHandler = function (event) {
33
48
  const { method } = JSON.parse(Buffer.from(event.toString()));
34
49
  if (method) {
35
50
  switch (method) {
@@ -54,7 +69,8 @@ export class NetworkManager {
54
69
  // No default
55
70
  }
56
71
  }
57
- });
72
+ };
73
+ this.ws.on('message', messageHandler);
58
74
 
59
75
  const startTime = Date.now();
60
76
 
@@ -71,20 +87,7 @@ export class NetworkManager {
71
87
  inflight
72
88
  );
73
89
  }
74
- await this.bidi.unsubscribe(
75
- 'network.beforeRequestSent',
76
- this.browsingContextIds
77
- );
78
- await this.bidi.unsubscribe(
79
- 'network.responseCompleted',
80
- this.browsingContextIds
81
- );
82
-
83
- await this.bidi.unsubscribe(
84
- 'network.fetchError',
85
- this.browsingContextIds
86
- );
87
-
90
+ await this._cleanup(messageHandler);
88
91
  break;
89
92
  }
90
93
 
@@ -92,18 +95,7 @@ export class NetworkManager {
92
95
  log.info(
93
96
  'Timeout waiting for network idle. Inflight requests:' + inflight
94
97
  );
95
- await this.bidi.unsubscribe(
96
- 'network.beforeRequestSent',
97
- this.browsingContextIds
98
- );
99
- await this.bidi.unsubscribe(
100
- 'network.responseCompleted',
101
- this.browsingContextIds
102
- );
103
- await this.bidi.unsubscribe(
104
- 'network.fetchError',
105
- this.browsingContextIds
106
- );
98
+ await this._cleanup(messageHandler);
107
99
  break;
108
100
  }
109
101
 
@@ -266,4 +266,18 @@ export async function configureBuilder(builder, baseDir, options) {
266
266
  }
267
267
 
268
268
  builder.setFirefoxOptions(ffOptions);
269
+
270
+ // Snapshot every argument / preference / capability we just applied so it
271
+ // can be surfaced in the result JSON for debugging — issue #1622. The
272
+ // runner deep-merges options into a fresh container before this is called,
273
+ // so iteration.js threads it back to the engine after browser.start().
274
+ const ffInternals = ffOptions.firefoxOptions_();
275
+ options.recordedBrowserSettings = {
276
+ args: [...(ffInternals.args || [])],
277
+ preferences: { ...ffInternals.prefs },
278
+ binary: ffInternals.binary,
279
+ profile: ffInternals.profile,
280
+ androidPackage: ffInternals.androidPackage,
281
+ androidActivity: ffInternals.androidActivity
282
+ };
269
283
  }
@@ -65,9 +65,9 @@ export class Firefox {
65
65
  if (!this.options.skipHar) {
66
66
  this.har = new adapters.SeleniumBiDiHarRecorder({
67
67
  browsingContextIds: [this.windowId],
68
+ skipBodyData: this.includeResponseBodies === 'all' ? false : true,
68
69
  debugLogs:
69
70
  this.options.verbose >= 2 || this.firefoxConfig.enableBidiHarLog,
70
- maxBodySize: this.includeResponseBodies === 'all' ? 10_485_760 : 1,
71
71
  driver: runner.getDriver(),
72
72
  headerValueFormatter: this.options.cleanSensitiveHeaders
73
73
  ? cleanSensitiveHeaders
@@ -0,0 +1,131 @@
1
+ import { harFromMessages } from 'chrome-har';
2
+ import { getLogger } from '@sitespeed.io/log';
3
+ import { cleanSensitiveHeaders } from '../support/har/index.js';
4
+
5
+ const log = getLogger('browsertime.safari.har');
6
+
7
+ export async function getHar(
8
+ adapter,
9
+ result,
10
+ index,
11
+ inspectorClient,
12
+ includeResponseBodies,
13
+ aliasAndUrl,
14
+ cleanHeaders
15
+ ) {
16
+ const messages = adapter.getMessages();
17
+
18
+ let har;
19
+ try {
20
+ har = harFromMessages(messages);
21
+ } catch (error) {
22
+ log.error('Failed to generate HAR from messages: %s', error.message);
23
+ log.verbose(error);
24
+ return getEmptyHar();
25
+ }
26
+
27
+ // Fix timing values and remove cached entries without timing data
28
+ har.log.entries = har.log.entries.filter(entry => {
29
+ // Remove entries without _requestTime — these are cached resources
30
+ // that WebKit reported but have no useful timing data
31
+ const noTiming =
32
+ entry._requestTime === undefined || entry._requestTime === 0;
33
+ if (noTiming) {
34
+ return false;
35
+ }
36
+
37
+ // Fix any remaining NaN/non-number timing values
38
+ if (entry.timings) {
39
+ for (const key of Object.keys(entry.timings)) {
40
+ if (key === 'comment') {
41
+ // comment must be a string per HAR spec
42
+ if (typeof entry.timings[key] !== 'string') {
43
+ delete entry.timings[key];
44
+ }
45
+ } else if (
46
+ typeof entry.timings[key] !== 'number' ||
47
+ !Number.isFinite(entry.timings[key])
48
+ ) {
49
+ entry.timings[key] = 0;
50
+ }
51
+ }
52
+ }
53
+ if (typeof entry.time !== 'number' || !Number.isFinite(entry.time)) {
54
+ entry.time = 0;
55
+ }
56
+ return true;
57
+ });
58
+
59
+ // Add browser info
60
+ har.log.creator = {
61
+ name: 'Browsertime',
62
+ version: '1.0'
63
+ };
64
+ har.log.browser = {
65
+ name: 'Safari',
66
+ version: ''
67
+ };
68
+
69
+ // Clean sensitive headers if requested
70
+ if (cleanHeaders === true) {
71
+ for (const entry of har.log.entries ?? []) {
72
+ for (const header of entry.request?.headers ?? []) {
73
+ header.value = cleanSensitiveHeaders(header.name, header.value);
74
+ }
75
+ for (const header of entry.response?.headers ?? []) {
76
+ header.value = cleanSensitiveHeaders(header.name, header.value);
77
+ }
78
+ }
79
+ }
80
+
81
+ // Handle response bodies
82
+ if (
83
+ inspectorClient &&
84
+ (includeResponseBodies === 'html' || includeResponseBodies === 'all')
85
+ ) {
86
+ for (const entry of har.log.entries) {
87
+ if (
88
+ includeResponseBodies === 'html' &&
89
+ !entry.response.content.mimeType?.includes('text/html')
90
+ ) {
91
+ continue;
92
+ }
93
+ try {
94
+ const response = await inspectorClient.send('Network.getResponseBody', {
95
+ requestId: entry._requestId
96
+ });
97
+ if (response?.result?.body) {
98
+ entry.response.content.text = response.result.body;
99
+ if (response.result.base64Encoded) {
100
+ entry.response.content.encoding = 'base64';
101
+ }
102
+ }
103
+ } catch {
104
+ log.debug('Could not get response body for %s', entry.request.url);
105
+ }
106
+ }
107
+ }
108
+
109
+ // Set page title and URL
110
+ if (har.log.pages.length > 0) {
111
+ const url = result.url || '';
112
+ har.log.pages[0].title = `${url} run ${index}`;
113
+ const alias = aliasAndUrl[url] || url;
114
+ har.log.pages[0]._url = url;
115
+ har.log.pages[0]._alias = alias;
116
+ }
117
+
118
+ return har;
119
+ }
120
+
121
+ function getEmptyHar() {
122
+ return {
123
+ log: {
124
+ version: '1.2',
125
+ creator: { name: 'Browsertime', version: '1.0' },
126
+ browser: { name: 'Safari', version: '' },
127
+ pages: [],
128
+ entries: []
129
+ }
130
+ };
131
+ }