// my-custom-environment const wd = require('wd') const os = require('os') const http = require('http') const fetch = require('node-fetch') const getPort = require('get-port') const waitPort = require('wait-port') const chromedriver = require('chromedriver') const NodeEnvironment = require('jest-environment-node') const { BROWSER_NAME, BROWSERSTACK, BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY, } = process.env let browser let initialWindow let browserOptions = { browserName: BROWSER_NAME || 'chrome', ...(process.env.HEADLESS === 'true' ? { chromeOptions: { args: ['--headless'] }, } : {}), } let deviceIP = 'localhost' let driverPort = '9515' const isIE = BROWSER_NAME === 'ie' const isSafari = BROWSER_NAME === 'safari' const isFirefox = BROWSER_NAME === 'firefox' // 30 seconds for BrowserStack 5 seconds for local const isBrowserStack = BROWSERSTACK && BROWSERSTACK_USERNAME && BROWSERSTACK_ACCESS_KEY const browserTimeout = (isBrowserStack ? 30 : 5) * 1000 if (isBrowserStack) { const safariOpts = { os: 'OS X', os_version: 'Mojave', browser: 'Safari', } const ieOpts = { os: 'Windows', os_version: '10', browser: 'IE', } const firefoxOpts = { os: 'Windows', os_version: '10', browser: 'Firefox', } const sharedOpts = { 'browserstack.local': true, 'browserstack.video': false, 'browserstack.localIdentifier': global.browserStackLocal.localIdentifierFlag, } browserOptions = { ...browserOptions, ...sharedOpts, ...(isIE ? ieOpts : {}), ...(isSafari ? safariOpts : {}), ...(isFirefox ? firefoxOpts : {}), } } const newTabPg = ` new tab Click me ` class CustomEnvironment extends NodeEnvironment { async createBrowser() { // always create new browser session if not BrowserStack if (!browser) { if (!isBrowserStack) { driverPort = await getPort() chromedriver.start([`--port=${driverPort}`]) // https://github.com/giggio/node-chromedriver/issues/117 await waitPort({ port: driverPort, timeout: 1000 * 60 * 2, // 2 Minutes }) } browser = wd.promiseChainRemote( ...(isBrowserStack ? [ 'hub-cloud.browserstack.com', // seleniumHost 80, // seleniumPort BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY, ] : [`http://localhost:${driverPort}`]) ) // Setup the browser instance await browser.init(browserOptions) initialWindow = await browser.windowHandle() } // disable browser.close and we handle it manually browser.origClose = browser.close browser.close = () => {} // Since ie11 doesn't like dataURIs we have to spin up a // server to handle the new tab page this.server = http.createServer((req, res) => { res.statusCode = 200 res.end(newTabPg) }) this.newTabPort = await getPort() await new Promise((resolve, reject) => { this.server.listen(this.newTabPort, err => { if (err) return reject(err) resolve() }) }) if (isBrowserStack) { const networkIntfs = os.networkInterfaces() // find deviceIP to use with BrowserStack for (const intf of Object.keys(networkIntfs)) { const addresses = networkIntfs[intf] for (const { internal, address, family } of addresses) { if (family !== 'IPv4' || internal) continue try { const res = await fetch(`http://${address}:${this.newTabPort}`) if (res.ok) { deviceIP = address break } } catch (_) {} } } } this.global.browserName = browserOptions.browserName // Mock current browser set up this.global.sharedWD = async (appPort, pathname) => { const url = `http://${deviceIP}:${appPort}${pathname}` console.log(`\n> Loading browser with ${url}\n`) await this.freshWindow() return new Promise((resolve, reject) => { let timedOut = false const timeoutHandler = setTimeout(() => { timedOut = true reject(new Error(`Loading browser with ${url} timed out`)) }, browserTimeout) browser.get(url, err => { if (err) return reject(err) clearTimeout(timeoutHandler) if (!timedOut) resolve(browser) }) }) } } async freshWindow(tries = 0) { if (tries > 3) throw new Error('failed to get fresh browser window') // Since we need a fresh start for each window // we have to force a new tab which can be disposed const startWindows = await browser.windowHandles() // let's close all windows that aren't the initial window for (const window of startWindows) { if (!window || window === initialWindow) continue try { await browser.window(window) await browser.origClose() } catch (_) { /* should already be closed */ } } // focus initial window await browser.window(initialWindow) const newTabUrl = `http://${deviceIP}:${this.newTabPort}/` // load html to open new tab if ((await browser.url()) !== newTabUrl) { await browser.get(newTabUrl) } // click new tab link await browser.elementByCss('#new').click() // focus fresh window const newWindows = await browser.windowHandles() try { await browser.window( newWindows.find(win => { if (win && win !== initialWindow && startWindows.indexOf(win) < 0) { return win } return false }) ) } catch (err) { await this.freshWindow(tries + 1) } } async setup() { await super.setup() await this.createBrowser() } async teardown() { await super.teardown() if (this.server) this.server.close() if (browser) { // Close all remaining browser windows try { const windows = await browser.windowHandles() for (const window of windows) { if (!window) continue await browser.window(window) await browser.origClose() await browser.quit() } } catch (_) {} } chromedriver.stop() } } module.exports = CustomEnvironment