|
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 |
| -exportconstbrowsers=[ |
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 |
| -exportasyncfunctiongetAvailableBrowsers(){ |
22 |
| -constbrowsers=awaitgetBrowsers({flat:true}); |
23 |
| -constavailable=[ ...newSet(browsers.map(({ browser})=>browser))]; |
24 |
| -returnavailable.concat("jsdom"); |
| 1 | +importchalkfrom"chalk"; |
| 2 | +import{getBrowserString}from"./lib/getBrowserString.js"; |
| 3 | +import{ |
| 4 | +createWorker, |
| 5 | +deleteWorker, |
| 6 | +getAvailableSessions |
| 7 | +}from"./browserstack/api.js"; |
| 8 | +importcreateDriverfrom"./selenium/createDriver.js"; |
| 9 | +importcreateWindowfrom"./jsdom/createWindow.js"; |
| 10 | + |
| 11 | +constworkers=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 | +constACKNOWLEDGE_INTERVAL=1000; |
| 31 | +constACKNOWLEDGE_TIMEOUT=60*1000*5; |
| 32 | + |
| 33 | +constMAX_WORKER_RESTARTS=5; |
| 34 | + |
| 35 | +// No report after the time limit |
| 36 | +// should refresh the worker |
| 37 | +constRUN_WORKER_TIMEOUT=60*1000*2; |
| 38 | + |
| 39 | +constWORKER_WAIT_TIME=30000; |
| 40 | + |
| 41 | +// Limit concurrency to 8 by default in selenium |
| 42 | +constMAX_SELENIUM_CONCURRENCY=8; |
| 43 | + |
| 44 | +exportasyncfunctioncreateBrowserWorker(url,browser,options,restarts=0){ |
| 45 | +if(restarts>MAX_WORKER_RESTARTS){ |
| 46 | +thrownewError( |
| 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(awaitmaxWorkersReached(options)){ |
| 54 | +if(verbose){ |
| 55 | +console.log("\nWaiting for available sessions..."); |
| 56 | +} |
| 57 | +awaitnewPromise((resolve)=>setTimeout(resolve,WORKER_WAIT_TIME)); |
| 58 | +} |
| 59 | + |
| 60 | +constfullBrowser=getBrowserString(browser); |
| 61 | + |
| 62 | +letworker; |
| 63 | + |
| 64 | +if(browserstack){ |
| 65 | +worker=awaitcreateWorker({ |
| 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 | +}elseif(browser.browser==="jsdom"){ |
| 85 | +constwindow=awaitcreateWindow({ reportId, url, verbose}); |
| 86 | +worker={ |
| 87 | +quit:()=>window.close() |
| 88 | +}; |
| 89 | +}else{ |
| 90 | +constdriver=awaitcreateDriver({ |
| 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 | +returnensureAcknowledged(worker); |
| 112 | +} |
| 113 | + |
| 114 | +exportfunctiontouchBrowser(browser){ |
| 115 | +constfullBrowser=getBrowserString(browser); |
| 116 | +constworker=workers[fullBrowser]; |
| 117 | +if(worker){ |
| 118 | +worker.lastTouch=Date.now(); |
| 119 | +} |
| 120 | +} |
| 121 | + |
| 122 | +exportasyncfunctionsetBrowserWorkerUrl(browser,url){ |
| 123 | +constfullBrowser=getBrowserString(browser); |
| 124 | +constworker=workers[fullBrowser]; |
| 125 | +if(worker){ |
| 126 | +worker.url=url; |
| 127 | +} |
| 128 | +} |
| 129 | + |
| 130 | +exportasyncfunctionrestartBrowser(browser){ |
| 131 | +constfullBrowser=getBrowserString(browser); |
| 132 | +constworker=workers[fullBrowser]; |
| 133 | +if(worker){ |
| 134 | +awaitrestartWorker(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 | +exportasyncfunctioncheckLastTouches(){ |
| 144 | +for(const[fullBrowser,worker]ofObject.entries(workers)){ |
| 145 | +if(Date.now()-worker.lastTouch>RUN_WORKER_TIMEOUT){ |
| 146 | +constoptions=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 | +awaitrestartWorker(worker); |
| 155 | +} |
| 156 | +} |
| 157 | +} |
| 158 | + |
| 159 | +exportasyncfunctioncleanupAllBrowsers({ verbose}){ |
| 160 | +constworkersRemaining=Object.values(workers); |
| 161 | +constnumRemaining=workersRemaining.length; |
| 162 | +if(numRemaining){ |
| 163 | +try{ |
| 164 | +awaitPromise.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 | +asyncfunctionmaxWorkersReached({ |
| 179 | +browserstack, |
| 180 | +concurrency=MAX_SELENIUM_CONCURRENCY |
| 181 | +}){ |
| 182 | +if(browserstack){ |
| 183 | +return(awaitgetAvailableSessions())<=0; |
| 184 | +} |
| 185 | +returnworkers.length>=concurrency; |
| 186 | +} |
| 187 | + |
| 188 | +asyncfunctionwaitForAck(worker,{ fullBrowser, verbose}){ |
| 189 | +deleteworker.lastTouch; |
| 190 | +returnnewPromise((resolve,reject)=>{ |
| 191 | +constinterval=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 | +consttimeout=setTimeout(()=>{ |
| 203 | +clearInterval(interval); |
| 204 | +reject( |
| 205 | +newError( |
| 206 | +`${fullBrowser} not acknowledged after${ |
| 207 | +ACKNOWLEDGE_TIMEOUT/1000/60 |
| 208 | +}min.` |
| 209 | +) |
| 210 | +); |
| 211 | +},ACKNOWLEDGE_TIMEOUT); |
| 212 | +}); |
| 213 | +} |
| 214 | + |
| 215 | +asyncfunctionensureAcknowledged(worker){ |
| 216 | +constfullBrowser=getBrowserString(worker.browser); |
| 217 | +constverbose=worker.options.verbose; |
| 218 | +try{ |
| 219 | +awaitwaitForAck(worker,{ fullBrowser, verbose}); |
| 220 | +returnworker; |
| 221 | +}catch(error){ |
| 222 | +console.error(error.message); |
| 223 | +awaitrestartWorker(worker); |
| 224 | +} |
| 225 | +} |
| 226 | + |
| 227 | +asyncfunctioncleanupWorker(worker,{ verbose}){ |
| 228 | +for(const[fullBrowser,w]ofObject.entries(workers)){ |
| 229 | +if(w===worker){ |
| 230 | +deleteworkers[fullBrowser]; |
| 231 | +awaitworker.quit(); |
| 232 | +if(verbose){ |
| 233 | +console.log(`\nStopped${fullBrowser}.`); |
| 234 | +} |
| 235 | +return; |
| 236 | +} |
| 237 | +} |
| 238 | +} |
| 239 | + |
| 240 | +asyncfunctionrestartWorker(worker){ |
| 241 | +awaitcleanupWorker(worker,worker.options); |
| 242 | +awaitcreateBrowserWorker( |
| 243 | +worker.url, |
| 244 | +worker.browser, |
| 245 | +worker.options, |
| 246 | +worker.restarts+1 |
| 247 | +); |
25 | 248 | }
|