1 import chalk from "chalk";
2 import { getBrowserString } from "../lib/getBrowserString.js";
3 import { createWorker, deleteWorker, getAvailableSessions } from "./api.js";
5 const workers = Object.create( null );
8 * Keys are browser strings
9 * Structure of a worker:
11 * debug: boolean, // Stops the worker from being cleaned up when finished
13 * lastTouch: number, // The last time a request was received
15 * browser: object, // The browser object
16 * options: object // The options to create the worker
20 // Acknowledge the worker within the time limit.
21 // BrowserStack can take much longer spinning up
22 // some browsers, such as iOS 15 Safari.
23 const ACKNOWLEDGE_INTERVAL = 1000;
24 const ACKNOWLEDGE_TIMEOUT = 60 * 1000 * 5;
26 const MAX_WORKER_RESTARTS = 5;
28 // No report after the time limit
29 // should refresh the worker
30 const RUN_WORKER_TIMEOUT = 60 * 1000 * 2;
32 const WORKER_WAIT_TIME = 30000;
34 export function touchBrowser( browser ) {
35 const fullBrowser = getBrowserString( browser );
36 const worker = workers[ fullBrowser ];
38 worker.lastTouch = Date.now();
42 async function waitForAck( worker, { fullBrowser, verbose } ) {
43 delete worker.lastTouch;
44 return new Promise( ( resolve, reject ) => {
45 const interval = setInterval( () => {
46 if ( worker.lastTouch ) {
48 console.log( `\n${ fullBrowser } acknowledged.` );
50 clearTimeout( timeout );
51 clearInterval( interval );
54 }, ACKNOWLEDGE_INTERVAL );
56 const timeout = setTimeout( () => {
57 clearInterval( interval );
60 `${ fullBrowser } not acknowledged after ${
61 ACKNOWLEDGE_TIMEOUT / 1000 / 60
65 }, ACKNOWLEDGE_TIMEOUT );
69 async function restartWorker( worker ) {
70 await cleanupWorker( worker, worker.options );
71 await createBrowserWorker(
79 export async function restartBrowser( browser ) {
80 const fullBrowser = getBrowserString( browser );
81 const worker = workers[ fullBrowser ];
83 await restartWorker( worker );
87 async function ensureAcknowledged( worker ) {
88 const fullBrowser = getBrowserString( worker.browser );
89 const verbose = worker.options.verbose;
91 await waitForAck( worker, { fullBrowser, verbose } );
94 console.error( error.message );
95 await restartWorker( worker );
99 export async function createBrowserWorker( url, browser, options, restarts = 0 ) {
100 if ( restarts > MAX_WORKER_RESTARTS ) {
102 `Reached the maximum number of restarts for ${ chalk.yellow(
103 getBrowserString( browser )
107 const verbose = options.verbose;
108 while ( ( await getAvailableSessions() ) <= 0 ) {
110 console.log( "\nWaiting for available sessions..." );
112 await new Promise( ( resolve ) => setTimeout( resolve, WORKER_WAIT_TIME ) );
115 const { debug, runId, tunnelId } = options;
116 const fullBrowser = getBrowserString( browser );
118 const worker = await createWorker( {
120 url: encodeURI( url ),
122 build: `Run ${ runId }`,
124 // This is the maximum timeout allowed
125 // by BrowserStack. We do this because
126 // we control the timeout in the runner.
127 // See https://github.com/browserstack/api/blob/b324a6a5bc1b6052510d74e286b8e1c758c308a7/README.md#timeout300
130 // Not documented in the API docs,
131 // but required to make local testing work.
132 // See https://www.browserstack.com/docs/automate/selenium/manage-multiple-connections#nodejs
133 "browserstack.local": true,
134 "browserstack.localIdentifier": tunnelId
137 browser.debug = !!debug;
139 worker.browser = browser;
140 worker.restarts = restarts;
141 worker.options = options;
142 touchBrowser( browser );
143 workers[ fullBrowser ] = worker;
145 // Wait for the worker to show up in the list
146 // before returning it.
147 return ensureAcknowledged( worker );
150 export async function setBrowserWorkerUrl( browser, url ) {
151 const fullBrowser = getBrowserString( browser );
152 const worker = workers[ fullBrowser ];
159 * Checks that all browsers have received
160 * a response in the given amount of time.
161 * If not, the worker is restarted.
163 export async function checkLastTouches() {
164 for ( const [ fullBrowser, worker ] of Object.entries( workers ) ) {
165 if ( Date.now() - worker.lastTouch > RUN_WORKER_TIMEOUT ) {
166 const options = worker.options;
167 if ( options.verbose ) {
169 `\nNo response from ${ chalk.yellow( fullBrowser ) } in ${
170 RUN_WORKER_TIMEOUT / 1000 / 60
174 await restartWorker( worker );
179 export async function cleanupWorker( worker, { verbose } ) {
180 for ( const [ fullBrowser, w ] of Object.entries( workers ) ) {
181 if ( w === worker ) {
182 delete workers[ fullBrowser ];
183 await deleteWorker( worker.id );
185 console.log( `\nStopped ${ fullBrowser }.` );
192 export async function cleanupAllBrowsers( { verbose } ) {
193 const workersRemaining = Object.values( workers );
194 const numRemaining = workersRemaining.length;
195 if ( numRemaining ) {
198 workersRemaining.map( ( worker ) => deleteWorker( worker.id ) )
202 `Stopped ${ numRemaining } browser${ numRemaining > 1 ? "s" : "" }.`
207 // Log the error, but do not consider the test run failed
208 console.error( error );