Skip to content

Commit 4af5cae

Browse files
authored
Tests: align test runner with other repos
Close gh-2234
1 parent 213fdba commit 4af5cae

19 files changed

+884
-240
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ bower_components
33
node_modules
44
.sizecache.json
55
package-lock.json
6+
local.log

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
},
5757
"devDependencies": {
5858
"body-parser": "1.20.2",
59+
"browserstack-local": "1.5.5",
5960
"commitplease": "3.2.0",
6061
"diff": "5.2.0",
6162
"eslint-config-jquery": "3.0.2",

tests/runner/.eslintrc.json

+3-6
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,9 @@
77
{
88
"files": ["**/*"],
99
"env": {
10+
"es6": true,
1011
"node": true
1112
},
12-
"globals": {
13-
"fetch": false,
14-
"Promise": false,
15-
"require": false
16-
},
1713
"parserOptions": {
1814
"ecmaVersion": 2022,
1915
"sourceType": "module"
@@ -27,7 +23,8 @@
2723
},
2824
"globals": {
2925
"QUnit": false,
30-
"Symbol": false
26+
"Symbol": false,
27+
"require": false
3128
},
3229
"parserOptions": {
3330
"ecmaVersion": 5,

tests/runner/browsers.js

+241-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,242 @@
1-
// This list is static, so no requests are required
2-
// in the command help menu.
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";
39

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

0 commit comments

Comments
 (0)