browsertime 27.2.0 → 27.3.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 (53) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/bin/browsertime.js +1 -1
  3. package/bin/browsertimeWebPageReplay.js +1 -1
  4. package/lib/core/engine/command/measure.js +4 -5
  5. package/lib/core/engine/commands.js +14 -26
  6. package/lib/core/engine/context.js +4 -9
  7. package/lib/core/engine/index.js +6 -2
  8. package/lib/core/seleniumRunner.js +1 -2
  9. package/lib/safari/safariInspectorClient.js +0 -1
  10. package/lib/screenshot/index.js +1 -1
  11. package/lib/support/browserScript.js +2 -4
  12. package/lib/support/cli.js +41 -20
  13. package/lib/support/har/index.js +1 -2
  14. package/lib/support/helpTopics.js +174 -0
  15. package/lib/support/util.js +46 -0
  16. package/lib/video/postprocessing/finetune/addTextToVideo.js +1 -1
  17. package/package.json +16 -15
  18. package/scripting.d.ts +21 -0
  19. package/types/android/index.d.ts +2 -2
  20. package/types/android/index.d.ts.map +1 -1
  21. package/types/core/engine/command/measure.d.ts +6 -6
  22. package/types/core/engine/command/measure.d.ts.map +1 -1
  23. package/types/core/engine/command/simpleperf.d.ts +1 -1
  24. package/types/core/engine/command/simpleperf.d.ts.map +1 -1
  25. package/types/core/engine/commands.d.ts +51 -48
  26. package/types/core/engine/commands.d.ts.map +1 -1
  27. package/types/core/engine/context.d.ts +9 -8
  28. package/types/core/engine/context.d.ts.map +1 -1
  29. package/types/support/tcpdump.d.ts +1 -1
  30. package/types/support/tcpdump.d.ts.map +1 -1
  31. package/types/support/util.d.ts +5 -0
  32. package/types/support/util.d.ts.map +1 -1
  33. package/types/video/postprocessing/finetune/addTextToVideo.d.ts +1 -1
  34. package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
  35. package/types/video/postprocessing/finetune/convertFps.d.ts +1 -1
  36. package/types/video/postprocessing/finetune/convertFps.d.ts.map +1 -1
  37. package/types/video/postprocessing/finetune/removeOrange.d.ts +1 -1
  38. package/types/video/postprocessing/finetune/removeOrange.d.ts.map +1 -1
  39. package/types/video/postprocessing/visualmetrics/visualMetrics.d.ts +1 -1
  40. package/types/video/postprocessing/visualmetrics/visualMetrics.d.ts.map +1 -1
  41. package/types/video/screenRecording/desktop/convert.d.ts +1 -1
  42. package/types/video/screenRecording/desktop/convert.d.ts.map +1 -1
  43. package/types/video/screenRecording/desktop/desktopRecorder.d.ts +2 -2
  44. package/types/video/screenRecording/desktop/ffmpegRecorder.d.ts +1 -1
  45. package/types/video/screenRecording/desktop/osx/getSPDisplaysDataType.d.ts +1 -1
  46. package/types/video/screenRecording/desktop/osx/getSPDisplaysDataType.d.ts.map +1 -1
  47. package/types/video/screenRecording/desktop/osx/getScreen.d.ts +1 -1
  48. package/types/video/screenRecording/desktop/osx/getScreen.d.ts.map +1 -1
  49. package/types/video/screenRecording/iosSimulator/convertToMp4.d.ts +1 -1
  50. package/types/video/screenRecording/iosSimulator/convertToMp4.d.ts.map +1 -1
  51. package/types/video/screenRecording/iosSimulator/recorder.d.ts +2 -6
  52. package/types/video/screenRecording/iosSimulator/recorder.d.ts.map +1 -1
  53. package/types/scripting.d.ts +0 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Browsertime changelog (we do [semantic versioning](https://semver.org))
2
2
 
3
+ ## 27.3.0 - 2026-05-20
4
+
5
+ ### Added
6
+ * `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).
7
+
8
+ ### Fixed
9
+ * 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).
10
+ * 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).
11
+ * 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).
12
+ * 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).
13
+
3
14
  ## 27.2.0 - 2026-05-12
4
15
 
5
16
  ### 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
 
@@ -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));
@@ -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
  }
@@ -1,6 +1,5 @@
1
1
  import { arch as _arch } from 'node:os';
2
2
  import { getLogger } from '@sitespeed.io/log';
3
- import merge from 'lodash.merge';
4
3
  import { execaCommand as command } from 'execa';
5
4
  import { StorageManager } from '../../support/storageManager.js';
6
5
  import { Firefox } from '../../firefox/webdriver/firefox.js';
@@ -10,7 +9,12 @@ import {
10
9
  addConnectivity,
11
10
  removeConnectivity
12
11
  } from '../../connectivity/index.js';
13
- import { logResultLogLine, getProperty, toArray } from '../../support/util.js';
12
+ import {
13
+ logResultLogLine,
14
+ getProperty,
15
+ toArray,
16
+ merge
17
+ } from '../../support/util.js';
14
18
  import {
15
19
  getFullyLoaded,
16
20
  getMainDocumentTimings,
@@ -1,5 +1,4 @@
1
1
  import { getLogger } from '@sitespeed.io/log';
2
- import merge from 'lodash.merge';
3
2
  import { Condition } from 'selenium-webdriver';
4
3
  import { isAndroidConfigured } from '../android/index.js';
5
4
  import { UrlLoadError, BrowserError, TimeoutError } from '../support/errors.js';
@@ -8,7 +7,7 @@ import { getViewPort } from '../support/getViewPort.js';
8
7
  import { defaultPageCompleteCheck } from './pageCompleteChecks/defaultPageCompleteCheck.js';
9
8
  import { pageCompleteCheckByInactivity } from './pageCompleteChecks/pageCompleteCheckByInactivity.js';
10
9
  import { spaInactivity as spaCheck } from './pageCompleteChecks/spaInactivity.js';
11
- import { getProperty } from '../support/util.js';
10
+ import { getProperty, merge } from '../support/util.js';
12
11
  const log = getLogger('browsertime');
13
12
 
14
13
  const defaults = {
@@ -1,4 +1,3 @@
1
- /* global WebSocket */
2
1
  import { execa } from 'execa';
3
2
  import { getLogger } from '@sitespeed.io/log';
4
3
 
@@ -1,5 +1,4 @@
1
1
  import path from 'node:path';
2
- import merge from 'lodash.merge';
3
2
  import { getLogger } from '@sitespeed.io/log';
4
3
  import { screenshotDefaults } from './defaults.js';
5
4
  import {
@@ -8,6 +7,7 @@ import {
8
7
  saveJpg
9
8
  } from '../support/images/index.js';
10
9
  import { loadCustomJimp } from './loadCustomJimp.js';
10
+ import { merge } from '../support/util.js';
11
11
 
12
12
  const SCREENSHOT_DIR = 'screenshots';
13
13
  const log = getLogger('browsertime.screenshot');
@@ -141,8 +141,7 @@ async function findAndParseScripts(root, category) {
141
141
  const categoryName = path.basename(dirPath);
142
142
 
143
143
  return scriptsFromDirectory(dirPath).then(scripts => {
144
- const categories = {};
145
- categories[categoryName] = scripts;
144
+ const categories = { [categoryName]: scripts };
146
145
  return categories;
147
146
  });
148
147
  }
@@ -154,8 +153,7 @@ async function findAndParseScripts(root, category) {
154
153
  let scripts = {};
155
154
 
156
155
  scripts[name] = generateScriptObject(name, root, content);
157
- let categories = {};
158
- categories[category] = scripts;
156
+ let categories = { [category]: scripts };
159
157
 
160
158
  return categories;
161
159
  })
@@ -14,6 +14,12 @@ import { geckoProfilerDefaults } from '../firefox/settings/geckoProfilerDefaults
14
14
  import { findUpSync } from './fileUtil.js';
15
15
  import { setProperty, getProperty } from './util.js';
16
16
  import { execaSync } from 'execa';
17
+ import {
18
+ classifyHelp,
19
+ captureTopics,
20
+ applyHelpFilter,
21
+ buildEpilog
22
+ } from './helpTopics.js';
17
23
 
18
24
  const configPath = findUpSync(['.browsertime.json']);
19
25
 
@@ -152,7 +158,16 @@ export function parseCommandLine() {
152
158
  arg === '--android' ? '--android.enabled' : arg
153
159
  );
154
160
 
155
- let yargsInstance = yargs(hideBin(argvFix));
161
+ // Detect --help <topic> / --help-all up front and rewrite them to a
162
+ // bare --help so yargs still triggers its normal help+exit flow.
163
+ const rawArgs = hideBin(argvFix);
164
+ const help = classifyHelp(rawArgs);
165
+
166
+ let yargsInstance = yargs(help.args);
167
+ // Wrap .option() to record each option's key under its inline
168
+ // `group: '<topic>'` field. Browsertime already annotates every option
169
+ // with a group, so this gives us a topic -> keys map for free.
170
+ const topicKeys = captureTopics(yargsInstance);
156
171
  let validated = yargsInstance
157
172
  .parserConfiguration({
158
173
  'camel-case-expansion': false,
@@ -923,76 +938,76 @@ export function parseCommandLine() {
923
938
  type: 'boolean',
924
939
  default: false,
925
940
  describe: 'Save one screenshot per iteration.',
926
- group: 'Screenshot'
941
+ group: 'screenshot'
927
942
  })
928
943
  .option('screenshotLCP', {
929
944
  type: 'boolean',
930
945
  default: false,
931
946
  describe:
932
947
  'Save one screenshot per iteration that shows the largest contentful paint element (if the browser supports LCP).',
933
- group: 'Screenshot'
948
+ group: 'screenshot'
934
949
  })
935
950
  .option('screenshotLS', {
936
951
  type: 'boolean',
937
952
  default: false,
938
953
  describe:
939
954
  'Save one screenshot per iteration that shows the layout shift elements (if the browser supports layout shift).',
940
- group: 'Screenshot'
955
+ group: 'screenshot'
941
956
  })
942
957
  .option('screenshotParams.type', {
943
958
  describe: 'Set the file type of the screenshot',
944
959
  choices: ['png', 'jpg'],
945
960
  default: screenshotDefaults.type,
946
- group: 'Screenshot'
961
+ group: 'screenshot'
947
962
  })
948
963
  .option('screenshotParams.jpg.quality', {
949
964
  describe: 'Quality of the JPEG screenshot. 1-100',
950
965
  default: screenshotDefaults.jpg.quality,
951
- group: 'Screenshot'
966
+ group: 'screenshot'
952
967
  })
953
968
  .option('screenshotParams.maxSize', {
954
969
  describe: 'The max size of the screenshot (width and height).',
955
970
  default: screenshotDefaults.maxSize,
956
- group: 'Screenshot'
971
+ group: 'screenshot'
957
972
  })
958
973
  .option('pageCompleteCheck', {
959
974
  describe:
960
975
  'Supply a JavaScript (inline or JavaScript file) that decides when the browser is finished loading the page and can start to collect metrics. The JavaScript snippet is repeatedly queried to see if page has completed loading (indicated by the script returning true). Use it to fetch timings happening after the loadEventEnd. By default the tests ends 2 seconds after loadEventEnd. Also checkout --pageCompleteCheckInactivity and --pageCompleteCheckPollTimeout',
961
- group: 'PageLoad'
976
+ group: 'pageload'
962
977
  })
963
978
  .option('pageCompleteWaitTime', {
964
979
  describe:
965
980
  'How long time you want to wait for your pageComplteteCheck to finish, after it is signaled to closed. Extra parameter passed on to your pageCompleteCheck.',
966
981
  default: 8000,
967
- group: 'PageLoad'
982
+ group: 'pageload'
968
983
  })
969
984
  .option('pageCompleteCheckInactivity', {
970
985
  describe:
971
986
  'Alternative way to choose when to end your test. This will wait for 2 seconds of inactivity that happens after loadEventEnd.',
972
987
  type: 'boolean',
973
988
  default: false,
974
- group: 'PageLoad'
989
+ group: 'pageload'
975
990
  })
976
991
  .option('pageCompleteCheckNetworkIdle', {
977
992
  describe:
978
993
  'Alternative way to choose when to end your test that works in Chrome and Firefox. Uses CDP or WebDriver Bidi to look at network traffic instead of running JavaScript in the browser to know when to end the test. By default this will wait 5 seconds of inactivity in the network log (no requets/responses in 5 seconds). Use --timeouts.networkIdle to change the 5 seconds. The test will end after 2 minutes if there is still activity on the network. You can change that timout using --timeouts.pageCompleteCheck ',
979
994
  type: 'boolean',
980
995
  default: false,
981
- group: 'PageLoad'
996
+ group: 'pageload'
982
997
  })
983
998
  .option('pageCompleteCheckPollTimeout', {
984
999
  type: 'number',
985
1000
  default: 1500,
986
1001
  describe:
987
1002
  'The time in ms to wait for running the page complete check the next time.',
988
- group: 'PageLoad'
1003
+ group: 'pageload'
989
1004
  })
990
1005
  .option('pageCompleteCheckStartWait', {
991
1006
  type: 'number',
992
1007
  default: 5000,
993
1008
  describe:
994
1009
  'The time in ms to wait for running the page complete check for the first time. Use this when you have a pageLoadStrategy set to none',
995
- group: 'PageLoad'
1010
+ group: 'pageload'
996
1011
  })
997
1012
  .option('pageLoadStrategy', {
998
1013
  type: 'string',
@@ -1000,7 +1015,7 @@ export function parseCommandLine() {
1000
1015
  choices: ['eager', 'none', 'normal'],
1001
1016
  describe:
1002
1017
  'Set the strategy to waiting for document readiness after a navigation event. After the strategy is ready, your pageCompleteCheck will start running.',
1003
- group: 'PageLoad'
1018
+ group: 'pageload'
1004
1019
  })
1005
1020
  .option('iterations', {
1006
1021
  alias: 'n',
@@ -1025,14 +1040,14 @@ export function parseCommandLine() {
1025
1040
  default: 0,
1026
1041
  describe:
1027
1042
  'Extra time added for the browser to settle before starting to test a URL. This delay happens after the browser was opened and before the navigation to the URL',
1028
- group: 'PageLoad'
1043
+ group: 'pageload'
1029
1044
  })
1030
1045
  .option('webdriverPageload', {
1031
1046
  type: 'boolean',
1032
1047
  describe:
1033
1048
  'Use webdriver.get to initialize the page load instead of window.location.',
1034
1049
  default: false,
1035
- group: 'PageLoad'
1050
+ group: 'pageload'
1036
1051
  })
1037
1052
  .option('proxy.pac', {
1038
1053
  type: 'string',
@@ -1168,7 +1183,7 @@ export function parseCommandLine() {
1168
1183
  'Use internal browser functionality to clear browser cache between runs instead of only using Selenium.',
1169
1184
  type: 'boolean',
1170
1185
  default: false,
1171
- group: 'PageLoad'
1186
+ group: 'pageload'
1172
1187
  })
1173
1188
  .option('basicAuth', {
1174
1189
  describe:
@@ -1310,7 +1325,7 @@ export function parseCommandLine() {
1310
1325
  default: false,
1311
1326
  describe:
1312
1327
  'Flush DNS between runs, works on Mac OS and Linux. Your user needs sudo rights to be able to flush the DNS.',
1313
- group: 'PageLoad'
1328
+ group: 'pageload'
1314
1329
  })
1315
1330
  .option('extension', {
1316
1331
  describe:
@@ -1321,7 +1336,7 @@ export function parseCommandLine() {
1321
1336
  'Convenient parameter to use if you test a SPA application: will automatically wait for X seconds after last network activity and use hash in file names. Read more: https://www.sitespeed.io/documentation/sitespeed.io/spa/',
1322
1337
  type: 'boolean',
1323
1338
  default: false,
1324
- group: 'PageLoad'
1339
+ group: 'pageload'
1325
1340
  })
1326
1341
  .option('cjs', {
1327
1342
  describe:
@@ -1334,7 +1349,7 @@ export function parseCommandLine() {
1334
1349
  default: 3,
1335
1350
  describe:
1336
1351
  'If the browser fails to start, you can retry to start it this amount of times.',
1337
- group: 'PageLoad'
1352
+ group: 'pageload'
1338
1353
  })
1339
1354
  .option('preWarmServer', {
1340
1355
  type: 'boolean',
@@ -1363,8 +1378,14 @@ export function parseCommandLine() {
1363
1378
  .alias('V', 'version')
1364
1379
  .alias('v', 'verbose')
1365
1380
  .wrap(yargsInstance.terminalWidth())
1381
+ .epilog(buildEpilog(help.mode, help.topic, [...topicKeys.keys()]))
1366
1382
  .check(validateInput);
1367
1383
 
1384
+ // Filter the visible options based on the requested help mode. This
1385
+ // must happen before .argv is read because that's what triggers
1386
+ // yargs to render help and exit.
1387
+ applyHelpFilter(yargsInstance, help.mode, help.topic, topicKeys);
1388
+
1368
1389
  let argv = validated.argv;
1369
1390
 
1370
1391
  if (
@@ -1,12 +1,11 @@
1
1
  import { createRequire } from 'node:module';
2
- import merge from 'lodash.merge';
3
2
  import { pathToFolder } from '../pathToFolder.js';
4
3
  import { localTime } from '../util.js';
5
4
  import { getLogger } from '@sitespeed.io/log';
6
5
  const log = getLogger('browsertime');
7
6
  const require = createRequire(import.meta.url);
8
7
  const version = require('../../../package.json').version;
9
- import { pick, isEmpty, getProperty } from '../../support/util.js';
8
+ import { pick, isEmpty, getProperty, merge } from '../../support/util.js';
10
9
 
11
10
  const sensitiveHeaders = new Set([
12
11
  'authorization',