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 { cleanupAllJSDOM
, cleanupJSDOM
} from "./jsdom.js";
12 import { modules as allModules
} from "./modules.js";
13 import { cleanupAllBrowsers
, touchBrowser
} from "./browserstack/browsers.js";
19 } from "./browserstack/queue.js";
20 import { addSeleniumRun
, runAllSelenium
} from "./selenium/queue.js";
22 const EXIT_HOOK_WAIT_TIMEOUT
= 60 * 1000;
25 * Run modules in parallel in different browser instances.
27 export async
function run( {
28 browsers
: browserNames
,
40 if ( !browserNames
|| !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( ( 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
;
113 cleanupJSDOM( reportId
, { verbose
} );
117 const retry
= retryTest( reportId
, retries
);
119 // Retry if retryTest returns a test
123 errorMessages
.push( ...Object
.values( pendingErrors
[ reportId
] ) );
127 return getNextBrowserTest( reportId
);
130 const report
= reports
[ message
.id
];
131 touchBrowser( report
.browser
);
135 console
.warn( "Received unknown message type:", message
.type
);
139 // Start up local test server
142 await
new Promise( ( resolve
) => {
144 // Pass 0 to choose a random, unused port
145 server
= app
.listen( 0, () => {
146 port
= server
.address().port
;
151 if ( !server
|| !port
) {
152 throw new Error( "Server not started." );
156 console
.log( `Server started on port ${ port }.` );
159 function stopServer() {
160 return new Promise( ( resolve
) => {
161 server
.close( () => {
163 console
.log( "Server stopped." );
170 async
function cleanup() {
171 console
.log( "Cleaning up..." );
176 console
.log( "Stopped BrowserStackLocal." );
180 await
cleanupAllBrowsers( { verbose
} );
181 cleanupAllJSDOM( { verbose
} );
189 { wait
: EXIT_HOOK_WAIT_TIMEOUT
}
192 // Start up BrowserStackLocal
194 if ( browserstack
) {
198 "BrowserStack does not support headless mode. Running in normal mode."
204 // Convert browserstack to browser objects.
205 // If browserstack is an empty array, fall back
206 // to the browsers array.
207 if ( browserstack
.length
) {
208 browsers
= browserstack
.map( ( b
) => {
210 return browsers
[ 0 ];
212 return buildBrowserFromString( b
);
216 // Fill out browser defaults
217 browsers
= await Promise
.all(
218 browsers
.map( async( browser
) => {
220 // Avoid undici connect timeout errors
221 await
new Promise( ( resolve
) => setTimeout( resolve
, 100 ) );
223 const latestMatch
= await
getLatestBrowser( browser
);
224 if ( !latestMatch
) {
226 chalk
.red( `Browser not found: ${ getBrowserString( browser ) }.` )
234 tunnel
= await
localTunnel( tunnelId
);
236 console
.log( "Started BrowserStackLocal." );
238 printModuleHashes( modules
);
242 function queueRun( modules
, browser
) {
243 const fullBrowser
= getBrowserString( browser
, headless
);
244 const reportId
= generateHash( `${ modules.join( ":" ) } ${ fullBrowser }` );
245 reports
[ reportId
] = { browser
, headless
, modules
};
247 const url
= buildTestUrl( modules
, {
250 jsdom
: browser
.browser
=== "jsdom",
265 if ( browserstack
) {
266 addBrowserStackRun( url
, browser
, options
);
268 addSeleniumRun( 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 }...` );
284 if ( browserstack
) {
285 await
runAllBrowserStack( { verbose
} );
287 await
runAllSelenium( { concurrency
, verbose
} );
290 console
.error( error
);
296 if ( errorMessages
.length
=== 0 ) {
298 for ( const report
of Object
.values( reports
) ) {
299 if ( !report
.total
) {
303 `No tests were run for ${ report.modules.join(
305 ) } in ${ getBrowserString( report.browser ) }`
311 return gracefulExit( 1 );
313 console
.log( chalk
.green( "All tests passed!" ) );
315 if ( !debug
|| browserstack
) {
319 console
.error( chalk
.red( `${ errorMessages.length } tests failed.` ) );
321 errorMessages
.map( ( error
, i
) => `\n${ i + 1 }. ${ error }` ).join( "\n" )
326 if ( browserstack
) {
327 console
.log( "Leaving browsers with failures open for debugging." );
329 "View running sessions at https://automate.browserstack.com/dashboard/v2/"
332 console
.log( "Leaving browsers open for debugging." );
334 console
.log( "Press Ctrl+C to exit." );