browsertime 27.0.0 → 27.0.1

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Browsertime changelog (we do [semantic versioning](https://semver.org))
2
2
 
3
+ ## 27.0.1 - 2026-05-02
4
+
5
+ ### Fixed
6
+ * `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).
7
+ * `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).
8
+ * `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).
9
+
3
10
  ## 27.0.0 - 2026-05-01
4
11
 
5
12
  ### Highlights
@@ -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
- * Uses the Selenium Actions API to generate real OS-level mouse events, which means
10
- * the element must be visible and interactable. If you need to click a hidden element,
11
- * use {@link JavaScript#run commands.js.run} to trigger a JavaScript click instead.
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
- await driver.actions({ async: true }).click(element).perform();
59
- return driver.actions().clear();
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
- try {
210
- await engineDelegate.afterBrowserStopped();
211
- } catch {
212
- // Idempotent — may have already run via _stopBrowser
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,7 +1,7 @@
1
1
  {
2
2
  "name": "browsertime",
3
3
  "description": "Get performance metrics from your web page using Browsertime.",
4
- "version": "27.0.0",
4
+ "version": "27.0.1",
5
5
  "bin": "./bin/browsertime.js",
6
6
  "type": "module",
7
7
  "types": "./types/scripting.d.ts",
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Provides functionality to perform click actions on elements in a web page using various selectors.
3
- * Uses the Selenium Actions API to generate real OS-level mouse events, which means
4
- * the element must be visible and interactable. If you need to click a hidden element,
5
- * use {@link JavaScript#run commands.js.run} to trigger a JavaScript click instead.
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,sBAIC;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"}
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"}