Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 41 additions & 20 deletions lib/support/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import { geckoProfilerDefaults } from '../firefox/settings/geckoProfilerDefaults
import { findUpSync } from './fileUtil.js';
import { setProperty, getProperty } from './util.js';
import { execaSync } from 'execa';
import {
classifyHelp,
captureTopics,
applyHelpFilter,
buildEpilog
} from './helpTopics.js';

const configPath = findUpSync(['.browsertime.json']);

Expand Down Expand Up @@ -152,7 +158,16 @@ export function parseCommandLine() {
arg === '--android' ? '--android.enabled' : arg
);

let yargsInstance = yargs(hideBin(argvFix));
// Detect --help <topic> / --help-all up front and rewrite them to a
// bare --help so yargs still triggers its normal help+exit flow.
const rawArgs = hideBin(argvFix);
const help = classifyHelp(rawArgs);

let yargsInstance = yargs(help.args);
// Wrap .option() to record each option's key under its inline
// `group: '<topic>'` field. Browsertime already annotates every option
// with a group, so this gives us a topic -> keys map for free.
const topicKeys = captureTopics(yargsInstance);
let validated = yargsInstance
.parserConfiguration({
'camel-case-expansion': false,
Expand Down Expand Up @@ -923,84 +938,84 @@ export function parseCommandLine() {
type: 'boolean',
default: false,
describe: 'Save one screenshot per iteration.',
group: 'Screenshot'
group: 'screenshot'
})
.option('screenshotLCP', {
type: 'boolean',
default: false,
describe:
'Save one screenshot per iteration that shows the largest contentful paint element (if the browser supports LCP).',
group: 'Screenshot'
group: 'screenshot'
})
.option('screenshotLS', {
type: 'boolean',
default: false,
describe:
'Save one screenshot per iteration that shows the layout shift elements (if the browser supports layout shift).',
group: 'Screenshot'
group: 'screenshot'
})
.option('screenshotParams.type', {
describe: 'Set the file type of the screenshot',
choices: ['png', 'jpg'],
default: screenshotDefaults.type,
group: 'Screenshot'
group: 'screenshot'
})
.option('screenshotParams.jpg.quality', {
describe: 'Quality of the JPEG screenshot. 1-100',
default: screenshotDefaults.jpg.quality,
group: 'Screenshot'
group: 'screenshot'
})
.option('screenshotParams.maxSize', {
describe: 'The max size of the screenshot (width and height).',
default: screenshotDefaults.maxSize,
group: 'Screenshot'
group: 'screenshot'
})
.option('pageCompleteCheck', {
describe:
'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',
group: 'PageLoad'
group: 'pageload'
})
.option('pageCompleteWaitTime', {
describe:
'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.',
default: 8000,
group: 'PageLoad'
group: 'pageload'
})
.option('pageCompleteCheckInactivity', {
describe:
'Alternative way to choose when to end your test. This will wait for 2 seconds of inactivity that happens after loadEventEnd.',
type: 'boolean',
default: false,
group: 'PageLoad'
group: 'pageload'
})
.option('pageCompleteCheckNetworkIdle', {
describe:
'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 ',
type: 'boolean',
default: false,
group: 'PageLoad'
group: 'pageload'
})
.option('pageCompleteCheckPollTimeout', {
type: 'number',
default: 1500,
describe:
'The time in ms to wait for running the page complete check the next time.',
group: 'PageLoad'
group: 'pageload'
})
.option('pageCompleteCheckStartWait', {
type: 'number',
default: 5000,
describe:
'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',
group: 'PageLoad'
group: 'pageload'
})
.option('pageLoadStrategy', {
type: 'string',
default: 'none',
choices: ['eager', 'none', 'normal'],
describe:
'Set the strategy to waiting for document readiness after a navigation event. After the strategy is ready, your pageCompleteCheck will start running.',
group: 'PageLoad'
group: 'pageload'
})
.option('iterations', {
alias: 'n',
Expand All @@ -1025,14 +1040,14 @@ export function parseCommandLine() {
default: 0,
describe:
'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',
group: 'PageLoad'
group: 'pageload'
})
.option('webdriverPageload', {
type: 'boolean',
describe:
'Use webdriver.get to initialize the page load instead of window.location.',
default: false,
group: 'PageLoad'
group: 'pageload'
})
.option('proxy.pac', {
type: 'string',
Expand Down Expand Up @@ -1168,7 +1183,7 @@ export function parseCommandLine() {
'Use internal browser functionality to clear browser cache between runs instead of only using Selenium.',
type: 'boolean',
default: false,
group: 'PageLoad'
group: 'pageload'
})
.option('basicAuth', {
describe:
Expand Down Expand Up @@ -1310,7 +1325,7 @@ export function parseCommandLine() {
default: false,
describe:
'Flush DNS between runs, works on Mac OS and Linux. Your user needs sudo rights to be able to flush the DNS.',
group: 'PageLoad'
group: 'pageload'
})
.option('extension', {
describe:
Expand All @@ -1321,7 +1336,7 @@ export function parseCommandLine() {
'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/',
type: 'boolean',
default: false,
group: 'PageLoad'
group: 'pageload'
})
.option('cjs', {
describe:
Expand All @@ -1334,7 +1349,7 @@ export function parseCommandLine() {
default: 3,
describe:
'If the browser fails to start, you can retry to start it this amount of times.',
group: 'PageLoad'
group: 'pageload'
})
.option('preWarmServer', {
type: 'boolean',
Expand Down Expand Up @@ -1363,8 +1378,14 @@ export function parseCommandLine() {
.alias('V', 'version')
.alias('v', 'verbose')
.wrap(yargsInstance.terminalWidth())
.epilog(buildEpilog(help.mode, help.topic, [...topicKeys.keys()]))
.check(validateInput);

// Filter the visible options based on the requested help mode. This
// must happen before .argv is read because that's what triggers
// yargs to render help and exit.
applyHelpFilter(yargsInstance, help.mode, help.topic, topicKeys);

let argv = validated.argv;

if (
Expand Down
174 changes: 174 additions & 0 deletions lib/support/helpTopics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*eslint no-console: 0*/

// Help-topic dispatch for the Browsertime CLI.
//
// Browsertime declares ~250 options in one big yargs chain, with each
// option carrying an inline `group: '<topic>'` field. The unfiltered
// --help dump is overwhelming; this module lets users say:
//
// browsertime --help -> curated set of common options
// browsertime --help <topic> -> only that topic
// browsertime --help-all -> the full historical dump
//
// We intercept the yargs instance's .option() call to capture each
// key's topic from the inline `group` field, so the cli.js chain
// doesn't have to change shape.

// Curated set of "everyday" options the default --help should show.
// Keep this list short -- anything missing is one --help <topic> away.
const COMMON_KEYS = new Set([
// common everyday flags
'browser',
'b',
'iterations',
'n',
'video',
'visualMetrics',
'speedIndex',
'docker',
'preURL',
'headless',
'connectivity.profile',
'c',
'connectivity.engine',
'mobile',
'resultDir',
'cpu',
// meta
'config',
'help',
'h',
'verbose',
'v',
'version',
'V'
]);

// Meta-flags that always stay visible regardless of mode.
const ALWAYS_KEEP = new Set([
'help',
'h',
'version',
'V',
'config',
'verbose',
'v'
]);

const DOCS_URL = 'https://www.sitespeed.io/documentation/browsertime/';

// Parse the raw arg list, detect help intent, and return a cleaned arg
// list with `--help-all` / `--help <topic>` collapsed to a plain
// `--help` so yargs still triggers its normal help+exit flow.
//
// Returns { mode, topic, args } where mode is one of:
// 'none' -> no --help passed
// 'default' -> plain --help, show the curated common subset
// 'topic' -> --help <topic>, show only that topic
// 'all' -> --help-all (or --help all), show everything
export function classifyHelp(rawArgs) {
const cleaned = [];
let mode = 'none';
let topic;
for (let i = 0; i < rawArgs.length; i++) {
const a = rawArgs[i];
if (a === '--help-all') {
mode = 'all';
cleaned.push('--help');
continue;
}
if (a === '--help' || a === '-h' || a === '--help=true') {
const next = rawArgs[i + 1];
if (next && !next.startsWith('-')) {
if (next === 'all') {
mode = 'all';
i++;
} else {
mode = 'topic';
topic = next;
i++;
}
} else if (mode === 'none') {
mode = 'default';
}
cleaned.push('--help');
continue;
}
cleaned.push(a);
}
return { mode, topic, args: cleaned };
}

// Wrap a yargs instance so every subsequent .option(key, def) call also
// records `key` under `def.group` in the returned Map. Browsertime's
// option chain already carries inline `group: '<topic>'` annotations,
// so no chain edits are needed.
export function captureTopics(yargsInstance) {
const topicKeys = new Map();
const originalOption = yargsInstance.option.bind(yargsInstance);
yargsInstance.option = function (key, def) {
if (def && def.group) {
const list = topicKeys.get(def.group) || [];
list.push(key);
topicKeys.set(def.group, list);
}
return originalOption(key, def);
};
return topicKeys;
}

// Build the epilog (footer) shown under --help, varying by mode.
export function buildEpilog(mode, topic, topicNames) {
if (mode === 'all' || mode === 'none') {
return `Read the docs at ${DOCS_URL}`;
}
if (mode === 'topic') {
return [
`Showing only the "${topic}" options.`,
`Run with --help-all for the full reference, or --help to see common options.`,
`Read the docs at ${DOCS_URL}`
].join('\n');
}
// default
const topics = topicNames.join(', ');
return [
'Topics (use `browsertime --help <topic>`):',
' ' + topics,
'',
'Run with --help-all to see every option, or read the docs at ' + DOCS_URL
].join('\n');
}

// Apply the topic filter by calling yargs.hide() on every key not in
// the set we want to display. Returns true if filtering was applied.
export function applyHelpFilter(yargsInstance, mode, topic, topicKeys) {
if (mode === 'none' || mode === 'all') return false;

let keep;
if (mode === 'topic') {
if (!topicKeys.has(topic)) {
console.error(`Unknown help topic: ${topic}`);
console.error('Available topics: ' + [...topicKeys.keys()].join(', '));
// CLI dispatch: bailing out here is intentional (deliberate "no
// such topic" exit code), not an error to bubble up the stack.
// eslint-disable-next-line unicorn/no-process-exit
process.exit(2);
}
// Topic mode = just the topic. No COMMON_KEYS floor here -- topic
// views should focus on the topic, not re-render common stuff
// that's one --help away.
keep = new Set(topicKeys.get(topic));
} else {
// default
keep = COMMON_KEYS;
}

const allKeys = Object.keys(yargsInstance.getOptions().key);
for (const k of allKeys) {
if (ALWAYS_KEEP.has(k)) continue;
if (!keep.has(k)) {
yargsInstance.hide(k);
}
}
return true;
}
Loading
Loading