2019-12-26 21:01:22 +01:00
|
|
|
import os from 'os'
|
|
|
|
import path from 'path'
|
|
|
|
import fetch from 'node-fetch'
|
|
|
|
import { until, Builder, By } from 'selenium-webdriver'
|
|
|
|
import { Options as ChromeOptions } from 'selenium-webdriver/chrome'
|
|
|
|
import { Options as SafariOptions } from 'selenium-webdriver/safari'
|
|
|
|
import { Options as FireFoxOptions } from 'selenium-webdriver/firefox'
|
|
|
|
|
|
|
|
const {
|
|
|
|
BROWSER_NAME: browserName = 'chrome',
|
|
|
|
BROWSERSTACK,
|
|
|
|
BROWSERSTACK_USERNAME,
|
|
|
|
BROWSERSTACK_ACCESS_KEY,
|
|
|
|
HEADLESS,
|
|
|
|
CHROME_BIN,
|
|
|
|
} = process.env
|
|
|
|
|
|
|
|
let capabilities = {}
|
|
|
|
|
|
|
|
const isChrome = browserName === 'chrome'
|
|
|
|
const isSafari = browserName === 'safari'
|
|
|
|
const isFirefox = browserName === 'firefox'
|
|
|
|
const isIE = browserName === 'internet explorer'
|
|
|
|
|
|
|
|
if (process.env.ChromeWebDriver) {
|
|
|
|
process.env.PATH = `${process.env.ChromeWebDriver}${path.delimiter}${process.env.PATH}`
|
|
|
|
}
|
|
|
|
|
|
|
|
const isBrowserStack =
|
|
|
|
BROWSERSTACK && BROWSERSTACK_USERNAME && BROWSERSTACK_ACCESS_KEY
|
|
|
|
|
|
|
|
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.user': BROWSERSTACK_USERNAME,
|
|
|
|
'browserstack.key': BROWSERSTACK_ACCESS_KEY,
|
|
|
|
'browserstack.localIdentifier': global.browserStackLocalId,
|
|
|
|
}
|
|
|
|
|
|
|
|
capabilities = {
|
|
|
|
...capabilities,
|
|
|
|
...sharedOpts,
|
|
|
|
|
|
|
|
...(isIE ? ieOpts : {}),
|
|
|
|
...(isSafari ? safariOpts : {}),
|
|
|
|
...(isFirefox ? firefoxOpts : {}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let chromeOptions = new ChromeOptions()
|
|
|
|
let firefoxOptions = new FireFoxOptions()
|
|
|
|
let safariOptions = new SafariOptions()
|
|
|
|
|
|
|
|
if (HEADLESS) {
|
|
|
|
const screenSize = { width: 1280, height: 720 }
|
|
|
|
chromeOptions = chromeOptions.headless().windowSize(screenSize)
|
|
|
|
firefoxOptions = firefoxOptions.headless().windowSize(screenSize)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (CHROME_BIN) {
|
|
|
|
chromeOptions = chromeOptions.setChromeBinaryPath(path.resolve(CHROME_BIN))
|
|
|
|
}
|
|
|
|
|
|
|
|
let seleniumServer
|
|
|
|
|
|
|
|
if (isBrowserStack) {
|
|
|
|
seleniumServer = 'http://hub-cloud.browserstack.com/wd/hub'
|
|
|
|
} else if (global.seleniumServerPort) {
|
|
|
|
seleniumServer = `http://localhost:${global.seleniumServerPort}/wd/hub`
|
|
|
|
}
|
|
|
|
|
|
|
|
let browser = new Builder()
|
|
|
|
.usingServer(seleniumServer)
|
|
|
|
.withCapabilities(capabilities)
|
|
|
|
.forBrowser(browserName)
|
|
|
|
.setChromeOptions(chromeOptions)
|
|
|
|
.setFirefoxOptions(firefoxOptions)
|
|
|
|
.setSafariOptions(safariOptions)
|
|
|
|
.build()
|
|
|
|
|
|
|
|
global.wd = browser
|
|
|
|
|
|
|
|
/*
|
|
|
|
# Methods to match
|
|
|
|
|
|
|
|
- elementByCss
|
|
|
|
- elementsByCss
|
|
|
|
- waitForElementByCss
|
|
|
|
- elementByCss.text
|
|
|
|
- elementByCss.click
|
|
|
|
*/
|
|
|
|
|
|
|
|
let initialWindow
|
|
|
|
let deviceIP = 'localhost'
|
|
|
|
|
|
|
|
const getDeviceIP = async () => {
|
|
|
|
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}:${global._newTabPort}`)
|
|
|
|
if (res.ok) {
|
|
|
|
deviceIP = address
|
|
|
|
break
|
|
|
|
}
|
|
|
|
} catch (_) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
const freshWindow = async () => {
|
|
|
|
// First we close all extra windows left over
|
|
|
|
let allWindows = await browser.getAllWindowHandles()
|
|
|
|
|
|
|
|
for (const win of allWindows) {
|
|
|
|
if (win === initialWindow) continue
|
|
|
|
try {
|
|
|
|
await browser.switchTo().window(win)
|
|
|
|
await browser.close()
|
|
|
|
} catch (_) {}
|
|
|
|
}
|
|
|
|
await browser.switchTo().window(initialWindow)
|
|
|
|
|
|
|
|
// now we open a fresh window
|
|
|
|
await browser.get(`http://${deviceIP}:${global._newTabPort}`)
|
|
|
|
|
|
|
|
const newTabLink = await browser.findElement(By.css('#new'))
|
|
|
|
await newTabLink.click()
|
|
|
|
|
|
|
|
allWindows = await browser.getAllWindowHandles()
|
|
|
|
const newWindow = allWindows.find(win => win !== initialWindow)
|
|
|
|
await browser.switchTo().window(newWindow)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default async (appPort, path) => {
|
|
|
|
if (!initialWindow) {
|
|
|
|
initialWindow = await browser.getWindowHandle()
|
|
|
|
}
|
|
|
|
if (isBrowserStack && deviceIP === 'localhost') {
|
|
|
|
await getDeviceIP()
|
|
|
|
}
|
|
|
|
// browser.switchTo().window() fails with `missing field `handle``
|
|
|
|
// in safari and firefox so disabling freshWindow since our
|
|
|
|
// tests shouldn't rely on it
|
|
|
|
if (isChrome) {
|
|
|
|
await freshWindow()
|
|
|
|
}
|
|
|
|
|
|
|
|
const url = `http://${deviceIP}:${appPort}${path}`
|
|
|
|
console.log(`\n> Loading browser with ${url}\n`)
|
|
|
|
|
|
|
|
await browser.get(url)
|
|
|
|
console.log(`\n> Loaded browser with ${url}\n`)
|
|
|
|
|
|
|
|
class Chain {
|
|
|
|
updateChain(nextCall) {
|
|
|
|
if (!this.promise) {
|
|
|
|
this.promise = Promise.resolve()
|
|
|
|
}
|
|
|
|
this.promise = this.promise.then(nextCall)
|
|
|
|
this.then = cb => this.promise.then(cb)
|
|
|
|
this.catch = cb => this.promise.catch(cb)
|
|
|
|
this.finally = cb => this.promise.finally(cb)
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
elementByCss(sel) {
|
|
|
|
return this.updateChain(() =>
|
|
|
|
browser.findElement(By.css(sel)).then(el => {
|
|
|
|
el.sel = sel
|
|
|
|
el.text = () => el.getText()
|
|
|
|
el.getComputedCss = prop => el.getCssValue(prop)
|
|
|
|
el.type = text => el.sendKeys(text)
|
|
|
|
el.getValue = () =>
|
|
|
|
browser.executeScript(
|
|
|
|
`return document.querySelector('${sel}').value`
|
|
|
|
)
|
|
|
|
return el
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
elementById(sel) {
|
|
|
|
return this.elementByCss(`#${sel}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
getValue() {
|
|
|
|
return this.updateChain(el =>
|
|
|
|
browser.executeScript(
|
|
|
|
`return document.querySelector('${el.sel}').value`
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
text() {
|
|
|
|
return this.updateChain(el => el.getText())
|
|
|
|
}
|
|
|
|
|
|
|
|
type(text) {
|
|
|
|
return this.updateChain(el => el.sendKeys(text))
|
|
|
|
}
|
|
|
|
|
|
|
|
moveTo() {
|
|
|
|
return this.updateChain(el => {
|
|
|
|
return browser
|
|
|
|
.actions()
|
|
|
|
.move({ origin: el })
|
|
|
|
.perform()
|
|
|
|
.then(() => el)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
getComputedCss(prop) {
|
|
|
|
return this.updateChain(el => {
|
|
|
|
return el.getCssValue(prop)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
getAttribute(attr) {
|
|
|
|
return this.updateChain(el => el.getAttribute(attr))
|
|
|
|
}
|
|
|
|
|
|
|
|
hasElementByCssSelector(sel) {
|
|
|
|
return this.eval(`document.querySelector('${sel}')`)
|
|
|
|
}
|
|
|
|
|
|
|
|
click() {
|
|
|
|
return this.updateChain(el => {
|
|
|
|
return el.click().then(() => el)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
elementsByCss(sel) {
|
|
|
|
return this.updateChain(() => browser.findElements(By.css(sel)))
|
|
|
|
}
|
|
|
|
|
|
|
|
waitForElementByCss(sel, timeout) {
|
|
|
|
return this.updateChain(() =>
|
|
|
|
browser.wait(until.elementLocated(By.css(sel), timeout))
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
eval(snippet) {
|
|
|
|
if (typeof snippet === 'string' && !snippet.startsWith('return')) {
|
|
|
|
snippet = `return ${snippet}`
|
|
|
|
}
|
|
|
|
return this.updateChain(() => browser.executeScript(snippet))
|
|
|
|
}
|
|
|
|
|
|
|
|
log(type) {
|
|
|
|
return this.updateChain(() =>
|
|
|
|
browser
|
|
|
|
.manage()
|
|
|
|
.logs()
|
|
|
|
.get(type)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
url() {
|
|
|
|
return this.updateChain(() => browser.getCurrentUrl())
|
|
|
|
}
|
|
|
|
|
|
|
|
back() {
|
|
|
|
return this.updateChain(() => browser.navigate().back())
|
|
|
|
}
|
|
|
|
|
|
|
|
forward() {
|
|
|
|
return this.updateChain(() => browser.navigate().forward())
|
|
|
|
}
|
|
|
|
|
|
|
|
refresh() {
|
|
|
|
return this.updateChain(() => browser.navigate().refresh())
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
return this.updateChain(() => Promise.resolve())
|
|
|
|
}
|
|
|
|
quit() {
|
|
|
|
return this.close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const promiseProp = new Set(['then', 'catch', 'finally'])
|
|
|
|
|
|
|
|
return new Proxy(new Chain(), {
|
|
|
|
get(obj, prop) {
|
|
|
|
if (obj[prop] || promiseProp.has(prop)) {
|
|
|
|
return obj[prop]
|
|
|
|
}
|
|
|
|
return browser[prop]
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|