browsertime 27.0.0 → 27.0.2
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 +12 -0
- package/lib/chrome/har.js +22 -4
- package/lib/chrome/webdriver/chromium.js +1 -1
- package/lib/core/engine/command/click.js +34 -5
- package/lib/core/engine/iteration.js +12 -4
- package/package.json +1 -1
- package/types/core/engine/command/click.d.ts +3 -3
- package/types/core/engine/command/click.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Browsertime changelog (we do [semantic versioning](https://semver.org))
|
|
2
2
|
|
|
3
|
+
## 27.0.2 - 2026-05-02
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
* SPA scripts no longer drop a HAR page when Chrome's soft-navigation `PerformanceObserver` entry doesn't fire. [#2438](https://github.com/sitespeedio/browsertime/pull/2438) gated HAR-page creation on Chrome detecting all three soft-navigation criteria (interaction + URL change + visible paint within a short window), but real SPAs like Grafana sometimes paint too late and Chrome never emits the entry — chrome-har then sees only `Network.*` events for that measurement and emits zero pages, breaking downstream per-iteration page indexing in sitespeed.io. When the measurement was started without a URL (`commands.measure.start(alias)` form) and no soft-nav entry was detected, fall back to a synthetic page anchored at the current `location.href` so each `measure.start` produces exactly one HAR page.
|
|
7
|
+
|
|
8
|
+
## 27.0.1 - 2026-05-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
* `commands.click` now falls back to a JavaScript click when the Actions API can't reach the element. Hidden elements have a zero-size bounding box, so the Actions API click that became the default in [#2381](https://github.com/sitespeedio/browsertime/pull/2381) silently moved the pointer to (0,0) and clicked nothing instead of triggering the handler. The click command now pre-checks `isDisplayed()` and uses a JS click when the element isn't displayed, and otherwise catches any Actions API failure and retries with a JS click. Each fallback is logged at info level so it's visible without `--verbose`. Restores the documented "hide everything before clicking" pattern used by visual-metric scripts [#2452](https://github.com/sitespeedio/browsertime/pull/2452).
|
|
12
|
+
* `engineDelegate.afterBrowserStopped()` is no longer called twice per iteration on the happy path. [#2432](https://github.com/sitespeedio/browsertime/pull/2432) added a safety-net cleanup call in the iteration `finally` block so failure paths still release long-lived helpers, but in the happy path `_stopBrowser` had already invoked the cleanup, so it ran twice. The visible symptom on Chrome was a spurious `ENOENT` error from the second `cleanUserDataDir` (the first call had already removed the directory). It could also double-terminate the iOS simulator's Safari and the `ios-capture` server. The safety-net now runs only when `_stopBrowser` didn't already reach the cleanup [#2453](https://github.com/sitespeedio/browsertime/pull/2453).
|
|
13
|
+
* `cleanUserDataDir` cleanup now uses `rm(..., { recursive: true, force: true })` so a missing directory is treated as already-cleaned. Defense in depth alongside [#2453](https://github.com/sitespeedio/browsertime/pull/2453): if anything in the future double-calls the Chrome cleanup, or if Chrome failed to create the directory in the first place, the cleanup stays quiet instead of logging a spurious `ENOENT` [#2454](https://github.com/sitespeedio/browsertime/pull/2454).
|
|
14
|
+
|
|
3
15
|
## 27.0.0 - 2026-05-01
|
|
4
16
|
|
|
5
17
|
### Highlights
|
package/lib/chrome/har.js
CHANGED
|
@@ -23,10 +23,17 @@ 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
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
26
|
+
// Inject a SoftNavigation.detected event so chrome-har emits/renames a
|
|
27
|
+
// HAR page for this measurement. Chrome's PerformanceObserver
|
|
28
|
+
// soft-navigation entry is the trustworthy signal (user interaction +
|
|
29
|
+
// URL change + visible paint), but it doesn't always fire — Grafana and
|
|
30
|
+
// similar SPAs can defer their post-route paint past Chrome's window —
|
|
31
|
+
// so when this measurement was started without a URL (the
|
|
32
|
+
// `commands.measure.start(alias)` form, signalling "the next page is a
|
|
33
|
+
// soft-navigation I'm about to drive") we fall back to the current
|
|
34
|
+
// location.href. Without the fallback, chrome-har silently emits zero
|
|
35
|
+
// pages for that measurement and downstream consumers (sitespeed.io's
|
|
36
|
+
// per-iteration page indexing) drift out of alignment.
|
|
30
37
|
try {
|
|
31
38
|
const softNavEntries = await runner.runScript(
|
|
32
39
|
`const supported = PerformanceObserver.supportedEntryTypes;
|
|
@@ -44,6 +51,17 @@ export async function getHar(
|
|
|
44
51
|
method: 'SoftNavigation.detected',
|
|
45
52
|
params: { url: entry.url, startTime: entry.startTime }
|
|
46
53
|
});
|
|
54
|
+
} else if (!result.url) {
|
|
55
|
+
const fallbackUrl = await runner.runScript(
|
|
56
|
+
'return window.location.href;',
|
|
57
|
+
'GET_URL_FOR_HAR_FALLBACK'
|
|
58
|
+
);
|
|
59
|
+
if (fallbackUrl) {
|
|
60
|
+
messages.push({
|
|
61
|
+
method: 'SoftNavigation.detected',
|
|
62
|
+
params: { url: fallbackUrl, startTime: 0 }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
47
65
|
}
|
|
48
66
|
} catch {
|
|
49
67
|
// Soft navigations not supported, ignore
|
|
@@ -584,7 +584,7 @@ export class Chromium {
|
|
|
584
584
|
if (arg.includes('user-data-dir')) {
|
|
585
585
|
const userDataDir = arg.split('=')[1];
|
|
586
586
|
try {
|
|
587
|
-
await rm(userDataDir, { recursive: true });
|
|
587
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
588
588
|
log.info(`Deleted user data dir: ${userDataDir}`);
|
|
589
589
|
} catch (error) {
|
|
590
590
|
log.error(`Could not delete user data dir: ${userDataDir}`, error);
|
|
@@ -6,9 +6,9 @@ const log = getLogger('browsertime.command.click');
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Provides functionality to perform click actions on elements in a web page using various selectors.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Tries the Selenium Actions API first to generate real OS-level mouse events; if the element is
|
|
10
|
+
* not interactable (commonly because the page hides content before clicking — a recommended
|
|
11
|
+
* pattern for visual-metric scripts), falls back to a JavaScript click so the script keeps working.
|
|
12
12
|
*
|
|
13
13
|
* @class
|
|
14
14
|
* @hideconstructor
|
|
@@ -55,8 +55,37 @@ export class Click {
|
|
|
55
55
|
*/
|
|
56
56
|
async _clickElement(element) {
|
|
57
57
|
const driver = this.browser.getDriver();
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// The Actions API moves the pointer to the element's bounding-box center
|
|
59
|
+
// and clicks. A `display:none` element has a zero-size box, so the click
|
|
60
|
+
// silently lands at (0,0) instead of throwing — we have to detect that
|
|
61
|
+
// up front and use JS instead. Other interactability problems (overlays,
|
|
62
|
+
// pointer-events:none, etc.) that do throw are caught below.
|
|
63
|
+
const isDisplayed = await element.isDisplayed().catch(() => false);
|
|
64
|
+
if (!isDisplayed) {
|
|
65
|
+
log.info(
|
|
66
|
+
'Element is not displayed — using a JavaScript click instead of the Actions API.'
|
|
67
|
+
);
|
|
68
|
+
return driver.executeScript('arguments[0].click();', element);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await driver.actions({ async: true }).click(element).perform();
|
|
72
|
+
return driver.actions().clear();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
log.info(
|
|
75
|
+
'Actions API click failed (%s) — falling back to a JavaScript click.',
|
|
76
|
+
error && error.name
|
|
77
|
+
);
|
|
78
|
+
try {
|
|
79
|
+
await driver.actions().clear();
|
|
80
|
+
await driver.executeScript('arguments[0].click();', element);
|
|
81
|
+
} catch (jsError) {
|
|
82
|
+
log.error(
|
|
83
|
+
'Both the Actions API and the JavaScript click failed: %s',
|
|
84
|
+
(jsError && jsError.message) || jsError
|
|
85
|
+
);
|
|
86
|
+
throw jsError;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
60
89
|
}
|
|
61
90
|
|
|
62
91
|
/**
|
|
@@ -82,6 +82,7 @@ export class Iteration {
|
|
|
82
82
|
const batteryTemperature = {};
|
|
83
83
|
|
|
84
84
|
const engineDelegate = this.engineDelegate;
|
|
85
|
+
let afterBrowserStoppedRan = false;
|
|
85
86
|
|
|
86
87
|
try {
|
|
87
88
|
await this._startBrowser(browser, engineDelegate, options);
|
|
@@ -153,6 +154,7 @@ export class Iteration {
|
|
|
153
154
|
index,
|
|
154
155
|
commands
|
|
155
156
|
);
|
|
157
|
+
afterBrowserStoppedRan = true;
|
|
156
158
|
|
|
157
159
|
if (isAndroidConfigured(options)) {
|
|
158
160
|
batteryTemperature.stop = await this.android.getTemperature();
|
|
@@ -206,10 +208,16 @@ export class Iteration {
|
|
|
206
208
|
}
|
|
207
209
|
// Ensure delegate cleanup runs even on failure paths so that
|
|
208
210
|
// long-lived helpers (e.g. ios-capture, safaridriver) are released.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
// Skip when _stopBrowser already reached afterBrowserStopped on the
|
|
212
|
+
// happy path — running the cleanup twice surfaced as ENOENT noise on
|
|
213
|
+
// the Chrome user-data dir cleanup and could double-kill processes
|
|
214
|
+
// on Safari.
|
|
215
|
+
if (!afterBrowserStoppedRan) {
|
|
216
|
+
try {
|
|
217
|
+
await engineDelegate.afterBrowserStopped();
|
|
218
|
+
} catch {
|
|
219
|
+
// best-effort — helpers may already have been released
|
|
220
|
+
}
|
|
213
221
|
}
|
|
214
222
|
}
|
|
215
223
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provides functionality to perform click actions on elements in a web page using various selectors.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Tries the Selenium Actions API first to generate real OS-level mouse events; if the element is
|
|
4
|
+
* not interactable (commonly because the page hides content before clicking — a recommended
|
|
5
|
+
* pattern for visual-metric scripts), falls back to a JavaScript click so the script keeps working.
|
|
6
6
|
*
|
|
7
7
|
* @class
|
|
8
8
|
* @hideconstructor
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../../../lib/core/engine/command/click.js"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH;IACE,gEAaC;IAZC;;OAEG;IACH,gBAAsB;IACtB;;OAEG;IACH,0BAA0C;IAC1C;;OAEG;IACH,gBAAsB;IAGxB;;OAEG;IACH,wBAKC;IAED;;OAEG;IACH,iBAMC;IAED;;OAEG;IACH,
|
|
1
|
+
{"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../../../lib/core/engine/command/click.js"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH;IACE,gEAaC;IAZC;;OAEG;IACH,gBAAsB;IACtB;;OAEG;IACH,0BAA0C;IAC1C;;OAEG;IACH,gBAAsB;IAGxB;;OAEG;IACH,wBAKC;IAED;;OAEG;IACH,iBAMC;IAED;;OAEG;IACH,sBAiCC;IAED;;;;;;;;;;;OAWG;IACH,cANW,MAAM,YAEd;QAA0B,iBAAiB,GAAnC,OAAO;KACf,GAAU,OAAO,CAAC,IAAI,CAAC,CAoBzB;IAED;;;;;;;;OAQG;IACH,oBAcC;IAED;;;;;;;;OAQG;IACH,2BAEC;IAED;;;;;;;;OAQG;IACH,mBAQC;IAED;;;;;;;;OAQG;IACH,0BAQC;IAED;;;;;;;;OAQG;IACH,0BAQC;IAED;;;;;;;;OAQG;IACH,iCAQC;IAED;;;;;;;;;OASG;IACH,eAQC;IAED;;;;;;;;;OASG;IACH,sBAQC;IAED;;;;;;;;OAQG;IACH,gBAcC;IACD;;;;;;;;OAQG;IACH,uBAEC;IAED;;;;;;;;OAQG;IACH,aAeC;IAED;;;;;;;;OAQG;IACH,oBAEC;IAED;;;;;;;;OAQG;IACH,aAcC;IAED;;;;;;;;OAQG;IACH,eAcC;IAED;;;;;;OAMG;IACH,oBAEC;IAED;;;;;;;;OAQG;IACH,mBAcC;IAED;;;;;;;;OAQG;IACH,0BAEC;CACF"}
|