jquery-test-runner
Version:
A test runner built by the jQuery team to run QUnit tests in real browsers using Selenium and BrowserStack
262 lines (244 loc) • 8.85 kB
JavaScript
import { Command } from "commander";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { browsers } from "./browserList.js";
import { getPlan, listBrowsers, stopWorkers } from "../browserstack/api.js";
import { buildBrowserFromString } from "../browserstack/buildBrowserFromString.js";
import { run as runTests } from "../run.js";
import readYAML from "../lib/readYAML.js";
import { createTestServer } from "../createTestServer.js";
import { pathToFileURL } from "node:url";
const program = new Command();
const DEFAULT_PORT = 3000;
const pkg = JSON.parse(
await readFile( new URL( "../package.json", import.meta.url ) )
);
program.name( pkg.name ).version( pkg.version );
// Define the run command
program
.command( "run", { isDefault: true } )
.description(
"Run unit tests in real browsers using selenium or BrowserStack."
)
.option(
"-c, --config-file <path>",
"Path to a YAML configuration file. " +
"Use this to avoid passing options via the command line. " +
"jquery-test-runner will automatically search for jtr.yml or jtr.yaml."
)
.option(
"-u, --base-url <url>",
"Base URL for the test server. " +
"Expected to always start and end with a slash (/). Defaults to \"/test/\"."
)
.option(
"-t, --test-url <urls...>",
"URLs to load the tests from. Can be multiple, but defaults to the base URL."
)
.option(
"-f, --flag <flags...>",
"Add a universal flag to be added as a query parameter " +
"to the test URL for all test pages. e.g. --flag module=core"
)
.option(
"--run <runs...>",
"Reuse the same tunnel and browser by adding more runs with different flags. " +
"Each run is a separate test run. These have the same format as the --flag option."
)
.option(
"-b, --browser <browsers...>",
"Run tests in a specific browser. Pass multiple browsers by repeating the option. " +
"If using BrowserStack, specify browsers using --browserstack. " +
"Choices: " + browsers.join( ", " ) + ". Defaults to Chrome."
)
.option(
"-m, --middleware <middlewares...>",
"Add middleware to the test server by passing the path to a module that exports " +
"a middleware factory function. Pass multiple by repeating the option."
)
.option(
"--headless",
"Run tests in headless mode. Cannot be used with --debug or --browserstack."
)
.option(
"--concurrency <number>",
"Run tests in parallel in multiple browsers. Defaults to 8 in normal mode. " +
"In browserstack mode, defaults to the maximum available under your BrowserStack plan.",
parseInt
)
.option(
"-d, --debug",
"Leave the browser open for debugging. Cannot be used with --headless."
)
.option(
"-r, --retries <number>",
"Number of times to retry failed tests by refreshing the URL.",
parseInt
)
.option(
"--hard-retries <number>",
"Number of times to retry failed tests by restarting the worker. " +
"This is in addition to the normal retries and are only used " +
"when the normal retries are exhausted.",
parseInt
)
.option( "-v, --verbose", "Log additional information." )
.option(
"--browserstack <configs...>",
"Run tests in BrowserStack. Requires BROWSERSTACK_USERNAME and " +
"BROWSERSTACK_ACCESS_KEY environment variables. " +
"The value can be empty for the default configuration, " +
"or a string in the format of \"browser_[browserVersion | :device]_os_osVersion\" " +
"(see --list-browsers). Pass multiple browsers by repeating the option. " +
"The --browser option is ignored when --browserstack has a value. " +
"Otherwise, the --browser option will be used, with the latest version/device " +
"for that browser, on a matching OS."
)
.option( "--run-id <id>", "A unique identifier for the run in BrowserStack." )
.action( async( { configFile, ...argv } ) => {
const config = await readYAML( configFile );
const options = {
baseUrl: "/test/",
...config,
testUrl: config.testUrls,
...argv
};
options.flag = [ ...parseFlags( config.flags ), ...( options.flag ?? [] ) ];
options.run = [ ...parseRuns( config.runs ), ...( options.run ?? [] ) ];
options.middleware = await parseMiddleware( options );
return runTests( options );
} );
// Define the serve command
program
.command( "serve" )
.description( "Run a simple server for loading tests in a browser." )
.option(
"-c, --config-file <path>",
"Path to a YAML configuration file. " +
"Use this to avoid passing options via the command line."
)
.option(
"-u, --base-url <url>",
"Base URL for the test server. " +
"Expected to always start and end with a slash (/). Defaults to \"/test/\"."
)
.option( "-p, --port <number>", "Port to listen on. Defaults to 3000." )
.option(
"-q, --quiet",
"Whether to log requests to the console. Default: false."
)
.option(
"-m, --middleware <middlewares...>",
"Add middleware to the test server by " +
"passing the path to a module that exports a middleware factory function. " +
"Pass multiple by repeating the option."
)
.action( async( { configFile, ...argv } ) => {
console.log( "Starting server..." );
const config = await readYAML( configFile );
const options = {
baseUrl: "/test/",
port: DEFAULT_PORT,
...config,
...argv
};
options.middleware = await parseMiddleware( options );
/**
* Run a simple server for loading tests in a browser.
* Note: this server does not support middleware.
* To add middleware, use createTestServer directly.
*/
const app = await createTestServer( options );
return app.listen( { port: options.port, host: "0.0.0.0" }, function() {
console.log(
`Open tests at http://localhost:${ options.port }${ options.baseUrl }`
);
} );
} );
// Define the list-browsers command
program
.command( "list-browsers <filter>" )
.description(
"List available BrowserStack browsers and exit.\n" +
"Leave blank to view all browsers or pass " +
"\"browser_[browserVersion | :device]_os_osVersion\" with each parameter " +
"separated by an underscore to filter the list (any can be omitted).\n" +
"\"latest\" can be used in place of \"browserVersion\" to find the latest version.\n" +
"\"latest-n\" can be used to find the nth latest browser version.\n" +
"Use a colon to indicate a device.\n" +
"Examples: \"chrome__windows_10\", \"safari_latest\", " +
"\"Mobile Safari\", \"Android Browser_:Google Pixel 8 Pro\".\n" +
"Use quotes if spaces are necessary. Requires BROWSERSTACK_USERNAME and " +
"BROWSERSTACK_ACCESS_KEY environment variables."
)
.action( ( filter ) => {
console.log( "Listing browsers with filter:", filter );
return listBrowsers( buildBrowserFromString( filter ) );
} );
// Define the stop-workers command
program
.command( "stop-workers" )
.description(
"WARNING: This will stop all BrowserStack workers that may exist and exit," +
"including any workers running from other projects.\n" +
"This can be used as a failsafe when there are too many stray workers." +
"Requires BROWSERSTACK_USERNAME and " +
"BROWSERSTACK_ACCESS_KEY environment variables."
)
.action( () => {
console.log( "Stopping workers..." );
stopWorkers();
} );
// Define the browserstack-plan command
program
.command( "browserstack-plan" )
.description(
"Show BrowserStack plan information and exit. " +
"Requires BROWSERSTACK_USERNAME and " +
"BROWSERSTACK_ACCESS_KEY environment variables."
)
.action( async() => {
console.log( await getPlan() );
} );
program.parse( process.argv );
function parseFlags( flags ) {
return Object.keys( flags ?? [] ).flatMap( ( key ) =>
flags[ key ].map( ( value ) => `${ key }=${ value }` )
);
}
// Get all possible combinations of flag values.
// Example: { "jquery": [ "1.12.4", "3.5.1" ], "jquery-migrate": [ "dev", "min" ] }
// -> [ "jquery=1.12.4&jquery-migrate=dev", "jquery=3.5.1&jquery-migrate=dev",
// "jquery=1.12.4&jquery-migrate=min", "jquery=3.5.1&jquery-migrate=min" ]
function parseRuns( runs ) {
const results = [];
function dfs( run, keys, startIndex ) {
if ( startIndex === keys.length ) {
if ( run.length > 0 ) {
results.push( run.join( "&" ) );
}
return;
}
const key = keys[ startIndex ];
const values = runs[ key ];
for ( const value of values ) {
dfs( run.concat( `${ key }=${ value }` ), keys, startIndex + 1 );
}
}
dfs( [], Object.keys( runs ?? [] ), 0 );
return results;
}
async function parseMiddleware( options ) {
const middleware = await Promise.all(
( options.middleware ?? [] ).map( async( mw ) => {
const filepath = pathToFileURL( resolve( process.cwd(), mw ) ).toString();
if ( options.verbose ) {
console.log( `Loading middleware from ${ filepath }...` );
}
const module = await import( filepath );
return module.default;
} )
);
return middleware;
}