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 ensureAcknowledged( worker
, restarts
) {
70 const fullBrowser
= getBrowserString( worker
.browser
);
71 const verbose
= worker
.options
.verbose
;
73 await
waitForAck( worker
, { fullBrowser
, verbose
} );
76 console
.error( error
.message
);
77 await
cleanupWorker( worker
, { verbose
} );
78 await
createBrowserWorker(
87 export async
function createBrowserWorker( url
, browser
, options
, restarts
= 0 ) {
88 if ( restarts
> MAX_WORKER_RESTARTS
) {
90 `Reached the maximum number of restarts for ${ chalk.yellow(
91 getBrowserString( browser )
95 const verbose
= options
.verbose
;
96 while ( ( await
getAvailableSessions() ) <= 0 ) {
98 console
.log( "\nWaiting for available sessions..." );
100 await
new Promise( ( resolve
) => setTimeout( resolve
, WORKER_WAIT_TIME
) );
103 const { debug
, runId
, tunnelId
} = options
;
104 const fullBrowser
= getBrowserString( browser
);
106 const worker
= await
createWorker( {
108 url
: encodeURI( url
),
110 build
: `Run ${ runId }`,
112 // This is the maximum timeout allowed
113 // by BrowserStack. We do this because
114 // we control the timeout in the runner.
115 // See https://github.com/browserstack/api/blob/b324a6a5bc1b6052510d74e286b8e1c758c308a7/README.md#timeout300
118 // Not documented in the API docs,
119 // but required to make local testing work.
120 // See https://www.browserstack.com/docs/automate/selenium/manage-multiple-connections#nodejs
121 "browserstack.local": true,
122 "browserstack.localIdentifier": tunnelId
125 browser
.debug
= !!debug
;
127 worker
.browser
= browser
;
128 worker
.restarts
= restarts
;
129 worker
.options
= options
;
130 touchBrowser( browser
);
131 workers
[ fullBrowser
] = worker
;
133 // Wait for the worker to show up in the list
134 // before returning it.
135 return ensureAcknowledged( worker
, restarts
);
138 export async
function setBrowserWorkerUrl( browser
, url
) {
139 const fullBrowser
= getBrowserString( browser
);
140 const worker
= workers
[ fullBrowser
];
147 * Checks that all browsers have received
148 * a response in the given amount of time.
149 * If not, the worker is restarted.
151 export async
function checkLastTouches() {
152 for ( const [ fullBrowser
, worker
] of Object
.entries( workers
) ) {
153 if ( Date
.now() - worker
.lastTouch
> RUN_WORKER_TIMEOUT
) {
154 const options
= worker
.options
;
155 if ( options
.verbose
) {
157 `\nNo response from ${ chalk.yellow( fullBrowser ) } in ${
158 RUN_WORKER_TIMEOUT / 1000 / 60
162 await
cleanupWorker( worker
, options
);
163 await
createBrowserWorker(
173 export async
function cleanupWorker( worker
, { verbose
} ) {
174 for ( const [ fullBrowser
, w
] of Object
.entries( workers
) ) {
175 if ( w
=== worker
) {
176 delete workers
[ fullBrowser
];
177 await
deleteWorker( worker
.id
);
179 console
.log( `\nStopped ${ fullBrowser }.` );
186 export async
function cleanupAllBrowsers( { verbose
} ) {
187 const workersRemaining
= Object
.values( workers
);
188 const numRemaining
= workersRemaining
.length
;
189 if ( numRemaining
) {
192 workersRemaining
.map( ( worker
) => deleteWorker( worker
.id
) )
196 `Stopped ${ numRemaining } browser${ numRemaining > 1 ? "s" : "" }.`
201 // Log the error, but do not consider the test run failed
202 console
.error( error
);