Update tests for BrowserStack (#6810)

Update tests to setup webdriver stuff in `jest-environment` and re-use one browser session instead of spawning one for each webdriver call to prevent creating too many BrowserStack sessions.
This commit is contained in:
JJ Kasper 2019-03-29 10:05:53 -05:00 committed by Tim Neutkens
parent 9c2f690c0a
commit 533018f7d0
32 changed files with 542 additions and 243 deletions

View file

@ -31,6 +31,16 @@ jobs:
JEST_JUNIT_CLASSNAME: '{filepath}'
- store_test_results:
path: ~/repo/reports
test-production:
docker:
- image: circleci/node:8-browsers
working_directory: ~/repo
steps:
- attach_workspace:
at: .
- run:
name: Production Tests
command: '[[ ! -z $BROWSERSTACK_USERNAME ]] && yarn testall test/integration/production/ || echo "Not running for PR"'
deploy:
docker:
- image: circleci/node:8-browsers
@ -55,6 +65,9 @@ workflows:
- test:
requires:
- build
- test-production:
requires:
- build
- deploy:
requires:
- test

3
.gitignore vendored
View file

@ -18,7 +18,7 @@ pids
coverage
# test output
test/**/out
test/**/out*
.DS_Store
# Editors
@ -26,3 +26,4 @@ test/**/out
# example output
examples/**/out

View file

@ -4,10 +4,10 @@ module.exports = {
testMatch: ['**/*.test.js'],
verbose: true,
bail: true,
testEnvironment: 'node',
rootDir: 'test',
modulePaths: ['<rootDir>/lib'],
globalSetup: '<rootDir>/jest-global-setup.js',
globalTeardown: '<rootDir>/jest-global-teardown.js',
testEnvironment: '<rootDir>/jest-environment.js',
coverageReporters: ['text', 'lcov', 'cobertura']
}

View file

@ -70,6 +70,7 @@
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "9.0.0",
"babel-jest": "23.6.0",
"browserstack-local": "1.3.7",
"cheerio": "0.22.0",
"chromedriver": "2.46.0",
"clone": "2.1.1",

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
/* global jasmine */
/* global jasmine, webdriver */
import { join } from 'path'
import { readFileSync, writeFileSync } from 'fs'
import {
@ -14,7 +14,6 @@ import {
launchApp,
killApp
} from 'next-test-utils'
import webdriver from 'next-webdriver'
import cheerio from 'cheerio'
import amphtmlValidator from 'amphtml-validator'
const appDir = join(__dirname, '../')

View file

@ -1,6 +1,5 @@
/* eslint-env jest */
/* global jasmine */
import webdriver from 'next-webdriver'
/* global jasmine, webdriver */
import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
import {

View file

@ -1,6 +1,6 @@
/* eslint-env jest */
/* global webdriver */
import webdriver from 'next-webdriver'
import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
import { check } from 'next-test-utils'

View file

@ -1,20 +1,23 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
export default (context, render) => {
describe('With CSP enabled', () => {
it('should load inline script by hash', async () => {
const browser = await webdriver(context.appPort, '/?withCSP=hash')
const errLog = await browser.log('browser')
expect(errLog.filter((e) => e.source === 'security')).toEqual([])
if (browser.log) {
const errLog = await browser.log('browser')
expect(errLog.filter((e) => e.source === 'security')).toEqual([])
}
await browser.close()
})
it('should load inline script by nonce', async () => {
const browser = await webdriver(context.appPort, '/?withCSP=nonce')
const errLog = await browser.log('browser')
expect(errLog.filter((e) => e.source === 'security')).toEqual([])
if (browser.log) {
const errLog = await browser.log('browser')
expect(errLog.filter((e) => e.source === 'security')).toEqual([])
}
await browser.close()
})
})

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
import cheerio from 'cheerio'
import { waitFor, check } from 'next-test-utils'
@ -45,11 +45,13 @@ export default (context, render) => {
await check(() => browser.elementByCss('body').text(), /Nested 2/)
await check(() => browser.elementByCss('body').text(), /Browser hydrated/)
const logs = await browser.log('browser')
if (browser.log) {
const logs = await browser.log('browser')
logs.forEach(logItem => {
expect(logItem.message).not.toMatch(/Expected server HTML to contain/)
})
logs.forEach(logItem => {
expect(logItem.message).not.toMatch(/Expected server HTML to contain/)
})
}
} finally {
if (browser) {
await browser.close()

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
import { join } from 'path'
import { check, File, waitFor, getReactErrorOverlayContent, getBrowserBodyText } from 'next-test-utils'
@ -40,9 +40,8 @@ export default (context, renderViaHTTP) => {
it('should have installed the react-overlay-editor editor handler', async () => {
let browser
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
aboutPage.replace('</div>', 'div')
try {
aboutPage.replace('</div>', 'div')
browser = await webdriver(context.appPort, '/hmr/about')
// react-error-overlay uses the following inline style if an editorHandler is installed
@ -76,8 +75,10 @@ export default (context, renderViaHTTP) => {
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
try {
browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
aboutPage.replace('</div>', 'div')
@ -147,9 +148,10 @@ export default (context, renderViaHTTP) => {
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
try {
browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser
.elementByCss('p').text()
expect(text).toBe('This is the about page.')
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
aboutPage.replace('export', 'aa=20;\nexport')
@ -174,9 +176,10 @@ export default (context, renderViaHTTP) => {
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
try {
browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
aboutPage.replace('return', 'throw new Error("an-expected-error");\nreturn')
@ -210,8 +213,10 @@ export default (context, renderViaHTTP) => {
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
try {
browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
aboutPage.replace('export default', 'export default {};\nexport const fn =')
@ -249,8 +254,10 @@ export default (context, renderViaHTTP) => {
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
try {
browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
aboutPage.replace('export default', 'export default () => /search/;\nexport const fn =')
@ -288,8 +295,10 @@ export default (context, renderViaHTTP) => {
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
try {
browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
aboutPage.replace('export default', 'export default undefined;\nexport const fn =')

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs'
import { join } from 'path'
import { waitFor, check, getBrowserBodyText } from 'next-test-utils'
@ -135,15 +135,15 @@ export default (context, renderViaHTTP) => {
// Change the page
writeFileSync(pagePath, editedContent, 'utf8')
// wait for 5 seconds
await waitFor(5000)
try {
// Check whether the this page has reloaded or not.
const editedPTag = await browser.elementByCss('.hmr-style-page p')
const editedFontSize = await editedPTag.getComputedCss('font-size')
expect(editedFontSize).toBe('200px')
await check(
async () => {
const editedPTag = await browser.elementByCss('.hmr-style-page p')
return editedPTag.getComputedCss('font-size')
},
/200px/
)
} finally {
// Finally is used so that we revert the content back to the original regardless of the test outcome
// restore the about page content.
@ -173,14 +173,14 @@ export default (context, renderViaHTTP) => {
// Change the page
writeFileSync(pagePath, editedContent, 'utf8')
// wait for 5 seconds
await waitFor(5000)
// Check whether the this page has reloaded or not.
const editedPTag = await browser.elementByCss('.hmr-style-page p')
const editedFontSize = await editedPTag.getComputedCss('font-size')
expect(editedFontSize).toBe('200px')
await check(
async () => {
const editedPTag = await browser.elementByCss('.hmr-style-page p')
return editedPTag.getComputedCss('font-size')
},
/200px/
)
} finally {
if (browser) {
await browser.close()

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
export default (context) => {
describe('process.env', () => {

View file

@ -1,6 +1,5 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
export default (context) => {
describe('Client Navigation 404', () => {

View file

@ -1,7 +1,6 @@
/* eslint-env jest */
/* global jasmine */
/* global jasmine, webdriver */
import { join } from 'path'
import webdriver from 'next-webdriver'
import renderingSuite from './rendering'
import {
waitFor,

View file

@ -1,6 +1,6 @@
/* eslint-env jest */
/* global webdriver */
import webdriver from 'next-webdriver'
import { waitFor } from 'next-test-utils' /* check, File */
import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path'

View file

@ -1,9 +1,8 @@
/* eslint-env jest */
/* global jasmine */
/* global jasmine, webdriver */
import { join } from 'path'
import getPort from 'get-port'
import clone from 'clone'
import webdriver from 'next-webdriver'
import {
initNextServerScript,
killApp,

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
import { check, getBrowserBodyText } from 'next-test-utils'
export default function (context) {

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
import { renderViaHTTP, getBrowserBodyText, check } from 'next-test-utils'
import cheerio from 'cheerio'

View file

@ -1,8 +1,7 @@
/* eslint-env jest */
/* global jasmine */
/* global jasmine, webdriver */
import { join, resolve } from 'path'
import { existsSync } from 'fs'
import webdriver from 'next-webdriver'
import AbortController from 'abort-controller'
import {
renderViaHTTP,

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
import { waitFor } from 'next-test-utils'

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
/* global jasmine */
/* global jasmine, webdriver */
import { join } from 'path'
import {
nextServer,
@ -8,7 +8,6 @@ import {
stopApp,
runNextCommand
} from 'next-test-utils'
import webdriver from 'next-webdriver'
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
import cheerio from 'cheerio'
import { waitFor, check } from 'next-test-utils'

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
/* global jasmine */
/* global jasmine, webdriver, browserName */
import { readFileSync } from 'fs'
import { join } from 'path'
import {
@ -10,7 +10,6 @@ import {
renderViaHTTP,
waitFor
} from 'next-test-utils'
import webdriver from 'next-webdriver'
import fetch from 'node-fetch'
import dynamicImportTests from './dynamic'
import processEnv from './process-env'
@ -237,75 +236,78 @@ describe('Production Usage', () => {
await browser.close()
})
it('should add preload tags when Link prefetch prop is used', async () => {
const browser = await webdriver(appPort, '/prefetch')
const elements = await browser.elementsByCss('link[rel=preload]')
expect(elements.length).toBe(9)
await Promise.all(
elements.map(async (element) => {
const rel = await element.getAttribute('rel')
const as = await element.getAttribute('as')
expect(rel).toBe('preload')
expect(as).toBe('script')
})
)
await browser.close()
})
// This is a workaround to fix https://github.com/zeit/next.js/issues/5860
// TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed.
it('It does not add a timestamp to link tags with preload attribute', async () => {
const browser = await webdriver(appPort, '/prefetch')
const links = await browser.elementsByCss('link[rel=preload]')
await Promise.all(
links.map(async (element) => {
const href = await element.getAttribute('href')
expect(href).not.toMatch(/\?ts=/)
})
)
const scripts = await browser.elementsByCss('script[src]')
await Promise.all(
scripts.map(async (element) => {
const src = await element.getAttribute('src')
expect(src).not.toMatch(/\?ts=/)
})
)
await browser.close()
})
it('should reload the page on page script error with prefetch', async () => {
const browser = await webdriver(appPort, '/counter')
const counter = await browser
.elementByCss('#increase').click().click()
.elementByCss('#counter').text()
expect(counter).toBe('Counter: 2')
// Let the browser to prefetch the page and error it on the console.
await waitFor(3000)
const browserLogs = await browser.log('browser')
let foundLog = false
browserLogs.forEach((log) => {
if (log.message.match(/\/no-such-page\.js - Failed to load resource/)) {
foundLog = true
}
if (browserName === 'chrome') {
it('should add preload tags when Link prefetch prop is used', async () => {
const browser = await webdriver(appPort, '/prefetch')
const elements = await browser.elementsByCss('link[rel=preload]')
expect(elements.length).toBe(9)
await Promise.all(
elements.map(async (element) => {
const rel = await element.getAttribute('rel')
const as = await element.getAttribute('as')
expect(rel).toBe('preload')
expect(as).toBe('script')
})
)
await browser.close()
})
expect(foundLog).toBe(true)
// This is a workaround to fix https://github.com/zeit/next.js/issues/5860
// TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed.
it('It does not add a timestamp to link tags with preload attribute', async () => {
const browser = await webdriver(appPort, '/prefetch')
const links = await browser.elementsByCss('link[rel=preload]')
await Promise.all(
links.map(async (element) => {
const href = await element.getAttribute('href')
expect(href).not.toMatch(/\?ts=/)
})
)
const scripts = await browser.elementsByCss('script[src]')
await Promise.all(
scripts.map(async (element) => {
const src = await element.getAttribute('src')
expect(src).not.toMatch(/\?ts=/)
})
)
await browser.close()
})
// When we go to the 404 page, it'll do a hard reload.
// So, it's possible for the front proxy to load a page from another zone.
// Since the page is reloaded, when we go back to the counter page again,
// previous counter value should be gone.
const counterAfter404Page = await browser
.elementByCss('#no-such-page-prefetch').click()
.waitForElementByCss('h1')
.back()
.waitForElementByCss('#counter-page')
.elementByCss('#counter').text()
expect(counterAfter404Page).toBe('Counter: 0')
it('should reload the page on page script error with prefetch', async () => {
const browser = await webdriver(appPort, '/counter')
if (!browser.log) return
const counter = await browser
.elementByCss('#increase').click().click()
.elementByCss('#counter').text()
expect(counter).toBe('Counter: 2')
await browser.close()
})
// Let the browser to prefetch the page and error it on the console.
await waitFor(3000)
const browserLogs = await browser.log('browser')
let foundLog = false
browserLogs.forEach((log) => {
if (log.message.match(/\/no-such-page\.js - Failed to load resource/)) {
foundLog = true
}
})
expect(foundLog).toBe(true)
// When we go to the 404 page, it'll do a hard reload.
// So, it's possible for the front proxy to load a page from another zone.
// Since the page is reloaded, when we go back to the counter page again,
// previous counter value should be gone.
const counterAfter404Page = await browser
.elementByCss('#no-such-page-prefetch').click()
.waitForElementByCss('h1')
.back()
.waitForElementByCss('#counter-page')
.elementByCss('#counter').text()
expect(counterAfter404Page).toBe('Counter: 0')
await browser.close()
})
}
})
it('should not expose the compiled page file in development', async () => {
@ -342,5 +344,5 @@ describe('Production Usage', () => {
dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q))
processEnv(context)
security(context)
if (browserName === 'chrome') security(context)
})

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
/* global webdriver */
import { readFile } from 'fs'
import { promisify } from 'util'
import { join } from 'path'

View file

@ -1,8 +1,8 @@
/* eslint-env jest */
/* global webdriver */
import { readFileSync } from 'fs'
import { join, resolve as resolvePath } from 'path'
import { renderViaHTTP, getBrowserBodyText, waitFor } from 'next-test-utils'
import webdriver from 'next-webdriver'
import { recursiveReadDir } from 'next/dist/lib/recursive-readdir'
import { homedir } from 'os'

View file

@ -1,5 +1,5 @@
/* eslint-env jest */
/* global jasmine, test */
/* global jasmine, test, webdriver */
import { join } from 'path'
import { existsSync } from 'fs'
import {
@ -8,7 +8,6 @@ import {
renderViaHTTP
} from 'next-test-utils'
import startServer from '../server'
import webdriver from 'next-webdriver'
import fetch from 'node-fetch'
const appDir = join(__dirname, '../')

View file

@ -1,7 +1,6 @@
/* eslint-env jest */
/* global jasmine */
/* global jasmine, webdriver */
import { join } from 'path'
import webdriver from 'next-webdriver'
import {
getReactErrorOverlayContent,
nextServer,

210
test/jest-environment.js Normal file
View file

@ -0,0 +1,210 @@
// 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 NodeEnvironment = require('jest-environment-node')
const {
HEADLESS,
BROWSER_NAME,
BROWSERSTACK,
BROWSERSTACK_USERNAME,
BROWSERSTACK_ACCESS_KEY
} = process.env
let browser
let initialWindow
let driverPort = 9515
let browserOptions = {
browserName: BROWSER_NAME || 'chrome'
}
let deviceIP = 'localhost'
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
}
browserOptions = {
...browserOptions,
...sharedOpts,
...(isIE ? ieOpts : {}),
...(isSafari ? safariOpts : {}),
...(isFirefox ? firefoxOpts : {})
}
} else if (HEADLESS !== 'false') {
browserOptions.chromeOptions = { args: ['--headless'] }
}
const newTabPg = `
<!DOCTYPE html>
<html>
<head>
<title>new tab</title>
</head>
<body>
<a href="about:blank" target="_blank" id="new">Click me</a>
</body>
</html>
`
class CustomEnvironment extends NodeEnvironment {
async createBrowser (fromWebdriver = false) {
// always create new browser session if not BrowserStack
if ((!browser && isBrowserStack) || fromWebdriver) {
browser = isBrowserStack
? wd.promiseChainRemote(
'hub-cloud.browserstack.com', // seleniumHost
80, // seleniumPort
BROWSERSTACK_USERNAME,
BROWSERSTACK_ACCESS_KEY
)
: wd.promiseChainRemote(`http://localhost:${driverPort}/`)
// Setup the browser instance
await browser.init(browserOptions)
if (isBrowserStack) initialWindow = await browser.windowHandle()
global.browser = browser
}
if (isBrowserStack) {
// 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()
})
})
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 = BROWSER_NAME
// Mock current browser set up
this.global.webdriver = async (appPort, pathname) => {
if (!isBrowserStack) await this.createBrowser(true)
const url = `http://${deviceIP}:${appPort}${pathname}`
console.log(`\n> Loading browser with ${url}\n`)
if (isBrowserStack) 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
}
})
)
} 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()
}
}
module.exports = CustomEnvironment

View file

@ -1,14 +1,36 @@
'use strict'
let globalSetup
const chromedriver = require('chromedriver')
const waitPort = require('wait-port')
if (process.env.BROWSERSTACK) {
const { Local } = require('browserstack-local')
const browserStackLocal = new Local()
const localBrowserStackOpts = {
key: process.env.BROWSERSTACK_ACCESS_KEY
}
global.browserStackLocal = browserStackLocal
module.exports = async function globalSetup () {
chromedriver.start()
globalSetup = () => {
return new Promise((resolve, reject) => {
browserStackLocal.start(localBrowserStackOpts, err => {
if (err) return reject(err)
console.log('Started BrowserStackLocal', browserStackLocal.isRunning())
resolve()
})
})
}
} else {
const chromedriver = require('chromedriver')
const waitPort = require('wait-port')
// https://github.com/giggio/node-chromedriver/issues/117
await waitPort({
port: 9515,
timeout: 1000 * 60 * 2 // 2 Minutes
})
globalSetup = async function globalSetup () {
chromedriver.start()
// https://github.com/giggio/node-chromedriver/issues/117
await waitPort({
port: 9515,
timeout: 1000 * 60 * 2 // 2 Minutes
})
}
}
module.exports = () => globalSetup()

View file

@ -1,7 +1,27 @@
'use strict'
const chromedriver = require('chromedriver')
let globalTeardown
const browser = global.browser
module.exports = async function globalSetup () {
chromedriver.stop()
if (process.env.BROWSERSTACK) {
globalTeardown = () => global.browserStackLocal.killAllProcesses(() => {})
} else {
const chromedriver = require('chromedriver')
globalTeardown = () => chromedriver.stop()
}
module.exports = async () => {
await globalTeardown()
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()
}
} catch (_) {}
}
}

View file

@ -1,88 +0,0 @@
import wd from 'wd'
import getPort from 'get-port'
import waitPort from 'wait-port'
const doHeadless = process.env.HEADLESS !== 'false'
let driverPort = 9515
export default async function (appPort, pathname) {
if (typeof appPort === 'undefined') {
throw new Error('appPort is undefined')
}
const url = `http://localhost:${appPort}${pathname}`
console.log(`> Start loading browser with url: ${url}`)
// Sometimes browser won't initialize due to some random issues.
// So, we need to timeout the initialization and retry again.
for (let lc = 0; lc < 5; lc++) {
try {
const browser = await getBrowser(url, 5000)
console.log(`> Complete loading browser with url: ${url}`)
return browser
} catch (ex) {
console.warn(`> Error when loading browser with url: ${url}`)
// Try restarting chromedriver max twice
if (lc < 2) {
const chromedriver = require('chromedriver')
console.log('Trying to restart chromedriver with random port')
driverPort = await getPort()
chromedriver.stop()
chromedriver.start([`--port=${driverPort}`])
// https://github.com/giggio/node-chromedriver/issues/117
await waitPort({
port: driverPort,
timeout: 1000 * 30 // 30 seconds
})
continue
}
if (ex.message === 'TIMEOUT') continue
throw ex
}
}
console.error(`> Tried 5 times. Cannot load the browser for url: ${url}`)
throw new Error(`Couldn't start the browser for url: ${url}`)
}
function getBrowser (url, timeout) {
const browser = wd.promiseChainRemote(`http://localhost:${driverPort}/`)
return new Promise((resolve, reject) => {
let timeouted = false
const timeoutHandler = setTimeout(() => {
timeouted = true
const error = new Error('TIMEOUT')
reject(error)
}, timeout)
browser.init({
browserName: 'chrome',
...(doHeadless ? {
chromeOptions: { args: ['--headless'] }
} : {})
}).get(url, err => {
if (timeouted) {
try {
browser.close(() => {
// Ignore errors
})
} catch (err) {
// Ignore
}
return
}
clearTimeout(timeoutHandler)
if (err) {
reject(err)
return
}
resolve(browser)
})
})
}

119
yarn.lock
View file

@ -2896,6 +2896,17 @@ browserslist@^4.0.0, browserslist@^4.1.0:
electron-to-chromium "^1.3.103"
node-releases "^1.1.3"
browserstack-local@1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/browserstack-local/-/browserstack-local-1.3.7.tgz#cac9fc958eaa0a352e8f1ca1dc91bb141ba5da6f"
integrity sha512-ilZlmiy7XYJxsztYan7XueHVr3Ix9EVh/mCiYN1G53wRPEW/hg1KMsseM6UExzVbexEqFEfwjkBLeFlSqxh+bQ==
dependencies:
https-proxy-agent "^2.2.1"
is-running "^2.0.0"
ps-tree "=1.1.1"
sinon "^1.17.6"
temp-fs "^0.9.9"
bser@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
@ -4547,7 +4558,7 @@ duplexer3@^0.1.4:
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
duplexer@^0.1.1:
duplexer@^0.1.1, duplexer@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=
@ -4933,6 +4944,19 @@ etag@1.8.1, etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
event-stream@=3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=
dependencies:
duplexer "~0.1.1"
from "~0"
map-stream "~0.1.0"
pause-stream "0.0.11"
split "0.3"
stream-combiner "~0.0.4"
through "~2.3.1"
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
@ -5504,6 +5528,13 @@ form-data@~2.3.1, form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
formatio@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9"
integrity sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=
dependencies:
samsam "~1.1"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -5529,6 +5560,11 @@ from2@^2.1.0:
inherits "^2.0.1"
readable-stream "^2.0.0"
from@~0:
version "0.1.7"
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@ -6798,6 +6834,11 @@ is-retry-allowed@^1.0.0:
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=
is-running@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-running/-/is-running-2.1.0.tgz#30a73ff5cc3854e4fc25490809e9f5abf8de09e0"
integrity sha1-MKc/9cw4VOT8JUkICen1q/jeCeA=
is-ssh@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.1.tgz#f349a8cadd24e65298037a522cf7520f2e81a0f3"
@ -7994,6 +8035,11 @@ log-update@^1.0.2:
ansi-escapes "^1.0.0"
cli-cursor "^1.0.2"
lolex@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31"
integrity sha1-fD2mL/yzDw9agKJWbKJORdigHzE=
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -8099,6 +8145,11 @@ map-obj@^2.0.0:
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9"
integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk=
map-stream@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=
map-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@ -9448,6 +9499,13 @@ path-type@^3.0.0:
dependencies:
pify "^3.0.0"
pause-stream@0.0.11:
version "0.0.11"
resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=
dependencies:
through "~2.3"
pbkdf2@^3.0.3:
version "3.0.17"
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6"
@ -10053,6 +10111,13 @@ prr@~1.0.1:
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
ps-tree@=1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.1.tgz#5f1ba35455b8c25eeb718d04c37de1555d96d3db"
integrity sha512-kef7fYYSKVqQffmzTMsVcUD1ObNJMp8sNSmHGlGKsZQyL/ht9MZKk86u0Rd1NhpTOAuhqwKCLLpktwkqz+MF8A==
dependencies:
event-stream "=3.3.4"
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@ -10904,6 +10969,13 @@ rimraf@2.6.2:
dependencies:
glob "^7.0.5"
rimraf@~2.5.2:
version "2.5.4"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04"
integrity sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ=
dependencies:
glob "^7.0.5"
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@ -10972,6 +11044,16 @@ safe-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
samsam@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567"
integrity sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=
samsam@~1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621"
integrity sha1-n1CHQZtNCR8jJXHn+lLpCw9VJiE=
sane@^2.0.0:
version "2.5.2"
resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa"
@ -11220,6 +11302,16 @@ simple-swizzle@^0.2.2:
dependencies:
is-arrayish "^0.3.1"
sinon@^1.17.6:
version "1.17.7"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf"
integrity sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=
dependencies:
formatio "1.1.1"
lolex "1.3.2"
samsam "1.1.2"
util ">=0.10.3 <1"
sisteransi@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce"
@ -11433,6 +11525,13 @@ split2@^2.0.0:
dependencies:
through2 "^2.0.2"
split@0.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f"
integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=
dependencies:
through "2"
split@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
@ -11555,6 +11654,13 @@ stream-browserify@^2.0.1:
inherits "~2.0.1"
readable-stream "^2.0.2"
stream-combiner@~0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=
dependencies:
duplexer "~0.1.1"
stream-each@^1.1.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
@ -11901,6 +12007,13 @@ temp-dir@^1.0.0:
resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d"
integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=
temp-fs@^0.9.9:
version "0.9.9"
resolved "https://registry.yarnpkg.com/temp-fs/-/temp-fs-0.9.9.tgz#8071730437870720e9431532fe2814364f8803d7"
integrity sha1-gHFzBDeHByDpQxUy/igUNk+IA9c=
dependencies:
rimraf "~2.5.2"
temp-write@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492"
@ -11985,7 +12098,7 @@ through2@~0.4.1:
readable-stream "~1.0.17"
xtend "~2.1.1"
through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6:
through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3, through@~2.3.1:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@ -12561,7 +12674,7 @@ util@0.10.3:
dependencies:
inherits "2.0.1"
util@^0.11.0:
"util@>=0.10.3 <1", util@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==