1 import chalk from "chalk";
2 import { asyncExitHook, gracefulExit } from "exit-hook";
3 import { getLatestBrowser } from "./browserstack/api.js";
4 import { buildBrowserFromString } from "./browserstack/buildBrowserFromString.js";
5 import { localTunnel } from "./browserstack/local.js";
6 import { reportEnd, reportTest } from "./reporter.js";
7 import { createTestServer } from "./createTestServer.js";
8 import { buildTestUrl } from "./lib/buildTestUrl.js";
9 import { generateHash, printModuleHashes } from "./lib/generateHash.js";
10 import { getBrowserString } from "./lib/getBrowserString.js";
11 import { modules as allModules } from "./flags/modules.js";
12 import { cleanupAllBrowsers, touchBrowser } from "./browsers.js";
21 const EXIT_HOOK_WAIT_TIMEOUT = 60 * 1000;
24 * Run modules in parallel in different browser instances.
26 export async function run( {
27 browser: browserNames = [],
40 if ( !browserNames.length ) {
41 browserNames = [ "chrome" ];
43 if ( !modules.length ) {
46 if ( headless && debug ) {
48 "Cannot run in headless mode and debug mode at the same time."
53 console.log( browserstack ? "Running in BrowserStack." : "Running locally." );
56 const errorMessages = [];
57 const pendingErrors = {};
59 // Convert browser names to browser objects
60 let browsers = browserNames.map( ( b ) => ( { browser: b } ) );
61 const tunnelId = generateHash(
62 `${ Date.now() }-${ modules.join( ":" ) }-${ ( browserstack || [] )
63 .concat( browserNames )
67 // A unique identifier for this run
72 // Create the test app and
73 // hook it up to the reporter
74 const reports = Object.create( null );
75 const app = await createTestServer( async( message ) => {
76 switch ( message.type ) {
78 const reportId = message.id;
79 const report = reports[ reportId ];
80 touchBrowser( report.browser );
81 const errors = reportTest( message.data, reportId, report );
82 pendingErrors[ reportId ] ??= Object.create( null );
84 pendingErrors[ reportId ][ message.data.name ] = errors;
86 const existing = pendingErrors[ reportId ][ message.data.name ];
88 // Show a message for flakey tests
93 chalk.gray( existing.replace( "Test failed", "Test flakey" ) )
97 delete pendingErrors[ reportId ][ message.data.name ];
103 const reportId = message.id;
104 const report = reports[ reportId ];
105 touchBrowser( report.browser );
106 const { failed, total } = reportEnd(
111 report.total = total;
115 const retry = retryTest( reportId, retries );
117 // Retry if retryTest returns a test
122 // Return early if hardRetryTest returns true
123 if ( await hardRetryTest( reportId, hardRetries ) ) {
126 errorMessages.push( ...Object.values( pendingErrors[ reportId ] ) );
130 return getNextBrowserTest( reportId );
133 const report = reports[ message.id ];
134 touchBrowser( report.browser );
138 console.warn( "Received unknown message type:", message.type );
141 // Hide test server request logs in CLI output
142 }, { quiet: true } );
144 // Start up local test server
147 await new Promise( ( resolve ) => {
149 // Pass 0 to choose a random, unused port
150 server = app.listen( 0, () => {
151 port = server.address().port;
156 if ( !server || !port ) {
157 throw new Error( "Server not started." );
161 console.log( `Server started on port ${ port }.` );
164 function stopServer() {
165 return new Promise( ( resolve ) => {
166 server.close( () => {
168 console.log( "Server stopped." );
175 async function cleanup() {
176 console.log( "Cleaning up..." );
178 await cleanupAllBrowsers( { verbose } );
183 console.log( "Stopped BrowserStackLocal." );
193 { wait: EXIT_HOOK_WAIT_TIMEOUT }
196 // Start up BrowserStackLocal
198 if ( browserstack ) {
202 "BrowserStack does not support headless mode. Running in normal mode."
208 // Convert browserstack to browser objects.
209 // If browserstack is an empty array, fall back
210 // to the browsers array.
211 if ( browserstack.length ) {
212 browsers = browserstack.map( ( b ) => {
214 return browsers[ 0 ];
216 return buildBrowserFromString( b );
220 // Fill out browser defaults
221 browsers = await Promise.all(
222 browsers.map( async( browser ) => {
224 // Avoid undici connect timeout errors
225 await new Promise( ( resolve ) => setTimeout( resolve, 100 ) );
227 const latestMatch = await getLatestBrowser( browser );
228 if ( !latestMatch ) {
230 chalk.red( `Browser not found: ${ getBrowserString( browser ) }.` )
238 tunnel = await localTunnel( tunnelId );
240 console.log( "Started BrowserStackLocal." );
242 printModuleHashes( modules );
246 function queueRun( modules, browser ) {
247 const fullBrowser = getBrowserString( browser, headless );
248 const reportId = generateHash( `${ modules.join( ":" ) } ${ fullBrowser }` );
249 reports[ reportId ] = { browser, headless, modules };
251 const url = buildTestUrl( modules, {
254 jsdom: browser.browser === "jsdom",
271 addRun( url, browser, options );
274 for ( const browser of browsers ) {
276 for ( const module of modules ) {
277 queueRun( [ module ], browser );
280 queueRun( modules, browser );
285 console.log( `Starting Run ${ runId }...` );
288 console.error( error );
294 if ( errorMessages.length === 0 ) {
296 for ( const report of Object.values( reports ) ) {
297 if ( !report.total ) {
301 `No tests were run for ${ report.modules.join(
303 ) } in ${ getBrowserString( report.browser ) }`
309 return gracefulExit( 1 );
311 console.log( chalk.green( "All tests passed!" ) );
313 if ( !debug || browserstack ) {
317 console.error( chalk.red( `${ errorMessages.length } tests failed.` ) );
319 errorMessages.map( ( error, i ) => `\n${ i + 1 }. ${ error }` ).join( "\n" )
324 if ( browserstack ) {
325 console.log( "Leaving browsers with failures open for debugging." );
327 "View running sessions at https://automate.browserstack.com/dashboard/v2/"
330 console.log( "Leaving browsers open for debugging." );
332 console.log( "Press Ctrl+C to exit." );