Skip to content

Commit 284b082

Browse files
authored
Tests: share queue/browser handling for all worker types
- one queue to rule them all: browserstack, selenium, and jsdom - retries and hard retries are now supported in selenium - selenium tests now re-use browsers in the same way as browserstack Close jquerygh-5460
1 parent 691c0ae commit 284b082

File tree

13 files changed

+336
-446
lines changed

13 files changed

+336
-446
lines changed

test/runner/browsers.js

+247-24
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,248 @@
1-
// This list is static, so no requests are required
2-
// in the command help menu.
3-
4-
import { getBrowsers } from "./browserstack/api.js";
5-
6-
export const browsers = [
7-
"chrome",
8-
"ie",
9-
"firefox",
10-
"edge",
11-
"safari",
12-
"opera",
13-
"yandex",
14-
"IE Mobile",
15-
"Android Browser",
16-
"Mobile Safari",
17-
"jsdom"
18-
];
19-
20-
// A function that can be used to update the above list.
21-
export async function getAvailableBrowsers() {
22-
const browsers = await getBrowsers( { flat: true } );
23-
const available = [ ...new Set( browsers.map( ( { browser } ) => browser ) ) ];
24-
return available.concat( "jsdom" );
1+
import chalk from "chalk";
2+
import { getBrowserString } from "./lib/getBrowserString.js";
3+
import {
4+
createWorker,
5+
deleteWorker,
6+
getAvailableSessions
7+
} from "./browserstack/api.js";
8+
import createDriver from "./selenium/createDriver.js";
9+
import createWindow from "./jsdom/createWindow.js";
10+
11+
const workers = Object.create( null );
12+
13+
/**
14+
* Keys are browser strings
15+
* Structure of a worker:
16+
* {
17+
* browser: object // The browser object
18+
* debug: boolean // Stops the worker from being cleaned up when finished
19+
* lastTouch: number // The last time a request was received
20+
* restarts: number // The number of times the worker has been restarted
21+
* options: object // The options to create the worker
22+
* url: string // The URL the worker is on
23+
* quit: function // A function to stop the worker
24+
* }
25+
*/
26+
27+
// Acknowledge the worker within the time limit.
28+
// BrowserStack can take much longer spinning up
29+
// some browsers, such as iOS 15 Safari.
30+
const ACKNOWLEDGE_INTERVAL = 1000;
31+
const ACKNOWLEDGE_TIMEOUT = 60 * 1000 * 5;
32+
33+
const MAX_WORKER_RESTARTS = 5;
34+
35+
// No report after the time limit
36+
// should refresh the worker
37+
const RUN_WORKER_TIMEOUT = 60 * 1000 * 2;
38+
39+
const WORKER_WAIT_TIME = 30000;
40+
41+
// Limit concurrency to 8 by default in selenium
42+
const MAX_SELENIUM_CONCURRENCY = 8;
43+
44+
export async function createBrowserWorker( url, browser, options, restarts = 0 ) {
45+
if ( restarts > MAX_WORKER_RESTARTS ) {
46+
throw new Error(
47+
`Reached the maximum number of restarts for ${ chalk.yellow(
48+
getBrowserString( browser )
49+
) }`
50+
);
51+
}
52+
const { browserstack, debug, headless, reportId, runId, tunnelId, verbose } = options;
53+
while ( await maxWorkersReached( options ) ) {
54+
if ( verbose ) {
55+
console.log( "\nWaiting for available sessions..." );
56+
}
57+
await new Promise( ( resolve ) => setTimeout( resolve, WORKER_WAIT_TIME ) );
58+
}
59+
60+
const fullBrowser = getBrowserString( browser );
61+
62+
let worker;
63+
64+
if ( browserstack ) {
65+
worker = await createWorker( {
66+
...browser,
67+
url: encodeURI( url ),
68+
project: "jquery",
69+
build: `Run ${ runId }`,
70+
71+
// This is the maximum timeout allowed
72+
// by BrowserStack. We do this because
73+
// we control the timeout in the runner.
74+
// See https://github.com/browserstack/api/blob/b324a6a5bc1b6052510d74e286b8e1c758c308a7/README.md#timeout300
75+
timeout: 1800,
76+
77+
// Not documented in the API docs,
78+
// but required to make local testing work.
79+
// See https://www.browserstack.com/docs/automate/selenium/manage-multiple-connections#nodejs
80+
"browserstack.local": true,
81+
"browserstack.localIdentifier": tunnelId
82+
} );
83+
worker.quit = () => deleteWorker( worker.id );
84+
} else if ( browser.browser === "jsdom" ) {
85+
const window = await createWindow( { reportId, url, verbose } );
86+
worker = {
87+
quit: () => window.close()
88+
};
89+
} else {
90+
const driver = await createDriver( {
91+
browserName: browser.browser,
92+
headless,
93+
url,
94+
verbose
95+
} );
96+
worker = {
97+
quit: () => driver.quit()
98+
};
99+
}
100+
101+
worker.debug = !!debug;
102+
worker.url = url;
103+
worker.browser = browser;
104+
worker.restarts = restarts;
105+
worker.options = options;
106+
touchBrowser( browser );
107+
workers[ fullBrowser ] = worker;
108+
109+
// Wait for the worker to show up in the list
110+
// before returning it.
111+
return ensureAcknowledged( worker );
112+
}
113+
114+
export function touchBrowser( browser ) {
115+
const fullBrowser = getBrowserString( browser );
116+
const worker = workers[ fullBrowser ];
117+
if ( worker ) {
118+
worker.lastTouch = Date.now();
119+
}
120+
}
121+
122+
export async function setBrowserWorkerUrl( browser, url ) {
123+
const fullBrowser = getBrowserString( browser );
124+
const worker = workers[ fullBrowser ];
125+
if ( worker ) {
126+
worker.url = url;
127+
}
128+
}
129+
130+
export async function restartBrowser( browser ) {
131+
const fullBrowser = getBrowserString( browser );
132+
const worker = workers[ fullBrowser ];
133+
if ( worker ) {
134+
await restartWorker( worker );
135+
}
136+
}
137+
138+
/**
139+
* Checks that all browsers have received
140+
* a response in the given amount of time.
141+
* If not, the worker is restarted.
142+
*/
143+
export async function checkLastTouches() {
144+
for ( const [ fullBrowser, worker ] of Object.entries( workers ) ) {
145+
if ( Date.now() - worker.lastTouch > RUN_WORKER_TIMEOUT ) {
146+
const options = worker.options;
147+
if ( options.verbose ) {
148+
console.log(
149+
`\nNo response from ${ chalk.yellow( fullBrowser ) } in ${
150+
RUN_WORKER_TIMEOUT / 1000 / 60
151+
}min.`
152+
);
153+
}
154+
await restartWorker( worker );
155+
}
156+
}
157+
}
158+
159+
export async function cleanupAllBrowsers( { verbose } ) {
160+
const workersRemaining = Object.values( workers );
161+
const numRemaining = workersRemaining.length;
162+
if ( numRemaining ) {
163+
try {
164+
await Promise.all( workersRemaining.map( ( worker ) => worker.quit() ) );
165+
if ( verbose ) {
166+
console.log(
167+
`Stopped ${ numRemaining } browser${ numRemaining > 1 ? "s" : "" }.`
168+
);
169+
}
170+
} catch ( error ) {
171+
172+
// Log the error, but do not consider the test run failed
173+
console.error( error );
174+
}
175+
}
176+
}
177+
178+
async function maxWorkersReached( {
179+
browserstack,
180+
concurrency = MAX_SELENIUM_CONCURRENCY
181+
} ) {
182+
if ( browserstack ) {
183+
return ( await getAvailableSessions() ) <= 0;
184+
}
185+
return workers.length >= concurrency;
186+
}
187+
188+
async function waitForAck( worker, { fullBrowser, verbose } ) {
189+
delete worker.lastTouch;
190+
return new Promise( ( resolve, reject ) => {
191+
const interval = setInterval( () => {
192+
if ( worker.lastTouch ) {
193+
if ( verbose ) {
194+
console.log( `\n${ fullBrowser } acknowledged.` );
195+
}
196+
clearTimeout( timeout );
197+
clearInterval( interval );
198+
resolve();
199+
}
200+
}, ACKNOWLEDGE_INTERVAL );
201+
202+
const timeout = setTimeout( () => {
203+
clearInterval( interval );
204+
reject(
205+
new Error(
206+
`${ fullBrowser } not acknowledged after ${
207+
ACKNOWLEDGE_TIMEOUT / 1000 / 60
208+
}min.`
209+
)
210+
);
211+
}, ACKNOWLEDGE_TIMEOUT );
212+
} );
213+
}
214+
215+
async function ensureAcknowledged( worker ) {
216+
const fullBrowser = getBrowserString( worker.browser );
217+
const verbose = worker.options.verbose;
218+
try {
219+
await waitForAck( worker, { fullBrowser, verbose } );
220+
return worker;
221+
} catch ( error ) {
222+
console.error( error.message );
223+
await restartWorker( worker );
224+
}
225+
}
226+
227+
async function cleanupWorker( worker, { verbose } ) {
228+
for ( const [ fullBrowser, w ] of Object.entries( workers ) ) {
229+
if ( w === worker ) {
230+
delete workers[ fullBrowser ];
231+
await worker.quit();
232+
if ( verbose ) {
233+
console.log( `\nStopped ${ fullBrowser }.` );
234+
}
235+
return;
236+
}
237+
}
238+
}
239+
240+
async function restartWorker( worker ) {
241+
await cleanupWorker( worker, worker.options );
242+
await createBrowserWorker(
243+
worker.url,
244+
worker.browser,
245+
worker.options,
246+
worker.restarts + 1
247+
);
25248
}

0 commit comments

Comments
 (0)