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
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,78 @@
|
|
|
1
1
|
# Browsertime changelog (we do [semantic versioning](https://semver.org))
|
|
2
2
|
|
|
3
|
+
## 27.0.0 - 2026-05-01
|
|
4
|
+
|
|
5
|
+
### Highlights
|
|
6
|
+
|
|
7
|
+
This release is a big push on the scripting API and on iOS real-device testing. All old scripting commands will work but we have new and improveed
|
|
8
|
+
syntax that you can use if you follow our documentation. This release also tries to support the upcoming Chrome soft navigations.
|
|
9
|
+
|
|
10
|
+
* **Unified selector syntax across commands.** All interaction commands now accept selectors directly (`commands.click('#btn')`, `commands.addText('#input', 'text')`, etc.) with optional `id:`/`xpath:`/`text:`/`link:`/`name:`/`class:` prefixes. The old `AndWait` family is deprecated in favour of `{ waitForNavigation: true }`.
|
|
11
|
+
* **A much bigger convenience-command surface.** New `type`, `find`, `getText`, `getValue`, `isVisible`, `exists`, `clear`, `fill`, `getAttribute`, `isEnabled`, `isChecked`, `hover`, `press`, `getTitle`, `getUrl`, `check`, `uncheck`, `scrollIntoView`, `select.byText`, `waitForUrl`, `clickAndMeasure`, `click.byText`, plus a full `cookie` command.
|
|
12
|
+
* **Chrome soft navigations.** SPA route changes (React, Next.js, Vue, Turbo) now produce a HAR page and the standard FCP/LCP/CLS/INP metrics, using Chrome's PerformanceObserver soft-navigation entry type.
|
|
13
|
+
* **Safari on iOS over USB.** HAR via `ios_webkit_debug_proxy`, plus video recording and visual metrics via a CoreMediaIO screen-capture helper.
|
|
14
|
+
* **TypeScript navigation scripts** are now supported on Node.js 22.18.0+ via native type stripping.
|
|
15
|
+
|
|
16
|
+
### Breaking
|
|
17
|
+
* Changed default `--timeouts.elementWait` from 0 to 6000ms. All element commands (click, addText) now auto-wait up to 6 seconds for elements to appear. Set `--timeouts.elementWait 0` to restore the old behavior [#2389](https://github.com/sitespeedio/browsertime/pull/2389).
|
|
18
|
+
* Click commands now use the Selenium Actions API for real OS-level mouse events instead of JavaScript `.click()`. Elements must be visible and interactable. If you need to click a hidden element, use `commands.js.run()` instead [#2381](https://github.com/sitespeedio/browsertime/pull/2381).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
* Unified selector syntax for all interaction commands. You can now call `commands.click('#btn')`, `commands.addText('#input', 'text')`, `commands.wait('#el', { timeout: 5000 })`, `commands.select('#dropdown', 'value')`, and `commands.set('#field', 'value')` directly. Supports prefixes: `id:`, `xpath:`, `text:`, `link:`, `name:`, `class:` — CSS selector is the default [#2390](https://github.com/sitespeedio/browsertime/pull/2390) [#2408](https://github.com/sitespeedio/browsertime/pull/2408).
|
|
22
|
+
* The `AndWait` methods are deprecated in favour of `{ waitForNavigation: true }` [#2390](https://github.com/sitespeedio/browsertime/pull/2390) [#2396](https://github.com/sitespeedio/browsertime/pull/2396).
|
|
23
|
+
* Navigation methods (back, forward, refresh) now support `{ waitForNavigation: true }` for consistency [#2408](https://github.com/sitespeedio/browsertime/pull/2408).
|
|
24
|
+
* Added `commands.type(selector, text)` as a shorthand for `addText` with a more conventional parameter order [#2387](https://github.com/sitespeedio/browsertime/pull/2387).
|
|
25
|
+
* Added `commands.find(selector, options)` helper that combines element lookup with optional waiting and visibility checks [#2386](https://github.com/sitespeedio/browsertime/pull/2386).
|
|
26
|
+
* Added `commands.measure.clickAndMeasure(alias, selector)` shortcut for the common measure.start/click/measure.stop pattern [#2382](https://github.com/sitespeedio/browsertime/pull/2382).
|
|
27
|
+
* Added `commands.click.byText(text)` to click any element by visible text, not just links [#2384](https://github.com/sitespeedio/browsertime/pull/2384).
|
|
28
|
+
* Added `--timeouts.elementWait` option for auto-waiting on elements before interaction [#2383](https://github.com/sitespeedio/browsertime/pull/2383).
|
|
29
|
+
* Added support for TypeScript scripts (.ts, .mts, .cts) using Node.js native type stripping (requires Node.js 22.18.0+) [#2363](https://github.com/sitespeedio/browsertime/pull/2363).
|
|
30
|
+
* Added `commands.getText(selector)`, `commands.getValue(selector)`, and `commands.isVisible(selector)` convenience methods [#2411](https://github.com/sitespeedio/browsertime/pull/2411).
|
|
31
|
+
* Added `commands.exists(selector, { timeout })` for checking element presence without throwing [#2412](https://github.com/sitespeedio/browsertime/pull/2412).
|
|
32
|
+
* Added `commands.clear(selector)` to clear form element content [#2413](https://github.com/sitespeedio/browsertime/pull/2413).
|
|
33
|
+
* Added `commands.fill({ selector: text, ... })` for filling multiple form fields at once [#2414](https://github.com/sitespeedio/browsertime/pull/2414).
|
|
34
|
+
* Added unified selector syntax to mouse commands: `commands.mouse.singleClick(selector)`, `commands.mouse.doubleClick(selector)`, `commands.mouse.contextClick(selector)`, `commands.mouse.moveTo(selector)` [#2415](https://github.com/sitespeedio/browsertime/pull/2415).
|
|
35
|
+
* Added `commands.cookie` for managing browser cookies: `get`, `getAll`, `set`, `delete`, `deleteAll` [#2420](https://github.com/sitespeedio/browsertime/pull/2420).
|
|
36
|
+
* Added `commands.getAttribute(selector, name)`, `commands.isEnabled(selector)`, and `commands.isChecked(selector)` for element state queries [#2421](https://github.com/sitespeedio/browsertime/pull/2421).
|
|
37
|
+
* Added `commands.hover(selector)` as alias for mouse.moveTo, `commands.press(key)` for keyboard input, `commands.getTitle()` and `commands.getUrl()` for page info [#2422](https://github.com/sitespeedio/browsertime/pull/2422).
|
|
38
|
+
* Added `commands.check(selector)` and `commands.uncheck(selector)` for checkbox/radio interactions [#2423](https://github.com/sitespeedio/browsertime/pull/2423).
|
|
39
|
+
* Added `commands.scrollIntoView(selector)` to scroll elements into the viewport [#2425](https://github.com/sitespeedio/browsertime/pull/2425).
|
|
40
|
+
* Added `commands.select.byText(selector, text)` to select dropdown options by visible text [#2426](https://github.com/sitespeedio/browsertime/pull/2426).
|
|
41
|
+
* Added `commands.waitForUrl(pattern, { timeout })` to wait for URL changes after navigation [#2427](https://github.com/sitespeedio/browsertime/pull/2427).
|
|
42
|
+
* Added video recording and visual metrics for Safari on iOS over USB. A native helper captures the device's screen via CoreMediaIO and pipes frames to ffmpeg. The device is woken into screen-capture mode automatically — no manual QuickTime step required [#2432](https://github.com/sitespeedio/browsertime/pull/2432) [#2436](https://github.com/sitespeedio/browsertime/pull/2436).
|
|
43
|
+
* Added HAR capture for Safari on iOS via `ios_webkit_debug_proxy`. Browsertime starts and stops `iwdp` automatically and exits with a clear error if it isn't installed (`brew install ios-webkit-debug-proxy`). New `--safari.includeResponseBodies` CLI option. macOS Safari is unaffected [#2431](https://github.com/sitespeedio/browsertime/pull/2431).
|
|
44
|
+
* Added soft navigation support for Chrome. Single-page app route changes (React, Next.js, Vue, Turbo) now produce a HAR page and the standard FCP/LCP/CLS/INP metrics, using Chrome's PerformanceObserver soft-navigation entry type to confirm user interaction + URL change + visible paint [#2438](https://github.com/sitespeedio/browsertime/pull/2438).
|
|
45
|
+
* Updated dependencies to latest compatible versions: chrome-har 1.2.1, selenium-webdriver 4.43.0, execa 9.6.1, jimp 1.6.1 [#2437](https://github.com/sitespeedio/browsertime/pull/2437).
|
|
46
|
+
* Firefox 149 in the Docker container [#2375](https://github.com/sitespeedio/browsertime/pull/2375).
|
|
47
|
+
* Record the full Chrome/Edge configuration browsertime applied to the browser (args, preferences, mobile emulation, binary path, extensions count) under `info.browser` in the result JSON, not just what the user passed via `--chrome.args` [#2447](https://github.com/sitespeedio/browsertime/pull/2447).
|
|
48
|
+
* Record Firefox preferences and the resolved binary path under `info.browser` in the result JSON [#2448](https://github.com/sitespeedio/browsertime/pull/2448).
|
|
49
|
+
* Write long-task ranges and user-timing marks/measures into the HAR under `pages[].pageTimings` as `_longTasks` (array of `[startMs, endMs]` tuples) and `_userTimings` (object keyed by entry name → startTime), so downstream waterfall viewers can show what the browser was doing between requests [#2449](https://github.com/sitespeedio/browsertime/pull/2449).
|
|
50
|
+
* Added modern device profiles to the mobile emulation device list [#2446](https://github.com/sitespeedio/browsertime/pull/2446).
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
* Pin the iOS Safari tab being measured so unrelated open tabs cannot clobber the active inspector target. The WebDriver tab is navigated to a sentinel URL and identified by it before iWDP messages are processed, otherwise an unrelated `Target.targetCreated` event could leave the HAR empty [#2440](https://github.com/sitespeedio/browsertime/pull/2440).
|
|
54
|
+
* Fixed `drawtext` failing on ffmpeg 8 by quoting font paths and forwarding ffmpeg stderr so font/filter errors are visible [#2435](https://github.com/sitespeedio/browsertime/pull/2435).
|
|
55
|
+
* Fixed Firefox network idle timeout on repeated iterations. The WebSocket message listener was never removed between iterations, causing the inflight counter to go negative [#2391](https://github.com/sitespeedio/browsertime/pull/2391).
|
|
56
|
+
* Fixed Firefox network idle timeout when no network events arrive. Timestamps were initialized as undefined causing the idle check to never trigger [#2394](https://github.com/sitespeedio/browsertime/pull/2394).
|
|
57
|
+
* Fixed intermittent crash when setting orange background before document.body exists on Edge/Windows [#2393](https://github.com/sitespeedio/browsertime/pull/2393).
|
|
58
|
+
* Element interaction error messages now include the current page URL for easier debugging [#2385](https://github.com/sitespeedio/browsertime/pull/2385).
|
|
59
|
+
* Added `actions.clear()` after `perform()` in all mouse commands to prevent pointer/key state leaking between actions [#2406](https://github.com/sitespeedio/browsertime/pull/2406).
|
|
60
|
+
* Standardized error handling in all command `run()` methods using `executeCommand` with URL context. Fixed typo in select.deselectById and removed dead code in wait [#2410](https://github.com/sitespeedio/browsertime/pull/2410).
|
|
61
|
+
* Added clear error message when loading TypeScript scripts on Node.js older than 22.18.0 [#2404](https://github.com/sitespeedio/browsertime/pull/2404).
|
|
62
|
+
* Disabled the "You can zoom in and out" popup on Android [#2378](https://github.com/sitespeedio/browsertime/pull/2378).
|
|
63
|
+
* Warn when `--viewPort` is set with a width below 500px on desktop Chrome. Chrome enforces a hard ~500px minimum window width and silently clamps narrower requests, but browsertime kept recording the filmstrip and video at the requested size, leaving captures cropped. The warning points users at mobile emulation, which lays out at the requested size while the OS window stays desktop-sized [#2445](https://github.com/sitespeedio/browsertime/pull/2445).
|
|
64
|
+
* Stop a perceptual SSIM failure from discarding every other visual metric for the iteration. `calculate_perceptual_speed_index` now returns `(None, None)` on internal errors instead of letting the exception propagate up through `calculate_visual_metrics`, so SpeedIndex, FCP, LCP and the rest still flow through when SSIM can't run [#2444](https://github.com/sitespeedio/browsertime/pull/2444).
|
|
65
|
+
* Drain ffmpeg/ffprobe stderr after the process exits in `visualmetrics-portable.py`. The previous Popen+poll loop dropped output buffered between the last read and exit — silently losing `keep pts:` lines and therefore filmstrip frames — and could deadlock when ffmpeg's debug output filled the OS pipe buffer [#2443](https://github.com/sitespeedio/browsertime/pull/2443).
|
|
66
|
+
* Converted `visualmetrics-portable.py` path handling to `pathlib` for consistent cross-platform behaviour [#2442](https://github.com/sitespeedio/browsertime/pull/2442).
|
|
67
|
+
|
|
68
|
+
## 26.3.2 - 2026-01-24
|
|
69
|
+
### Fixed
|
|
70
|
+
* Hello the new and fixed bidi-har 0.0.22 that fixes the response body [#2361](https://github.com/sitespeedio/browsertime/pull/2361).
|
|
71
|
+
|
|
72
|
+
|
|
3
73
|
## 26.3.1 - 2026-01-11
|
|
4
74
|
### Fixed
|
|
5
|
-
* Reverting back to bidi-har 0.0.18. There where error/failures either in bidi har or in combination with browsertime in the latest release [#2360](https://github.com/sitespeedio/browsertime/pull/2360)
|
|
6
|
-
|
|
75
|
+
* Reverting back to bidi-har 0.0.18. There where error/failures either in bidi har or in combination with browsertime in the latest release [#2360](https://github.com/sitespeedio/browsertime/pull/2360).
|
|
7
76
|
|
|
8
77
|
## 26.3.0 - 2026-01-09
|
|
9
78
|
|
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ The default video will include a timer and showing when the metrics happens, but
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
## Browser Support
|
|
160
|
-
Browsertime supports Firefox, Chrome, and Edge (Chromium version) on desktop and Safari on Mac OS. On Android we support Chrome and Firefox. Safari on iOS
|
|
160
|
+
Browsertime supports Firefox, Chrome, and Edge (Chromium version) on desktop and Safari on Mac OS. On Android we support Chrome and Firefox. Safari on iOS supports HAR (via `ios_webkit_debug_proxy`) and video / visual metrics (via screen capture over USB). You can also use the Safari simulator on Mac OS.
|
|
161
161
|
|
|
162
162
|
## How does it work
|
|
163
163
|
Browsertime uses Selenium NodeJS to drive the browser. It starts the browser, load a URL, executes configurable Javascripts to collect metrics, collect a HAR file.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const supported = PerformanceObserver.supportedEntryTypes;
|
|
3
|
+
if (!supported || supported.indexOf('soft-navigation') === -1) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
var startIndex = window.__bt_softNavCount || 0;
|
|
7
|
+
const allNew = performance.getEntriesByType('soft-navigation').slice(startIndex);
|
|
8
|
+
if (allNew.length === 0) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
// Use the last entry — a single user interaction can trigger
|
|
12
|
+
// multiple intermediate soft navigations (e.g. React, Turbo)
|
|
13
|
+
const entry = allNew[allNew.length - 1];
|
|
14
|
+
var navId = entry.navigationId;
|
|
15
|
+
|
|
16
|
+
// CLS for this soft navigation
|
|
17
|
+
var cls = 0;
|
|
18
|
+
if (supported.indexOf('layout-shift') !== -1) {
|
|
19
|
+
const observer = new PerformanceObserver(list => {});
|
|
20
|
+
observer.observe({type: 'layout-shift', buffered: true});
|
|
21
|
+
const shifts = observer.takeRecords().filter(function (s) {
|
|
22
|
+
return s.navigationId === navId;
|
|
23
|
+
});
|
|
24
|
+
var max = 0;
|
|
25
|
+
var curr = 0;
|
|
26
|
+
var firstTs = Number.NEGATIVE_INFINITY;
|
|
27
|
+
var prevTs = Number.NEGATIVE_INFINITY;
|
|
28
|
+
for (let s of shifts) {
|
|
29
|
+
if (s.hadRecentInput) continue;
|
|
30
|
+
if (s.startTime - firstTs > 5000 || s.startTime - prevTs > 1000) {
|
|
31
|
+
firstTs = s.startTime;
|
|
32
|
+
curr = 0;
|
|
33
|
+
}
|
|
34
|
+
prevTs = s.startTime;
|
|
35
|
+
curr += s.value;
|
|
36
|
+
max = Math.max(max, curr);
|
|
37
|
+
}
|
|
38
|
+
cls = max;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// INP for this soft navigation
|
|
42
|
+
var inp = 0;
|
|
43
|
+
if (supported.indexOf('event') !== -1) {
|
|
44
|
+
const observer = new PerformanceObserver(list => {});
|
|
45
|
+
observer.observe({type: 'event', buffered: true});
|
|
46
|
+
const events = observer.takeRecords().filter(function (e) {
|
|
47
|
+
return e.navigationId === navId && e.interactionId;
|
|
48
|
+
});
|
|
49
|
+
const MAX_INTERACTIONS = 10;
|
|
50
|
+
const longestList = [];
|
|
51
|
+
const longestMap = {};
|
|
52
|
+
for (let e of events) {
|
|
53
|
+
var minLongest = longestList[longestList.length - 1];
|
|
54
|
+
var existing = longestMap[e.interactionId];
|
|
55
|
+
if (existing || longestList.length < MAX_INTERACTIONS || e.duration > minLongest.latency) {
|
|
56
|
+
if (existing) {
|
|
57
|
+
existing.latency = Math.max(existing.latency, e.duration);
|
|
58
|
+
} else {
|
|
59
|
+
var interaction = { id: e.interactionId, latency: e.duration };
|
|
60
|
+
longestMap[interaction.id] = interaction;
|
|
61
|
+
longestList.push(interaction);
|
|
62
|
+
}
|
|
63
|
+
longestList.sort(function (a, b) { return b.latency - a.latency; });
|
|
64
|
+
longestList.splice(MAX_INTERACTIONS).forEach(function (i) { delete longestMap[i.id]; });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
var inpEntry = longestList[longestList.length - 1];
|
|
68
|
+
inp = inpEntry ? inpEntry.latency : 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// LCP value
|
|
72
|
+
var lcp = 0;
|
|
73
|
+
if (entry.largestInteractionContentfulPaint) {
|
|
74
|
+
lcp = typeof entry.largestInteractionContentfulPaint === 'number'
|
|
75
|
+
? entry.largestInteractionContentfulPaint
|
|
76
|
+
: entry.largestInteractionContentfulPaint.renderTime ||
|
|
77
|
+
entry.largestInteractionContentfulPaint.loadTime ||
|
|
78
|
+
entry.largestInteractionContentfulPaint.startTime || 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return [{
|
|
82
|
+
name: entry.name,
|
|
83
|
+
navigationId: navId,
|
|
84
|
+
interactionId: entry.interactionId,
|
|
85
|
+
startTime: Number(entry.startTime.toFixed(0)),
|
|
86
|
+
duration: Number(entry.duration.toFixed(0)),
|
|
87
|
+
firstContentfulPaint: entry.presentationTime
|
|
88
|
+
? Number(entry.presentationTime.toFixed(0))
|
|
89
|
+
: 0,
|
|
90
|
+
largestInteractionContentfulPaint: Number(lcp.toFixed(0)),
|
|
91
|
+
cumulativeLayoutShift: cls,
|
|
92
|
+
interactionToNextPaint: inp
|
|
93
|
+
}];
|
|
94
|
+
})();
|
package/lib/chrome/har.js
CHANGED
|
@@ -23,6 +23,32 @@ export async function getHar(
|
|
|
23
23
|
const logs = await runner.getLogs(Type.PERFORMANCE);
|
|
24
24
|
const messages = logs.map(entry => JSON.parse(entry.message).message);
|
|
25
25
|
|
|
26
|
+
// Inject a SoftNavigation.detected event so chrome-har renames the
|
|
27
|
+
// current HAR page with the soft navigation URL. The PerformanceObserver
|
|
28
|
+
// API is the only reliable source that confirms all three soft navigation
|
|
29
|
+
// criteria (user interaction + URL change + visible paint).
|
|
30
|
+
try {
|
|
31
|
+
const softNavEntries = await runner.runScript(
|
|
32
|
+
`const supported = PerformanceObserver.supportedEntryTypes;
|
|
33
|
+
if (!supported || supported.indexOf('soft-navigation') === -1) return [];
|
|
34
|
+
const startIndex = window.__bt_softNavCount || 0;
|
|
35
|
+
const entries = performance.getEntriesByType('soft-navigation').slice(startIndex);
|
|
36
|
+
if (entries.length === 0) return [];
|
|
37
|
+
const last = entries[entries.length - 1];
|
|
38
|
+
return [{ url: last.name, startTime: last.startTime }];`,
|
|
39
|
+
'GET_SOFT_NAVIGATIONS_FOR_HAR'
|
|
40
|
+
);
|
|
41
|
+
if (softNavEntries && softNavEntries.length > 0) {
|
|
42
|
+
const entry = softNavEntries[0];
|
|
43
|
+
messages.push({
|
|
44
|
+
method: 'SoftNavigation.detected',
|
|
45
|
+
params: { url: entry.url, startTime: entry.startTime }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Soft navigations not supported, ignore
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
if (logPerfEntries) {
|
|
27
53
|
if (!result.extraJson) {
|
|
28
54
|
result.extraJson = {};
|
|
@@ -68,15 +94,20 @@ export async function getHar(
|
|
|
68
94
|
}
|
|
69
95
|
|
|
70
96
|
if (har.log.pages.length > 0) {
|
|
71
|
-
har.log.pages[0]
|
|
97
|
+
const page = har.log.pages[0];
|
|
98
|
+
// For soft navigations, use the soft navigation URL as the title
|
|
99
|
+
const pageUrl = page._softNavigation ? page.title : result.url;
|
|
100
|
+
page.title = `${pageUrl} run ${index}`;
|
|
72
101
|
// Hack to add the URL from a SPA
|
|
73
|
-
if (
|
|
102
|
+
if (page._softNavigation) {
|
|
103
|
+
page._url = pageUrl;
|
|
104
|
+
} else if (result.alias && !aliasAndUrl[result.alias]) {
|
|
74
105
|
aliasAndUrl[result.alias] = result.url;
|
|
75
|
-
|
|
106
|
+
page._url = result.url;
|
|
76
107
|
} else if (result.alias && aliasAndUrl[result.alias]) {
|
|
77
|
-
|
|
108
|
+
page._url = aliasAndUrl[result.alias];
|
|
78
109
|
} else {
|
|
79
|
-
|
|
110
|
+
page._url = result.url;
|
|
80
111
|
}
|
|
81
112
|
}
|
|
82
113
|
return har;
|
|
@@ -26,5 +26,6 @@ export const chromeAndroidOptions = [
|
|
|
26
26
|
'--disable-breakpad',
|
|
27
27
|
'--dont-require-litepage-redirect-infobar',
|
|
28
28
|
'--override-https-image-compression-infobar',
|
|
29
|
-
'--disable-fetching-hints-at-navigation-start'
|
|
29
|
+
'--disable-fetching-hints-at-navigation-start',
|
|
30
|
+
'--propagate-iph-for-testing'
|
|
30
31
|
];
|
|
@@ -420,6 +420,45 @@ export class Chromium {
|
|
|
420
420
|
result.renderBlocking.requests = render.renderBlockingInfo;
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
// If a soft navigation was detected, promote its metrics to the
|
|
424
|
+
// standard locations so they flow through googleWebVitals, logging,
|
|
425
|
+
// and statistics just like regular page load metrics.
|
|
426
|
+
if (
|
|
427
|
+
result.browserScripts.timings &&
|
|
428
|
+
result.browserScripts.timings.softNavigations &&
|
|
429
|
+
result.browserScripts.timings.softNavigations.length > 0
|
|
430
|
+
) {
|
|
431
|
+
const softNav = result.browserScripts.timings.softNavigations.at(-1);
|
|
432
|
+
const navStart = softNav.startTime;
|
|
433
|
+
|
|
434
|
+
if (softNav.firstContentfulPaint) {
|
|
435
|
+
result.browserScripts.timings.paintTiming =
|
|
436
|
+
result.browserScripts.timings.paintTiming || {};
|
|
437
|
+
result.browserScripts.timings.paintTiming['first-contentful-paint'] =
|
|
438
|
+
softNav.firstContentfulPaint - navStart;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (softNav.largestInteractionContentfulPaint > 0) {
|
|
442
|
+
result.browserScripts.timings.largestContentfulPaint = {
|
|
443
|
+
renderTime: Math.max(
|
|
444
|
+
0,
|
|
445
|
+
softNav.largestInteractionContentfulPaint - navStart
|
|
446
|
+
)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (softNav.cumulativeLayoutShift !== undefined) {
|
|
451
|
+
result.browserScripts.pageinfo = result.browserScripts.pageinfo || {};
|
|
452
|
+
result.browserScripts.pageinfo.cumulativeLayoutShift =
|
|
453
|
+
softNav.cumulativeLayoutShift;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (softNav.interactionToNextPaint) {
|
|
457
|
+
result.browserScripts.timings.interactionToNextPaint =
|
|
458
|
+
softNav.interactionToNextPaint;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
423
462
|
// Google Web Vitals hacksery
|
|
424
463
|
result.googleWebVitals = {};
|
|
425
464
|
if (result.browserScripts.pageinfo) {
|
|
@@ -19,6 +19,8 @@ const CHROME_AMD_EDGE_INTERNAL_PHONE_HOME = [
|
|
|
19
19
|
'MAP optimizationguide-pa.googleapis.com 127.0.0.1'
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
+
const CHROME_FEATURES_THAT_WE_ENABLE = ['SoftNavigationHeuristics'];
|
|
23
|
+
|
|
22
24
|
const CHROME_FEATURES_THAT_WE_DISABLES = [
|
|
23
25
|
'AutofillServerCommunication',
|
|
24
26
|
'CalculateNativeWinOcclusion',
|
|
@@ -43,10 +45,36 @@ export function setupChromiumOptions(
|
|
|
43
45
|
options,
|
|
44
46
|
baseDir
|
|
45
47
|
) {
|
|
48
|
+
// Record every argument / preference / capability we apply to the browser
|
|
49
|
+
// so it can be surfaced in the result JSON for debugging — issue #1622.
|
|
50
|
+
// Stored on `options` because both Chrome and Edge call this and both end
|
|
51
|
+
// up needing it later in the engine for `result.info.browser`.
|
|
52
|
+
const recorded = {
|
|
53
|
+
args: [],
|
|
54
|
+
preferences: {},
|
|
55
|
+
mobileEmulation: undefined,
|
|
56
|
+
binaryPath: undefined,
|
|
57
|
+
extensions: 0
|
|
58
|
+
};
|
|
59
|
+
options.recordedBrowserSettings = recorded;
|
|
60
|
+
|
|
61
|
+
const addArgs = arg => {
|
|
62
|
+
seleniumOptions.addArguments(arg);
|
|
63
|
+
if (Array.isArray(arg)) {
|
|
64
|
+
recorded.args.push(...arg);
|
|
65
|
+
} else {
|
|
66
|
+
recorded.args.push(arg);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const setPrefs = prefs => {
|
|
70
|
+
seleniumOptions.setUserPreferences(prefs);
|
|
71
|
+
Object.assign(recorded.preferences, prefs);
|
|
72
|
+
};
|
|
73
|
+
|
|
46
74
|
// Fixing save password popup, only on Desktop
|
|
47
75
|
|
|
48
76
|
if (!isAndroidConfigured(options)) {
|
|
49
|
-
|
|
77
|
+
setPrefs({
|
|
50
78
|
'profile.password_manager_enable': false,
|
|
51
79
|
'profile.default_content_setting_values.notifications': 2,
|
|
52
80
|
credentials_enable_service: false
|
|
@@ -54,28 +82,50 @@ export function setupChromiumOptions(
|
|
|
54
82
|
}
|
|
55
83
|
|
|
56
84
|
if (options.headless) {
|
|
57
|
-
|
|
85
|
+
addArgs('--headless=new');
|
|
58
86
|
}
|
|
59
87
|
|
|
60
88
|
// If we run in Docker we need to always use no-sandbox
|
|
61
89
|
if (options.docker) {
|
|
62
|
-
|
|
63
|
-
|
|
90
|
+
addArgs('--no-sandbox');
|
|
91
|
+
addArgs('--disable-setuid-sandbox');
|
|
64
92
|
}
|
|
65
93
|
|
|
66
94
|
if (options.xvfb && (options.xvfb === true || options.xvfb === 'true')) {
|
|
67
|
-
|
|
95
|
+
addArgs('--disable-gpu');
|
|
68
96
|
}
|
|
69
97
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
98
|
+
addArgs('--disable-features=' + CHROME_FEATURES_THAT_WE_DISABLES.join(','));
|
|
99
|
+
|
|
100
|
+
addArgs('--enable-features=' + CHROME_FEATURES_THAT_WE_ENABLE.join(','));
|
|
73
101
|
|
|
74
102
|
const viewPort = getViewPort(options);
|
|
75
103
|
// If viewport is defined (only on desktop) then set start args
|
|
76
104
|
if (viewPort) {
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
addArgs('--window-position=0,0');
|
|
106
|
+
addArgs('--window-size=' + viewPort.replace('x', ','));
|
|
107
|
+
|
|
108
|
+
// Desktop Chrome enforces a minimum window width (~500px) regardless of
|
|
109
|
+
// --window-size, so a request like --viewPort 360x640 silently becomes
|
|
110
|
+
// 500x640. The video and filmstrips are still cropped to the requested
|
|
111
|
+
// size, which is why they look chopped. Mobile emulation is the supported
|
|
112
|
+
// way to test smaller viewports — it tells the renderer to lay out at
|
|
113
|
+
// the requested size while the OS window can stay desktop-sized.
|
|
114
|
+
// See https://github.com/sitespeedio/browsertime/issues/1222
|
|
115
|
+
const usingEmulation =
|
|
116
|
+
browserOptions.mobileEmulation &&
|
|
117
|
+
(browserOptions.mobileEmulation.deviceName ||
|
|
118
|
+
(browserOptions.mobileEmulation.width &&
|
|
119
|
+
browserOptions.mobileEmulation.height));
|
|
120
|
+
if (!usingEmulation) {
|
|
121
|
+
const [w] = viewPort.split('x').map(Number);
|
|
122
|
+
if (w && w < 500) {
|
|
123
|
+
log.warn(
|
|
124
|
+
'Requested viewport %s — desktop Chrome enforces a minimum window width of about 500px and will silently use a wider window. The video, filmstrip and screenshots will be cropped because they are sized to the requested viewport. To test smaller viewports, use Chrome mobile emulation: --chrome.mobileEmulation.deviceName "iPhone X" (or set --chrome.mobileEmulation.width and --chrome.mobileEmulation.height).',
|
|
125
|
+
viewPort
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
79
129
|
}
|
|
80
130
|
|
|
81
131
|
// If we are recording responses and we also block on domain
|
|
@@ -92,10 +142,10 @@ export function setupChromiumOptions(
|
|
|
92
142
|
}
|
|
93
143
|
excludes += ' EXCLUDE localhost';
|
|
94
144
|
|
|
95
|
-
|
|
145
|
+
addArgs('--host-resolver-rules=' + excludes);
|
|
96
146
|
} // If we are replaying with WebPageReplay
|
|
97
147
|
else if (browserOptions.webPageReplayHostResolver) {
|
|
98
|
-
|
|
148
|
+
addArgs(
|
|
99
149
|
`--host-resolver-rules= "MAP *:80 127.0.0.1:${browserOptions.webPageReplayHTTPPort}, MAP *:443 127.0.0.1:${browserOptions.webPageReplayHTTPSPort}, EXCLUDE localhost"`
|
|
100
150
|
);
|
|
101
151
|
} // If we do not use WebPageReplay but wanna block on domain
|
|
@@ -105,7 +155,7 @@ export function setupChromiumOptions(
|
|
|
105
155
|
for (let domain of excludesDomains) {
|
|
106
156
|
excludes += 'MAP * 127.0.0.1, EXCLUDE ' + domain + ',';
|
|
107
157
|
}
|
|
108
|
-
|
|
158
|
+
addArgs('--host-resolver-rules=' + excludes);
|
|
109
159
|
} else {
|
|
110
160
|
// Make sure we only set this if we do not have any other host resolver rules
|
|
111
161
|
const chromeCommandLineArguments = toArray(browserOptions.args);
|
|
@@ -113,7 +163,7 @@ export function setupChromiumOptions(
|
|
|
113
163
|
argument => argument.includes('host-resolver-rules')
|
|
114
164
|
);
|
|
115
165
|
if (argumentsWithHostResolverRules.length === 0) {
|
|
116
|
-
|
|
166
|
+
addArgs(
|
|
117
167
|
`--host-resolver-rules="${CHROME_AMD_EDGE_INTERNAL_PHONE_HOME.join(
|
|
118
168
|
','
|
|
119
169
|
)}"`
|
|
@@ -129,51 +179,48 @@ export function setupChromiumOptions(
|
|
|
129
179
|
seleniumOptions.addExtensions(
|
|
130
180
|
readFileSync(extension, { encoding: 'base64' })
|
|
131
181
|
);
|
|
182
|
+
recorded.extensions += 1;
|
|
132
183
|
}
|
|
133
184
|
}
|
|
134
185
|
|
|
135
186
|
if (options.debug) {
|
|
136
|
-
|
|
187
|
+
addArgs('--auto-open-devtools-for-tabs');
|
|
137
188
|
}
|
|
138
189
|
|
|
139
190
|
const perfLogConfig = { enableNetwork: true, enablePage: true };
|
|
140
191
|
seleniumOptions.setPerfLoggingPrefs(perfLogConfig);
|
|
141
192
|
|
|
142
193
|
if (options.userAgent) {
|
|
143
|
-
|
|
194
|
+
addArgs('--user-agent=' + options.userAgent);
|
|
144
195
|
}
|
|
145
196
|
|
|
146
197
|
if (browserOptions.ignoreCertificateErrors) {
|
|
147
|
-
|
|
198
|
+
addArgs('--ignore-certificate-errors');
|
|
148
199
|
}
|
|
149
200
|
|
|
150
201
|
if (browserOptions.collectNetLog) {
|
|
151
202
|
const dir = isAndroidConfigured(browserOptions)
|
|
152
203
|
? '/data/local/tmp'
|
|
153
204
|
: baseDir;
|
|
154
|
-
|
|
205
|
+
addArgs(`--log-net-log=${dir}/chromeNetlog.json`);
|
|
155
206
|
const level = browserOptions.netLogCaptureMode || 'IncludeSensitive';
|
|
156
|
-
|
|
207
|
+
addArgs(`--net-log-capture-mode=${level}`);
|
|
157
208
|
}
|
|
158
209
|
|
|
159
210
|
if (browserOptions.android) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
'--remote-debugging-port=' + options.devToolsPort
|
|
163
|
-
);
|
|
211
|
+
addArgs(defaultAndroidChromeOptions);
|
|
212
|
+
addArgs('--remote-debugging-port=' + options.devToolsPort);
|
|
164
213
|
} else {
|
|
165
214
|
if (browserOptions.noDefaultOptions) {
|
|
166
215
|
log.info('Skip setting default options for Chrome');
|
|
167
216
|
} else {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
'--remote-debugging-port=' + options.devToolsPort
|
|
171
|
-
);
|
|
217
|
+
addArgs(defaultChromeOptions);
|
|
218
|
+
addArgs('--remote-debugging-port=' + options.devToolsPort);
|
|
172
219
|
}
|
|
173
220
|
}
|
|
174
221
|
|
|
175
222
|
if (browserOptions.enableVideoAutoplay) {
|
|
176
|
-
|
|
223
|
+
addArgs('--autoplay-policy=no-user-gesture-required');
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
// It's a new splash screen introduced in Chrome 98
|
|
@@ -186,23 +233,31 @@ export function setupChromiumOptions(
|
|
|
186
233
|
argument.includes('disable-features') &&
|
|
187
234
|
!argument.includes('ChromeWhatsNewUI')
|
|
188
235
|
) {
|
|
189
|
-
|
|
236
|
+
addArgs(`${argument},ChromeWhatsNewUI`);
|
|
190
237
|
log.debug('Set Chrome args %j', `${argument},ChromeWhatsNewUI`);
|
|
238
|
+
} else if (
|
|
239
|
+
argument.includes('enable-features') &&
|
|
240
|
+
!argument.includes('SoftNavigationHeuristics')
|
|
241
|
+
) {
|
|
242
|
+
addArgs(`${argument},SoftNavigationHeuristics`);
|
|
243
|
+
log.debug('Set Chrome args %j', `${argument},SoftNavigationHeuristics`);
|
|
191
244
|
} else {
|
|
192
|
-
|
|
245
|
+
addArgs(argument);
|
|
193
246
|
log.debug('Set Chrome args %j', argument);
|
|
194
247
|
}
|
|
195
248
|
}
|
|
196
249
|
} else {
|
|
197
|
-
|
|
250
|
+
addArgs('--disable-features=ChromeWhatsNewUI');
|
|
198
251
|
}
|
|
199
252
|
|
|
200
253
|
if (browserOptions.binaryPath) {
|
|
201
254
|
seleniumOptions.setChromeBinaryPath(browserOptions.binaryPath);
|
|
255
|
+
recorded.binaryPath = browserOptions.binaryPath;
|
|
202
256
|
}
|
|
203
257
|
|
|
204
258
|
if (browserOptions.mobileEmulation) {
|
|
205
259
|
seleniumOptions.setMobileEmulation(browserOptions.mobileEmulation);
|
|
260
|
+
recorded.mobileEmulation = browserOptions.mobileEmulation;
|
|
206
261
|
}
|
|
207
262
|
|
|
208
263
|
// See https://bugs.chromium.org/p/chromium/issues/detail?id=818483
|