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.
- package/CHANGELOG.md +71 -2
- package/README.md +1 -1
- package/browserscripts/timings/softNavigations.js +94 -0
- package/lib/chrome/har.js +36 -5
- package/lib/chrome/settings/chromeAndroidOptions.js +2 -1
- package/lib/chrome/webdriver/chromium.js +39 -0
- package/lib/chrome/webdriver/setupChromiumOptions.js +86 -31
- package/lib/core/engine/collector.js +340 -317
- package/lib/core/engine/command/addText.js +93 -12
- package/lib/core/engine/command/click.js +239 -139
- package/lib/core/engine/command/commandHelper.js +45 -0
- package/lib/core/engine/command/cookie.js +85 -0
- package/lib/core/engine/command/element.js +37 -2
- package/lib/core/engine/command/javaScript.js +14 -24
- package/lib/core/engine/command/measure/screenshots.js +114 -0
- package/lib/core/engine/command/measure/video.js +85 -0
- package/lib/core/engine/command/measure.js +104 -180
- package/lib/core/engine/command/mouse/clickAndHold.js +6 -3
- package/lib/core/engine/command/mouse/contextClick.js +56 -4
- package/lib/core/engine/command/mouse/doubleClick.js +49 -1
- package/lib/core/engine/command/mouse/mouseMove.js +50 -1
- package/lib/core/engine/command/mouse/singleClick.js +165 -129
- package/lib/core/engine/command/navigation.js +6 -6
- package/lib/core/engine/command/select.js +95 -3
- package/lib/core/engine/command/selectorParser.js +58 -0
- package/lib/core/engine/command/set.js +55 -1
- package/lib/core/engine/command/wait.js +41 -2
- package/lib/core/engine/commands.js +550 -42
- package/lib/core/engine/index.js +206 -150
- package/lib/core/engine/iteration.js +269 -137
- package/lib/firefox/networkManager.js +22 -30
- package/lib/firefox/webdriver/builder.js +14 -0
- package/lib/firefox/webdriver/firefox.js +1 -1
- package/lib/safari/har.js +131 -0
- package/lib/safari/native/ios-capture +0 -0
- package/lib/safari/native/ios-capture.m +340 -0
- package/lib/safari/safariInspectorClient.js +322 -0
- package/lib/safari/webdriver/safari.js +303 -13
- package/lib/safari/webkitToCdpAdapter.js +241 -0
- package/lib/support/cli.js +13 -0
- package/lib/support/engineUtils.js +16 -5
- package/lib/support/getViewPort.js +5 -0
- package/lib/support/har/index.js +41 -3
- package/lib/support/usbPower.js +2 -3
- package/lib/video/postprocessing/finetune/addTextToVideo.js +4 -0
- package/lib/video/postprocessing/finetune/getFont.js +3 -3
- package/lib/video/postprocessing/finetune/getTimingMetrics.js +7 -0
- package/lib/video/postprocessing/finetune/index.js +26 -9
- package/lib/video/screenRecording/ios/iosDeviceRecorder.js +109 -0
- package/lib/video/screenRecording/recorder.js +2 -2
- package/lib/video/screenRecording/setOrangeBackground.js +3 -0
- package/package.json +12 -12
- package/types/core/engine/command/addText.d.ts +35 -6
- package/types/core/engine/command/addText.d.ts.map +1 -1
- package/types/core/engine/command/click.d.ts +87 -16
- package/types/core/engine/command/click.d.ts.map +1 -1
- package/types/core/engine/command/commandHelper.d.ts +16 -0
- package/types/core/engine/command/commandHelper.d.ts.map +1 -0
- package/types/core/engine/command/cookie.d.ts +65 -0
- package/types/core/engine/command/cookie.d.ts.map +1 -0
- package/types/core/engine/command/element.d.ts +19 -1
- package/types/core/engine/command/element.d.ts.map +1 -1
- package/types/core/engine/command/javaScript.d.ts +4 -0
- package/types/core/engine/command/javaScript.d.ts.map +1 -1
- package/types/core/engine/command/measure/screenshots.d.ts +15 -0
- package/types/core/engine/command/measure/screenshots.d.ts.map +1 -0
- package/types/core/engine/command/measure/video.d.ts +26 -0
- package/types/core/engine/command/measure/video.d.ts.map +1 -0
- package/types/core/engine/command/measure.d.ts +59 -33
- package/types/core/engine/command/measure.d.ts.map +1 -1
- package/types/core/engine/command/mouse/clickAndHold.d.ts.map +1 -1
- package/types/core/engine/command/mouse/contextClick.d.ts +28 -4
- package/types/core/engine/command/mouse/contextClick.d.ts.map +1 -1
- package/types/core/engine/command/mouse/doubleClick.d.ts +24 -4
- package/types/core/engine/command/mouse/doubleClick.d.ts.map +1 -1
- package/types/core/engine/command/mouse/mouseMove.d.ts +30 -5
- package/types/core/engine/command/mouse/mouseMove.d.ts.map +1 -1
- package/types/core/engine/command/mouse/singleClick.d.ts +50 -13
- package/types/core/engine/command/mouse/singleClick.d.ts.map +1 -1
- package/types/core/engine/command/navigation.d.ts +3 -3
- package/types/core/engine/command/select.d.ts +45 -8
- package/types/core/engine/command/select.d.ts.map +1 -1
- package/types/core/engine/command/selectorParser.d.ts +20 -0
- package/types/core/engine/command/selectorParser.d.ts.map +1 -0
- package/types/core/engine/command/set.d.ts +34 -7
- package/types/core/engine/command/set.d.ts.map +1 -1
- package/types/core/engine/command/wait.d.ts +29 -6
- package/types/core/engine/command/wait.d.ts.map +1 -1
- package/types/core/engine/commands.d.ts +296 -28
- package/types/core/engine/commands.d.ts.map +1 -1
- package/types/support/engineUtils.d.ts.map +1 -1
- package/types/support/getViewPort.d.ts.map +1 -1
- package/types/support/usbPower.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/getTimingMetrics.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/index.d.ts.map +1 -1
- package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts +28 -0
- package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts.map +1 -0
- package/types/video/screenRecording/recorder.d.ts +2 -2
- package/types/video/screenRecording/recorder.d.ts.map +1 -1
- package/types/video/screenRecording/setOrangeBackground.d.ts.map +1 -1
- package/types/video/video.d.ts +1 -1
- package/types/video/video.d.ts.map +1 -1
- package/visualmetrics/visualmetrics-portable.py +136 -136
- package/lib/video/screenRecording/ios/convertToMp4.js +0 -22
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
+
}
|