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 );
142 // Start up local test server
145 await new Promise( ( resolve ) => {
147 // Pass 0 to choose a random, unused port
148 server = app.listen( 0, () => {
149 port = server.address().port;
154 if ( !server || !port ) {
155 throw new Error( "Server not started." );
159 console.log( `Server started on port ${ port }.` );
162 function stopServer() {
163 return new Promise( ( resolve ) => {
164 server.close( () => {
166 console.log( "Server stopped." );
173 async function cleanup() {
174 console.log( "Cleaning up..." );
176 await cleanupAllBrowsers( { verbose } );
181 console.log( "Stopped BrowserStackLocal." );
191 { wait: EXIT_HOOK_WAIT_TIMEOUT }
194 // Start up BrowserStackLocal
196 if ( browserstack ) {
200 "BrowserStack does not support headless mode. Running in normal mode."
206 // Convert browserstack to browser objects.
207 // If browserstack is an empty array, fall back
208 // to the browsers array.
209 if ( browserstack.length ) {
210 browsers = browserstack.map( ( b ) => {
212 return browsers[ 0 ];
214 return buildBrowserFromString( b );
218 // Fill out browser defaults
219 browsers = await Promise.all(
220 browsers.map( async( browser ) => {
222 // Avoid undici connect timeout errors
223 await new Promise( ( resolve ) => setTimeout( resolve, 100 ) );
225 const latestMatch = await getLatestBrowser( browser );
226 if ( !latestMatch ) {
228 chalk.red( `Browser not found: ${ getBrowserString( browser ) }.` )
236 tunnel = await localTunnel( tunnelId );
238 console.log( "Started BrowserStackLocal." );
240 printModuleHashes( modules );
244 function queueRun( modules, browser ) {
245 const fullBrowser = getBrowserString( browser, headless );
246 const reportId = generateHash( `${ modules.join( ":" ) } ${ fullBrowser }` );
247 reports[ reportId ] = { browser, headless, modules };
249 const url = buildTestUrl( modules, {
252 jsdom: browser.browser === "jsdom",
269 addRun( url, browser, options );
272 for ( const browser of browsers ) {
274 for ( const module of modules ) {
275 queueRun( [ module ], browser );
278 queueRun( modules, browser );
283 console.log( `Starting Run ${ runId }...` );
286 console.error( error );
292 if ( errorMessages.length === 0 ) {
294 for ( const report of Object.values( reports ) ) {
295 if ( !report.total ) {
299 `No tests were run for ${ report.modules.join(
301 ) } in ${ getBrowserString( report.browser ) }`
307 return gracefulExit( 1 );
309 console.log( chalk.green( "All tests passed!" ) );
311 if ( !debug || browserstack ) {
315 console.error( chalk.red( `${ errorMessages.length } tests failed.` ) );
317 errorMessages.map( ( error, i ) => `\n${ i + 1 }. ${ error }` ).join( "\n" )
322 if ( browserstack ) {
323 console.log( "Leaving browsers with failures open for debugging." );
325 "View running sessions at https://automate.browserstack.com/dashboard/v2/"
328 console.log( "Leaving browsers open for debugging." );
330 console.log( "Press Ctrl+C to exit." );