Tests: fix flakey message logs; ignore delete worker failures
[jquery.git] / test / runner / run.js
blob9f7a38aa7e30f9e96020995cd917f7f74f0c7841
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";
14 import {
15 addBrowserStackRun,
16 getNextBrowserTest,
17 retryTest,
18 runAllBrowserStack
19 } from "./browserstack/queue.js";
20 import { addSeleniumRun, runAllSelenium } from "./selenium/queue.js";
22 const EXIT_HOOK_WAIT_TIMEOUT = 60 * 1000;
24 /**
25 * Run modules in parallel in different browser instances.
27 export async function run( {
28 browsers: browserNames,
29 browserstack,
30 concurrency,
31 debug,
32 esm,
33 headless,
34 isolate,
35 modules = [],
36 retries = 0,
37 runId,
38 verbose
39 } ) {
40 if ( !browserNames || !browserNames.length ) {
41 browserNames = [ "chrome" ];
43 if ( !modules.length ) {
44 modules = allModules;
46 if ( headless && debug ) {
47 throw new Error(
48 "Cannot run in headless mode and debug mode at the same time."
52 if ( verbose ) {
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 )
64 .join( ":" ) }`
67 // A unique identifier for this run
68 if ( !runId ) {
69 runId = tunnelId;
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 ) {
77 case "testEnd": {
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 );
83 if ( errors ) {
84 pendingErrors[ reportId ][ message.data.name ] = errors;
85 } else {
86 const existing = pendingErrors[ reportId ][ message.data.name ];
88 // Show a message for flakey tests
89 if ( existing ) {
90 console.log();
91 console.warn(
92 chalk.italic(
93 chalk.gray( existing.replace( "Test failed", "Test flakey" ) )
96 console.log();
97 delete pendingErrors[ reportId ][ message.data.name ];
100 break;
102 case "runEnd": {
103 const reportId = message.id;
104 const report = reports[ reportId ];
105 touchBrowser( report.browser );
106 const { failed, total } = reportEnd(
107 message.data,
108 message.id,
109 reports[ reportId ]
111 report.total = total;
113 cleanupJSDOM( reportId, { verbose } );
115 // Handle failure
116 if ( failed ) {
117 const retry = retryTest( reportId, retries );
119 // Retry if retryTest returns a test
120 if ( retry ) {
121 return retry;
123 errorMessages.push( ...Object.values( pendingErrors[ reportId ] ) );
126 // Run the next test
127 return getNextBrowserTest( reportId );
129 case "ack": {
130 const report = reports[ message.id ];
131 touchBrowser( report.browser );
132 break;
134 default:
135 console.warn( "Received unknown message type:", message.type );
137 } );
139 // Start up local test server
140 let server;
141 let port;
142 await new Promise( ( resolve ) => {
144 // Pass 0 to choose a random, unused port
145 server = app.listen( 0, () => {
146 port = server.address().port;
147 resolve();
148 } );
149 } );
151 if ( !server || !port ) {
152 throw new Error( "Server not started." );
155 if ( verbose ) {
156 console.log( `Server started on port ${ port }.` );
159 function stopServer() {
160 return new Promise( ( resolve ) => {
161 server.close( () => {
162 if ( verbose ) {
163 console.log( "Server stopped." );
165 resolve();
166 } );
167 } );
170 async function cleanup() {
171 console.log( "Cleaning up..." );
173 if ( tunnel ) {
174 await tunnel.stop();
175 if ( verbose ) {
176 console.log( "Stopped BrowserStackLocal." );
180 await cleanupAllBrowsers( { verbose } );
181 cleanupAllJSDOM( { verbose } );
184 asyncExitHook(
185 async() => {
186 await stopServer();
187 await cleanup();
189 { wait: EXIT_HOOK_WAIT_TIMEOUT }
192 // Start up BrowserStackLocal
193 let tunnel;
194 if ( browserstack ) {
195 if ( headless ) {
196 console.warn(
197 chalk.italic(
198 "BrowserStack does not support headless mode. Running in normal mode."
201 headless = false;
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 ) => {
209 if ( !b ) {
210 return browsers[ 0 ];
212 return buildBrowserFromString( b );
213 } );
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 ) {
225 console.error(
226 chalk.red( `Browser not found: ${ getBrowserString( browser ) }.` )
228 gracefulExit( 1 );
230 return latestMatch;
234 tunnel = await localTunnel( tunnelId );
235 if ( verbose ) {
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, {
248 browserstack,
249 esm,
250 jsdom: browser.browser === "jsdom",
251 port,
252 reportId
253 } );
255 const options = {
256 debug,
257 headless,
258 modules,
259 reportId,
260 runId,
261 tunnelId,
262 verbose
265 if ( browserstack ) {
266 addBrowserStackRun( url, browser, options );
267 } else {
268 addSeleniumRun( url, browser, options );
272 for ( const browser of browsers ) {
273 if ( isolate ) {
274 for ( const module of modules ) {
275 queueRun( [ module ], browser );
277 } else {
278 queueRun( modules, browser );
282 try {
283 console.log( `Starting Run ${ runId }...` );
284 if ( browserstack ) {
285 await runAllBrowserStack( { verbose } );
286 } else {
287 await runAllSelenium( { concurrency, verbose } );
289 } catch ( error ) {
290 console.error( error );
291 if ( !debug ) {
292 gracefulExit( 1 );
294 } finally {
295 console.log();
296 if ( errorMessages.length === 0 ) {
297 let stop = false;
298 for ( const report of Object.values( reports ) ) {
299 if ( !report.total ) {
300 stop = true;
301 console.error(
302 chalk.red(
303 `No tests were run for ${ report.modules.join(
304 ", "
305 ) } in ${ getBrowserString( report.browser ) }`
310 if ( stop ) {
311 return gracefulExit( 1 );
313 console.log( chalk.green( "All tests passed!" ) );
315 if ( !debug || browserstack ) {
316 gracefulExit( 0 );
318 } else {
319 console.error( chalk.red( `${ errorMessages.length } tests failed.` ) );
320 console.log(
321 errorMessages.map( ( error, i ) => `\n${ i + 1 }. ${ error }` ).join( "\n" )
324 if ( debug ) {
325 console.log();
326 if ( browserstack ) {
327 console.log( "Leaving browsers with failures open for debugging." );
328 console.log(
329 "View running sessions at https://automate.browserstack.com/dashboard/v2/"
331 } else {
332 console.log( "Leaving browsers open for debugging." );
334 console.log( "Press Ctrl+C to exit." );
335 } else {
336 gracefulExit( 1 );