Skip to content

Commit b649844

Browse files
committed
feat: initial commit
1 parent f9f6dc6 commit b649844

24 files changed

+4211
-130
lines changed

.gitignore

Lines changed: 1 addition & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1 @@
1-
# Logs
2-
logs
3-
*.log
4-
npm-debug.log*
5-
yarn-debug.log*
6-
yarn-error.log*
7-
lerna-debug.log*
8-
.pnpm-debug.log*
9-
10-
# Diagnostic reports (https://nodejs.org/api/report.html)
11-
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12-
13-
# Runtime data
14-
pids
15-
*.pid
16-
*.seed
17-
*.pid.lock
18-
19-
# Directory for instrumented libs generated by jscoverage/JSCover
20-
lib-cov
21-
22-
# Coverage directory used by tools like istanbul
23-
coverage
24-
*.lcov
25-
26-
# nyc test coverage
27-
.nyc_output
28-
29-
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30-
.grunt
31-
32-
# Bower dependency directory (https://bower.io/)
33-
bower_components
34-
35-
# node-waf configuration
36-
.lock-wscript
37-
38-
# Compiled binary addons (https://nodejs.org/api/addons.html)
39-
build/Release
40-
41-
# Dependency directories
42-
node_modules/
43-
jspm_packages/
44-
45-
# Snowpack dependency directory (https://snowpack.dev/)
46-
web_modules/
47-
48-
# TypeScript cache
49-
*.tsbuildinfo
50-
51-
# Optional npm cache directory
52-
.npm
53-
54-
# Optional eslint cache
55-
.eslintcache
56-
57-
# Optional stylelint cache
58-
.stylelintcache
59-
60-
# Microbundle cache
61-
.rpt2_cache/
62-
.rts2_cache_cjs/
63-
.rts2_cache_es/
64-
.rts2_cache_umd/
65-
66-
# Optional REPL history
67-
.node_repl_history
68-
69-
# Output of 'npm pack'
70-
*.tgz
71-
72-
# Yarn Integrity file
73-
.yarn-integrity
74-
75-
# dotenv environment variable files
76-
.env
77-
.env.development.local
78-
.env.test.local
79-
.env.production.local
80-
.env.local
81-
82-
# parcel-bundler cache (https://parceljs.org/)
83-
.cache
84-
.parcel-cache
85-
86-
# Next.js build output
87-
.next
88-
out
89-
90-
# Nuxt.js build / generate output
91-
.nuxt
92-
dist
93-
94-
# Gatsby files
95-
.cache/
96-
# Comment in the public line in if your project uses Gatsby and not Next.js
97-
# https://nextjs.org/blog/next-9-1#public-directory-support
98-
# public
99-
100-
# vuepress build output
101-
.vuepress/dist
102-
103-
# vuepress v2.x temp and cache directory
104-
.temp
105-
.cache
106-
107-
# Docusaurus cache and generated files
108-
.docusaurus
109-
110-
# Serverless directories
111-
.serverless/
112-
113-
# FuseBox cache
114-
.fusebox/
115-
116-
# DynamoDB Local files
117-
.dynamodb/
118-
119-
# TernJS port file
120-
.tern-port
121-
122-
# Stores VSCode versions used for testing VSCode extensions
123-
.vscode-test
124-
125-
# yarn v2
126-
.yarn/cache
127-
.yarn/unplugged
128-
.yarn/build-state.yml
129-
.yarn/install-state.gz
130-
.pnp.*
1+
/node_modules

browsers.js

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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+
);
248+
}

0 commit comments

Comments
 (0)