browsertime 27.2.0 → 27.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/browsertime.js +31 -5
  3. package/bin/browsertimeWebPageReplay.js +1 -1
  4. package/lib/chrome/coverage.js +198 -0
  5. package/lib/chrome/webdriver/chromium.js +14 -0
  6. package/lib/core/engine/collector.js +8 -0
  7. package/lib/core/engine/command/measure.js +4 -5
  8. package/lib/core/engine/commands.js +14 -26
  9. package/lib/core/engine/context.js +4 -9
  10. package/lib/core/engine/index.js +6 -2
  11. package/lib/core/seleniumRunner.js +1 -2
  12. package/lib/safari/safariInspectorClient.js +0 -1
  13. package/lib/screenshot/index.js +1 -1
  14. package/lib/support/browserScript.js +2 -4
  15. package/lib/support/cli.js +56 -21
  16. package/lib/support/har/index.js +6 -3
  17. package/lib/support/helpTopics.js +174 -0
  18. package/lib/support/util.js +46 -0
  19. package/lib/video/postprocessing/finetune/addTextToVideo.js +1 -1
  20. package/package.json +16 -15
  21. package/scripting.d.ts +21 -0
  22. package/types/android/index.d.ts +2 -2
  23. package/types/android/index.d.ts.map +1 -1
  24. package/types/core/engine/command/measure.d.ts +6 -6
  25. package/types/core/engine/command/measure.d.ts.map +1 -1
  26. package/types/core/engine/command/simpleperf.d.ts +1 -1
  27. package/types/core/engine/command/simpleperf.d.ts.map +1 -1
  28. package/types/core/engine/commands.d.ts +51 -48
  29. package/types/core/engine/commands.d.ts.map +1 -1
  30. package/types/core/engine/context.d.ts +9 -8
  31. package/types/core/engine/context.d.ts.map +1 -1
  32. package/types/support/tcpdump.d.ts +1 -1
  33. package/types/support/tcpdump.d.ts.map +1 -1
  34. package/types/support/util.d.ts +5 -0
  35. package/types/support/util.d.ts.map +1 -1
  36. package/types/video/postprocessing/finetune/addTextToVideo.d.ts +1 -1
  37. package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
  38. package/types/video/postprocessing/finetune/convertFps.d.ts +1 -1
  39. package/types/video/postprocessing/finetune/convertFps.d.ts.map +1 -1
  40. package/types/video/postprocessing/finetune/removeOrange.d.ts +1 -1
  41. package/types/video/postprocessing/finetune/removeOrange.d.ts.map +1 -1
  42. package/types/video/postprocessing/visualmetrics/visualMetrics.d.ts +1 -1
  43. package/types/video/postprocessing/visualmetrics/visualMetrics.d.ts.map +1 -1
  44. package/types/video/screenRecording/desktop/convert.d.ts +1 -1
  45. package/types/video/screenRecording/desktop/convert.d.ts.map +1 -1
  46. package/types/video/screenRecording/desktop/desktopRecorder.d.ts +2 -2
  47. package/types/video/screenRecording/desktop/ffmpegRecorder.d.ts +1 -1
  48. package/types/video/screenRecording/desktop/osx/getSPDisplaysDataType.d.ts +1 -1
  49. package/types/video/screenRecording/desktop/osx/getSPDisplaysDataType.d.ts.map +1 -1
  50. package/types/video/screenRecording/desktop/osx/getScreen.d.ts +1 -1
  51. package/types/video/screenRecording/desktop/osx/getScreen.d.ts.map +1 -1
  52. package/types/video/screenRecording/iosSimulator/convertToMp4.d.ts +1 -1
  53. package/types/video/screenRecording/iosSimulator/convertToMp4.d.ts.map +1 -1
  54. package/types/video/screenRecording/iosSimulator/recorder.d.ts +2 -6
  55. package/types/video/screenRecording/iosSimulator/recorder.d.ts.map +1 -1
  56. package/types/scripting.d.ts +0 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Browsertime changelog (we do [semantic versioning](https://semver.org))
2
2
 
3
+ ## 27.4.0 - 2026-05-24
4
+
5
+ ### Added
6
+ * JS/CSS coverage collection for Chrome. `--chrome.coverage` turns it on for every iteration for users who want the data and accept that detailed V8 coverage deoptimizes scripts and will skew their timing metrics. `--enableProfileRun` also turns coverage on for Chrome alongside the existing trace, so users who want coverage without affecting their timings can lean on the same extra-iteration pattern they already use for tracing — the result is merged back into the main `browsertime.json` so consumers like sitespeed.io see the data in the same place they read every other metric. Each entry has a per-file breakdown (`url`, `totalBytes`, `usedBytes`, `unusedBytes`, `unusedPercent`) [#2480](https://github.com/sitespeedio/browsertime/pull/2480).
7
+
8
+ ### Fixed
9
+ * HAR post-processing no longer throws when a page has no matching entries. Occasionally a HAR contains a page whose `pageref` has no entries — typically a navigation that didn't produce any requests, or an extension-injected page. `getDocumentRequests` and `getFinalURL` dereferenced `shift()` / `pop()` without a guard, and `getMainDocumentTimings` wrapped its entire per-page loop in a single try/catch so one bad page wiped out the main-document timings for every other page in the same HAR. Downstream this surfaced as a noisy triplet of warnings followed by an empty coach-data payload in sitespeed.io. The three crash points are now guarded and unresolvable pages are skipped [#2483](https://github.com/sitespeedio/browsertime/pull/2483).
10
+ * Bumped `geckodriver` to a version with native ARM-64 support [#2482](https://github.com/sitespeedio/browsertime/pull/2482).
11
+
12
+ ## 27.3.0 - 2026-05-20
13
+
14
+ ### Added
15
+ * `browsertime --help` is now navigable by topic. The default view shows a short curated list of common options plus the available topic names. `browsertime --help <topic>` shows just that topic, and `browsertime --help-all` reproduces the historical full dump for scripts and power users. The old single-screen dump of every option from all 13 topic groups was overwhelming for new users and slow to skim; the topic-filtered view follows the curl/ffmpeg pattern [#2467](https://github.com/sitespeedio/browsertime/pull/2467).
16
+
17
+ ### Fixed
18
+ * TypeScript types and IntelliSense for navigation scripts are tightened across the script-facing surface. A new `BrowsertimeScript` type alias lets scripts annotate their default export and get both `context` and `commands` typed in one line. The high-level command helpers (`navigate`, `find`, `getText`, `fill`, `hover`, `press`, `getTitle`, `waitForUrl`, …) now surface their JSDoc parameter and return types in IntelliSense instead of being typed as `Function`, and `commands.find` resolves to a real `WebElement` so chained calls get completion from selenium-webdriver [#2476](https://github.com/sitespeedio/browsertime/pull/2476). `context.options`, `context.result`, and `context.taskData` are typed as `Record<string, any>` so editors offer completion on `context.options.` etc. [#2478](https://github.com/sitespeedio/browsertime/pull/2478). `commands.mouse` is now a proper shape (selector typed as `string`, `singleClick`'s options surface `waitForNavigation`) instead of an anonymous object literal, and `measure.stop` / `measure.clickAndMeasure` / `measure.stopAsError` return types are explicit (`Promise<void>` / `Promise<unknown>`) instead of `Promise<any>` [#2479](https://github.com/sitespeedio/browsertime/pull/2479).
19
+ * How Browsertime publishes its TypeScript types is now safer. `scripting.d.ts` moved to the repo root next to `package.json` so it can't be clobbered by a future `tsc` outDir change, the `types` condition comes first under `exports` to match TypeScript's documented resolution rules, and `publint` runs as part of `prepublishOnly` so common package.json mistakes fail before a release goes out [#2477](https://github.com/sitespeedio/browsertime/pull/2477).
20
+ * Replaced the direct `lodash.merge` dependency with a small in-tree `merge()` helper in `lib/support/util.js`. The helper matches lodash's actual semantics for the cases Browsertime relies on — arrays merged by index, `undefined` source values skipped unless the key is missing on the target, `Date`/`RegExp`/class instances passed by reference, prototype-pollution keys rejected — which finishes the lodash unbundling the same way sitespeed.io did [#2468](https://github.com/sitespeedio/browsertime/pull/2468).
21
+ * Bumped dependencies: `chrome-har` 1.3.1 [#2469](https://github.com/sitespeedio/browsertime/pull/2469), `chrome-remote-interface` and `selenium-webdriver` [#2475](https://github.com/sitespeedio/browsertime/pull/2475), `@sitespeed.io/log` 2.0 [#2471](https://github.com/sitespeedio/browsertime/pull/2471), `chromedriver` / `edgedriver` / `geckodriver` [#2472](https://github.com/sitespeedio/browsertime/pull/2472).
22
+
3
23
  ## 27.2.0 - 2026-05-12
4
24
 
5
25
  ### Added
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import merge from 'lodash.merge';
3
2
  import { getLogger } from '@sitespeed.io/log';
4
3
  import { existsSync, mkdirSync } from 'node:fs';
5
4
  import path from 'node:path';
@@ -13,6 +12,7 @@ import { configure } from '../lib/support/logging.js';
13
12
  import { parseCommandLine } from '../lib/support/cli.js';
14
13
  import { StorageManager } from '../lib/support/storageManager.js';
15
14
  import { loadScript } from '../lib/support/engineUtils.js';
15
+ import { merge } from '../lib/support/util.js';
16
16
  import { setProperty, getProperty } from '../lib/support/util.js';
17
17
  import { isAndroidConfigured } from '../lib/android/index.js';
18
18
 
@@ -95,12 +95,16 @@ async function run(urls, options) {
95
95
  scriptsByCategory = merge(scriptsByCategory, userScripts);
96
96
  }
97
97
 
98
+ let result;
99
+ let storageManager;
100
+ let jsonName;
101
+
98
102
  try {
99
103
  if (options.preWarmServer) {
100
104
  await preWarmServer(urls, options);
101
105
  }
102
106
  await engine.start();
103
- const result = await engine.runMultiple(urls, scriptsByCategory);
107
+ result = await engine.runMultiple(urls, scriptsByCategory);
104
108
  let saveOperations = [];
105
109
 
106
110
  // TODO setup by name
@@ -109,9 +113,9 @@ async function run(urls, options) {
109
113
  if (Array.isArray(firstUrl)) {
110
114
  firstUrl = firstUrl[0];
111
115
  }
112
- const storageManager = new StorageManager(firstUrl, options);
116
+ storageManager = new StorageManager(firstUrl, options);
113
117
  const harName = options.har ?? 'browsertime';
114
- const jsonName = options.output ?? 'browsertime';
118
+ jsonName = options.output ?? 'browsertime';
115
119
 
116
120
  saveOperations.push(storageManager.writeJson(jsonName + '.json', result));
117
121
 
@@ -169,6 +173,7 @@ async function run(urls, options) {
169
173
  options.chrome.traceCategory = [
170
174
  'disabled-by-default-v8.cpu_profiler'
171
175
  ];
176
+ options.chrome.coverage = true;
172
177
  }
173
178
  }
174
179
  if (options.enableVideoRun) {
@@ -186,9 +191,30 @@ async function run(urls, options) {
186
191
  }
187
192
  const traceEngine = new Engine(options);
188
193
  await traceEngine.start();
189
- await traceEngine.runMultiple(urls, scriptsByCategory);
194
+ const profileResult = await traceEngine.runMultiple(
195
+ urls,
196
+ scriptsByCategory
197
+ );
190
198
  await traceEngine.stop();
191
199
  log.info('Extra run finished');
200
+
201
+ // Fill the main JSON's coverage from the profile run only when
202
+ // the main iterations didn't collect any (--enableProfileRun
203
+ // without --chrome.coverage). Otherwise the main run's
204
+ // per-iteration samples are what the user asked for.
205
+ let mergedCoverage = false;
206
+ for (const [i, pr] of profileResult.entries()) {
207
+ const main = result[i];
208
+ if (!main || !pr.coverage) continue;
209
+ if (main.coverage.js.length > 0 || main.coverage.css.length > 0) {
210
+ continue;
211
+ }
212
+ main.coverage = pr.coverage;
213
+ mergedCoverage = true;
214
+ }
215
+ if (mergedCoverage) {
216
+ await storageManager.writeJson(jsonName + '.json', result);
217
+ }
192
218
  }
193
219
  } catch (error) {
194
220
  log.error('Error running browsertime', error);
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import merge from 'lodash.merge';
3
2
  import yargs from 'yargs';
4
3
  import { hideBin } from 'yargs/helpers';
5
4
 
6
5
  import { Engine } from '../lib/core/engine/index.js';
7
6
  import { configure as logging } from '../lib/support/logging.js';
7
+ import { merge } from '../lib/support/util.js';
8
8
 
9
9
  async function runBrowsertime() {
10
10
  let yargsInstance = yargs(hideBin(process.argv));
@@ -0,0 +1,198 @@
1
+ import { getLogger } from '@sitespeed.io/log';
2
+
3
+ const log = getLogger('browsertime.chrome.coverage');
4
+
5
+ // Sum the union length of half-open [start, end) ranges.
6
+ function unionLength(ranges) {
7
+ if (ranges.length === 0) return 0;
8
+ const sorted = ranges
9
+ .map(r => [r.startOffset, r.endOffset])
10
+ .toSorted((a, b) => a[0] - b[0]);
11
+ let total = 0;
12
+ let [curStart, curEnd] = sorted[0];
13
+ for (let i = 1; i < sorted.length; i++) {
14
+ const [s, e] = sorted[i];
15
+ if (s <= curEnd) {
16
+ if (e > curEnd) curEnd = e;
17
+ } else {
18
+ total += curEnd - curStart;
19
+ curStart = s;
20
+ curEnd = e;
21
+ }
22
+ }
23
+ total += curEnd - curStart;
24
+ return total;
25
+ }
26
+
27
+ function summarize(files) {
28
+ let totalBytes = 0;
29
+ let usedBytes = 0;
30
+ for (const file of files) {
31
+ totalBytes += file.totalBytes;
32
+ usedBytes += file.usedBytes;
33
+ }
34
+ const unusedBytes = totalBytes - usedBytes;
35
+ return {
36
+ totalBytes,
37
+ usedBytes,
38
+ unusedBytes,
39
+ unusedPercent: totalBytes > 0 ? (unusedBytes / totalBytes) * 100 : 0
40
+ };
41
+ }
42
+
43
+ export class Coverage {
44
+ constructor(cdp) {
45
+ this.rawClient = cdp.getRawClient();
46
+ this.started = false;
47
+ this.styleSheetUrls = new Map();
48
+ this.disposeStyleSheetListener = undefined;
49
+ }
50
+
51
+ async start() {
52
+ const client = this.rawClient;
53
+ this.styleSheetUrls.clear();
54
+ this.disposeStyleSheetListener = client.CSS.styleSheetAdded(
55
+ ({ header }) => {
56
+ if (header && header.styleSheetId) {
57
+ this.styleSheetUrls.set(
58
+ header.styleSheetId,
59
+ header.sourceURL || header.sourceMapURL || ''
60
+ );
61
+ }
62
+ }
63
+ );
64
+
65
+ await client.Debugger.enable();
66
+ await client.Profiler.enable();
67
+ await client.DOM.enable();
68
+ await client.CSS.enable();
69
+ await client.Profiler.startPreciseCoverage({
70
+ callCount: false,
71
+ detailed: true,
72
+ allowTriggeredUpdates: false
73
+ });
74
+ await client.CSS.startRuleUsageTracking();
75
+ this.started = true;
76
+ }
77
+
78
+ async collect() {
79
+ if (!this.started) return;
80
+ const client = this.rawClient;
81
+
82
+ let js;
83
+ let css;
84
+ try {
85
+ const jsResult = await client.Profiler.takePreciseCoverage();
86
+ js = await this.#processJs(jsResult.result || []);
87
+ } catch (error) {
88
+ log.warn('Could not collect JS coverage: %s', error.message);
89
+ js = {
90
+ totalBytes: 0,
91
+ usedBytes: 0,
92
+ unusedBytes: 0,
93
+ unusedPercent: 0,
94
+ files: []
95
+ };
96
+ }
97
+
98
+ try {
99
+ const cssResult = await client.CSS.stopRuleUsageTracking();
100
+ css = await this.#processCss(cssResult.ruleUsage || []);
101
+ } catch (error) {
102
+ log.warn('Could not collect CSS coverage: %s', error.message);
103
+ css = {
104
+ totalBytes: 0,
105
+ usedBytes: 0,
106
+ unusedBytes: 0,
107
+ unusedPercent: 0,
108
+ files: []
109
+ };
110
+ }
111
+
112
+ try {
113
+ await client.Profiler.stopPreciseCoverage();
114
+ } catch {
115
+ // ignore — collection already returned data
116
+ }
117
+
118
+ if (this.disposeStyleSheetListener) {
119
+ this.disposeStyleSheetListener();
120
+ this.disposeStyleSheetListener = undefined;
121
+ }
122
+
123
+ this.started = false;
124
+ return { js, css };
125
+ }
126
+
127
+ async #processJs(scripts) {
128
+ const client = this.rawClient;
129
+ const files = [];
130
+ for (const script of scripts) {
131
+ if (!script.url || !/^https?:/i.test(script.url)) {
132
+ continue;
133
+ }
134
+ let source;
135
+ try {
136
+ const r = await client.Debugger.getScriptSource({
137
+ scriptId: script.scriptId
138
+ });
139
+ source = r.scriptSource || '';
140
+ } catch {
141
+ continue;
142
+ }
143
+ const totalBytes = source.length;
144
+ if (totalBytes === 0) continue;
145
+
146
+ const usedRanges = [];
147
+ for (const fn of script.functions) {
148
+ for (const r of fn.ranges) {
149
+ if (r.count > 0) usedRanges.push(r);
150
+ }
151
+ }
152
+ const usedBytes = unionLength(usedRanges);
153
+ const unusedBytes = totalBytes - usedBytes;
154
+ files.push({
155
+ url: script.url,
156
+ totalBytes,
157
+ usedBytes,
158
+ unusedBytes,
159
+ unusedPercent: (unusedBytes / totalBytes) * 100
160
+ });
161
+ }
162
+ return { ...summarize(files), files };
163
+ }
164
+
165
+ async #processCss(ruleUsage) {
166
+ const client = this.rawClient;
167
+ const byStylesheet = new Map();
168
+ for (const u of ruleUsage) {
169
+ const list = byStylesheet.get(u.styleSheetId) || [];
170
+ list.push(u);
171
+ byStylesheet.set(u.styleSheetId, list);
172
+ }
173
+ const files = [];
174
+ for (const [styleSheetId, usages] of byStylesheet) {
175
+ let text;
176
+ try {
177
+ const r = await client.CSS.getStyleSheetText({ styleSheetId });
178
+ text = r.text || '';
179
+ } catch {
180
+ continue;
181
+ }
182
+ const totalBytes = text.length;
183
+ if (totalBytes === 0) continue;
184
+
185
+ const usedBytes = unionLength(usages.filter(u => u.used));
186
+ const unusedBytes = totalBytes - usedBytes;
187
+ files.push({
188
+ url: this.styleSheetUrls.get(styleSheetId) || '',
189
+ styleSheetId,
190
+ totalBytes,
191
+ usedBytes,
192
+ unusedBytes,
193
+ unusedPercent: (unusedBytes / totalBytes) * 100
194
+ });
195
+ }
196
+ return { ...summarize(files), files };
197
+ }
198
+ }
@@ -15,6 +15,7 @@ import { parse } from '../traceCategoriesParser.js';
15
15
  import { pathToFolder } from '../../support/pathToFolder.js';
16
16
  import { loadUsbPowerProfiler } from '../../support/usbPower.js';
17
17
  import { ChromeDevtoolsProtocol } from '../chromeDevtoolsProtocol.js';
18
+ import { Coverage } from '../coverage.js';
18
19
  import { NetworkManager } from '../networkManager.js';
19
20
  import { Android, isAndroidConfigured } from '../../android/index.js';
20
21
  import { getRenderBlocking } from './traceUtilities.js';
@@ -181,6 +182,11 @@ export class Chromium {
181
182
  }
182
183
  }
183
184
 
185
+ if (this.chrome.coverage) {
186
+ this.coverage = new Coverage(this.cdpClient);
187
+ await this.coverage.start();
188
+ }
189
+
184
190
  if (
185
191
  this.collectTracingEvents &&
186
192
  !this.isTracing &&
@@ -325,6 +331,14 @@ export class Chromium {
325
331
  );
326
332
  }
327
333
 
334
+ if (this.coverage) {
335
+ const coverage = await this.coverage.collect();
336
+ this.coverage = undefined;
337
+ if (coverage) {
338
+ result.coverage = coverage;
339
+ }
340
+ }
341
+
328
342
  if (this.chrome.cdp && this.chrome.cdp.performance) {
329
343
  const rawCDPMetrics = await this.cdpClient.getPerformanceMetrics();
330
344
  const cleanedMetrics = {};
@@ -39,6 +39,7 @@ function getNewResult(url, options) {
39
39
  markedAsFailure: 0,
40
40
  failureMessages: [],
41
41
  cdp: { performance: [] },
42
+ coverage: { js: [], css: [] },
42
43
  android: { batteryTemperature: [], power: [] },
43
44
  timestamps: [],
44
45
  browserScripts: [],
@@ -185,6 +186,13 @@ export class Collector {
185
186
  });
186
187
  }
187
188
 
189
+ // Only available for Chrome with --chrome.coverage or
190
+ // --enableProfileRun.
191
+ if (data.coverage) {
192
+ results.coverage.js.push(data.coverage.js);
193
+ results.coverage.css.push(data.coverage.css);
194
+ }
195
+
188
196
  this._logIterationMetrics(data, url);
189
197
 
190
198
  if (data.visualMetrics) {
@@ -1,6 +1,5 @@
1
1
  import path from 'node:path';
2
2
  import { getLogger } from '@sitespeed.io/log';
3
- import merge from 'lodash.merge';
4
3
  import { timestamp as _timestamp } from '../../../support/engineUtils.js';
5
4
  import { pathToFolder } from '../../../support/pathToFolder.js';
6
5
  import {
@@ -9,7 +8,7 @@ import {
9
8
  } from '../../../support/userTiming.js';
10
9
  import { isAndroidConfigured, Android } from '../../../android/index.js';
11
10
  import { TCPDump } from '../../../support/tcpdump.js';
12
- import { getProperty } from '../../../support/util.js';
11
+ import { getProperty, merge } from '../../../support/util.js';
13
12
  import { MeasureScreenshots } from './measure/screenshots.js';
14
13
  import { MeasureVideo } from './measure/video.js';
15
14
  const log = getLogger('browsertime.command.measure');
@@ -329,7 +328,7 @@ export class Measure {
329
328
  *
330
329
  * @async
331
330
  * @param {string} errorMessage - The message about the error. This will end up on the HTML report for sitespeed.io so give it a good message so you know what's gone wrong.
332
- * @returns {Promise} A promise that resolves when the stop process has completed.
331
+ * @returns {Promise<void>} A promise that resolves when the stop process has completed.
333
332
  * @since 21.2.0
334
333
  */
335
334
  async stopAsError(errorMessage) {
@@ -362,7 +361,7 @@ export class Measure {
362
361
  * @async
363
362
  * @param {string} alias - The friendly name for this measurement.
364
363
  * @param {string} selector - The CSS selector of the element to click.
365
- * @returns {Promise} A promise that resolves with the collected metrics data.
364
+ * @returns {Promise<unknown>} A promise that resolves with the collected metrics data.
366
365
  * @throws {Error} Throws an error if the element is not found or the measurement fails.
367
366
  */
368
367
  async clickAndMeasure(alias, selector) {
@@ -379,7 +378,7 @@ export class Measure {
379
378
  * @async
380
379
  * @param {string} testedStartUrl - The URL that was initially tested. If not provided, it will be obtained from the browser.
381
380
  * @throws {Error} Throws an error if there are issues in stopping the measurement or collecting data.
382
- * @returns {Promise} A promise that resolves with the collected metrics data.
381
+ * @returns {Promise<unknown>} A promise that resolves with the collected metrics data.
383
382
  */
384
383
  async stop(testedStartUrl) {
385
384
  log.debug('Stop measuring');
@@ -227,10 +227,10 @@ export class Commands {
227
227
  * Navigates to a specified URL and handles additional setup for a page visit.
228
228
  * @async
229
229
  * @example await commands.navigate('https://www.example.org');
230
- * @type {Function}
231
230
  * @param {string} url - The URL to navigate to.
232
231
  * @throws {Error} Throws an error if navigation or setup fails.
233
232
  * @returns {Promise<void>} A promise that resolves when the navigation and setup are
233
+ * @type {(url: string) => Promise<void>}
234
234
  */
235
235
  this.navigate = measure._navigate.bind(measure);
236
236
 
@@ -244,7 +244,7 @@ export class Commands {
244
244
  * Add a text that will be an error attached to the current page.
245
245
  * @example await commands.error('My error message');
246
246
  * @param {string} message - The error message.
247
- * @type {Function}
247
+ * @type {(message: string) => void}
248
248
  */
249
249
  this.error = measure._error.bind(measure);
250
250
 
@@ -252,7 +252,7 @@ export class Commands {
252
252
  * Mark this run as an failure. Add a message that explains the failure.
253
253
  * @example await commands.markAsFailure('My failure message');
254
254
  * @param {string} message - The message attached as a failure
255
- * @type {Function}
255
+ * @type {(message: string) => void}
256
256
  */
257
257
  this.markAsFailure = measure._failure.bind(measure);
258
258
 
@@ -357,10 +357,6 @@ export class Commands {
357
357
  */
358
358
  this.debug = new Debug(browser, options);
359
359
 
360
- /**
361
- * Interact with the page using the mouse.
362
- * @type {Object}
363
- */
364
360
  const singleClickInstance = new SingleClick(
365
361
  browser,
366
362
  pageCompleteCheck,
@@ -375,6 +371,16 @@ export class Commands {
375
371
  const mouseMoveInstance = new MouseMove(browser, options);
376
372
  const clickAndHoldInstance = new ClickAndHold(browser);
377
373
 
374
+ /**
375
+ * Interact with the page using the mouse.
376
+ * @type {{
377
+ * moveTo: (selector: string) => Promise<void>,
378
+ * singleClick: (selector: string, mouseOptions?: { waitForNavigation?: boolean }) => Promise<void>,
379
+ * doubleClick: (selector: string) => Promise<void>,
380
+ * contextClick: (selector: string) => Promise<void>,
381
+ * clickAndHold: ClickAndHold
382
+ * }}
383
+ */
378
384
  this.mouse = {
379
385
  moveTo: async selector => mouseMoveInstance.run(selector),
380
386
  singleClick: async (selector, mouseOptions) =>
@@ -485,7 +491,6 @@ export class Commands {
485
491
  * @param {string} text - The text to type into the element.
486
492
  * @returns {Promise<void>} A promise that resolves when the text has been added.
487
493
  * @throws {Error} Throws an error if the element is not found.
488
- * @type {Function}
489
494
  */
490
495
  this.type = async (selector, text) => {
491
496
  return this.addText(selector, text);
@@ -502,7 +507,7 @@ export class Commands {
502
507
  * @param {boolean} [options.visible=false] - If true, waits for the element to be visible.
503
508
  * @returns {Promise<WebElement>} A promise that resolves to the WebElement found.
504
509
  * @throws {Error} Throws an error if the element is not found.
505
- * @type {Function}
510
+ * @type {(selector: string, options?: { timeout?: number, visible?: boolean }) => Promise<import('selenium-webdriver').WebElement>}
506
511
  */
507
512
  this.find = this.element.find.bind(this.element);
508
513
 
@@ -518,7 +523,6 @@ export class Commands {
518
523
  * @param {Object} [existsOptions] - Options.
519
524
  * @param {number} [existsOptions.timeout=0] - Maximum time to wait for the element. Default 0 (no wait).
520
525
  * @returns {Promise<boolean>} True if the element exists.
521
- * @type {Function}
522
526
  */
523
527
  this.exists = async (selector, existsOptions = {}) => {
524
528
  const { locator } = parseSelector(selector);
@@ -552,7 +556,6 @@ export class Commands {
552
556
  * @param {string} selector - The CSS selector or prefixed selector.
553
557
  * @returns {Promise<string>} The visible text content.
554
558
  * @throws {Error} Throws an error if the element is not found.
555
- * @type {Function}
556
559
  */
557
560
  this.getText = async selector => {
558
561
  const element = await findElement(selector);
@@ -567,7 +570,6 @@ export class Commands {
567
570
  * @param {string} selector - The CSS selector or prefixed selector.
568
571
  * @returns {Promise<string>} The element's value.
569
572
  * @throws {Error} Throws an error if the element is not found.
570
- * @type {Function}
571
573
  */
572
574
  this.getValue = async selector => {
573
575
  const element = await findElement(selector);
@@ -581,7 +583,6 @@ export class Commands {
581
583
  * @param {string} selector - The CSS selector or prefixed selector.
582
584
  * @returns {Promise<boolean>} True if the element is visible.
583
585
  * @throws {Error} Throws an error if the element is not found.
584
- * @type {Function}
585
586
  */
586
587
  this.isVisible = async selector => {
587
588
  const element = await findElement(selector);
@@ -597,7 +598,6 @@ export class Commands {
597
598
  * @param {string} attribute - The attribute name.
598
599
  * @returns {Promise<string>} The attribute value.
599
600
  * @throws {Error} Throws an error if the element is not found.
600
- * @type {Function}
601
601
  */
602
602
  this.getAttribute = async (selector, attribute) => {
603
603
  const element = await findElement(selector);
@@ -611,7 +611,6 @@ export class Commands {
611
611
  * @param {string} selector - The CSS selector or prefixed selector.
612
612
  * @returns {Promise<boolean>} True if the element is enabled.
613
613
  * @throws {Error} Throws an error if the element is not found.
614
- * @type {Function}
615
614
  */
616
615
  this.isEnabled = async selector => {
617
616
  const element = await findElement(selector);
@@ -625,7 +624,6 @@ export class Commands {
625
624
  * @param {string} selector - The CSS selector or prefixed selector.
626
625
  * @returns {Promise<boolean>} True if the element is checked/selected.
627
626
  * @throws {Error} Throws an error if the element is not found.
628
- * @type {Function}
629
627
  */
630
628
  this.isChecked = async selector => {
631
629
  const element = await findElement(selector);
@@ -640,7 +638,6 @@ export class Commands {
640
638
  * @param {string} selector - The CSS selector or prefixed selector.
641
639
  * @returns {Promise<void>} A promise that resolves when the element is cleared.
642
640
  * @throws {Error} Throws an error if the element is not found.
643
- * @type {Function}
644
641
  */
645
642
  this.clear = async selector => {
646
643
  const { locator } = parseSelector(selector);
@@ -665,7 +662,6 @@ export class Commands {
665
662
  * @param {Object<string, string>} fields - An object mapping selectors to values.
666
663
  * @returns {Promise<void>} A promise that resolves when all fields are filled.
667
664
  * @throws {Error} Throws an error if any element is not found.
668
- * @type {Function}
669
665
  */
670
666
  this.fill = async fields => {
671
667
  for (const [selector, text] of Object.entries(fields)) {
@@ -682,7 +678,6 @@ export class Commands {
682
678
  * @param {string} selector - The CSS selector or prefixed selector.
683
679
  * @returns {Promise<void>}
684
680
  * @throws {Error} Throws an error if the element is not found.
685
- * @type {Function}
686
681
  */
687
682
  this.hover = async selector => {
688
683
  return this.mouse.moveTo(selector);
@@ -697,7 +692,6 @@ export class Commands {
697
692
  * @example await commands.press('Escape');
698
693
  * @param {string} key - The key name to press (e.g. 'Enter', 'Tab', 'Escape').
699
694
  * @returns {Promise<void>}
700
- * @type {Function}
701
695
  */
702
696
  this.press = async key => {
703
697
  const keyValue = webdriver.Key[key.toUpperCase()] || key;
@@ -711,7 +705,6 @@ export class Commands {
711
705
  * @async
712
706
  * @example const title = await commands.getTitle();
713
707
  * @returns {Promise<string>} The page title.
714
- * @type {Function}
715
708
  */
716
709
  this.getTitle = async () => {
717
710
  return browser.getDriver().getTitle();
@@ -722,7 +715,6 @@ export class Commands {
722
715
  * @async
723
716
  * @example const url = await commands.getUrl();
724
717
  * @returns {Promise<string>} The current URL.
725
- * @type {Function}
726
718
  */
727
719
  this.getUrl = async () => {
728
720
  return browser.getDriver().getCurrentUrl();
@@ -736,7 +728,6 @@ export class Commands {
736
728
  * @param {string} selector - The CSS selector or prefixed selector.
737
729
  * @returns {Promise<void>}
738
730
  * @throws {Error} Throws an error if the element is not found.
739
- * @type {Function}
740
731
  */
741
732
  this.check = async selector => {
742
733
  const element = await findElement(selector);
@@ -754,7 +745,6 @@ export class Commands {
754
745
  * @param {string} selector - The CSS selector or prefixed selector.
755
746
  * @returns {Promise<void>}
756
747
  * @throws {Error} Throws an error if the element is not found.
757
- * @type {Function}
758
748
  */
759
749
  this.uncheck = async selector => {
760
750
  const element = await findElement(selector);
@@ -772,7 +762,6 @@ export class Commands {
772
762
  * @param {string} selector - The CSS selector or prefixed selector.
773
763
  * @returns {Promise<void>}
774
764
  * @throws {Error} Throws an error if the element is not found.
775
- * @type {Function}
776
765
  */
777
766
  this.scrollIntoView = async selector => {
778
767
  const element = await findElement(selector);
@@ -795,7 +784,6 @@ export class Commands {
795
784
  * @param {number} [urlOptions.timeout=10000] - Maximum time to wait in milliseconds.
796
785
  * @returns {Promise<void>}
797
786
  * @throws {Error} Throws an error if the URL does not match within the timeout.
798
- * @type {Function}
799
787
  */
800
788
  this.waitForUrl = async (pattern, urlOptions = {}) => {
801
789
  const timeout = urlOptions.timeout ?? 10_000;
@@ -30,13 +30,13 @@ export class Context {
30
30
  ) {
31
31
  /**
32
32
  * This is the yargs object you get from the cli. If you add --my.id you can get that using options.my.id.
33
- * @type {Object}
33
+ * @type {Record<string, any>}
34
34
  */
35
35
  this.options = options;
36
36
 
37
37
  /**
38
38
  * Here the result from each run is stored.
39
- * @type {Object}
39
+ * @type {Record<string, any>}
40
40
  */
41
41
  this.result = result;
42
42
 
@@ -58,7 +58,8 @@ export class Context {
58
58
  this.storageManager = storageManager;
59
59
 
60
60
  /**
61
- * @type {Object}
61
+ * Scratch space shared between commands within a single iteration.
62
+ * @type {Record<string, any>}
62
63
  */
63
64
  this.taskData = {};
64
65
 
@@ -68,13 +69,7 @@ export class Context {
68
69
  * @type {{webdriver: WebDriverClass, driver: WebDriverInstance}}
69
70
  */
70
71
  this.selenium = {
71
- /**
72
- * @typedef {typeof import('selenium-webdriver')} WebDriverClass
73
- */
74
72
  webdriver,
75
- /**
76
- * @typedef {import('selenium-webdriver').WebDriver} WebDriverInstance
77
- */
78
73
  driver: instantiatedDriver
79
74
  };
80
75
  }