* no suspense test and trailing slash test * use playwright for integration tests * remove seed from server mode test in auth integration * change pkg-dir version for blitz cli * use pkg-dir 5.0.0 * clean up * move seed location in auth test * explict timeout for auth test Co-authored-by: beerose <alexsandra.sikora@gmail.com>
332 lines
9.1 KiB
TypeScript
332 lines
9.1 KiB
TypeScript
import path from "path"
|
|
import resolveFrom from "resolve-from"
|
|
import {execSync} from "child_process"
|
|
import {Options as ChromeOptions} from "selenium-webdriver/chrome"
|
|
import {Options as SafariOptions} from "selenium-webdriver/safari"
|
|
import {Options as FireFoxOptions} from "selenium-webdriver/firefox"
|
|
import {Builder, By, ThenableWebDriver, until} from "selenium-webdriver"
|
|
|
|
import {BrowserInterface} from "./base"
|
|
|
|
const {
|
|
BROWSERSTACK,
|
|
BROWSERSTACK_USERNAME,
|
|
BROWSERSTACK_ACCESS_KEY,
|
|
HEADLESS,
|
|
CHROME_BIN,
|
|
LEGACY_SAFARI,
|
|
SKIP_LOCAL_SELENIUM_SERVER,
|
|
} = process.env
|
|
|
|
if (process.env.ChromeWebDriver) {
|
|
process.env.PATH = `${process.env.ChromeWebDriver}${path.delimiter}${process.env.PATH}`
|
|
}
|
|
|
|
let seleniumServer: any
|
|
let browserStackLocal: any
|
|
let browser: ThenableWebDriver
|
|
|
|
export async function quit() {
|
|
await Promise.all([
|
|
browser?.quit(),
|
|
new Promise<void>((resolve) => {
|
|
browserStackLocal ? browserStackLocal.killAllProcesses(() => resolve()) : resolve()
|
|
}),
|
|
])
|
|
seleniumServer?.kill()
|
|
|
|
browser = undefined
|
|
browserStackLocal = undefined
|
|
seleniumServer = undefined
|
|
}
|
|
|
|
class Selenium extends BrowserInterface {
|
|
private browserName: string
|
|
|
|
// TODO: support setting locale
|
|
async setup(browserName: string, locale?: string) {
|
|
if (browser) return
|
|
this.browserName = browserName
|
|
|
|
let capabilities = {}
|
|
const isSafari = browserName === "safari"
|
|
const isFirefox = browserName === "firefox"
|
|
const isIE = browserName === "internet explorer"
|
|
const isBrowserStack = BROWSERSTACK
|
|
const localSeleniumServer = SKIP_LOCAL_SELENIUM_SERVER !== "true"
|
|
|
|
// install conditional packages globally so the entire
|
|
// monorepo doesn't need to rebuild when testing
|
|
let globalNodeModules: string
|
|
|
|
if (isBrowserStack || localSeleniumServer) {
|
|
globalNodeModules = execSync("npm root -g").toString().trim()
|
|
}
|
|
|
|
if (isBrowserStack) {
|
|
const {Local} = require(resolveFrom(globalNodeModules, "browserstack-local"))
|
|
browserStackLocal = new Local()
|
|
|
|
const localBrowserStackOpts = {
|
|
key: process.env.BROWSERSTACK_ACCESS_KEY,
|
|
// Add a unique local identifier to run parallel tests
|
|
// on BrowserStack
|
|
localIdentifier: new Date().getTime(),
|
|
}
|
|
await new Promise<void>((resolve, reject) => {
|
|
browserStackLocal.start(localBrowserStackOpts, (err) => {
|
|
if (err) return reject(err)
|
|
console.log("Started BrowserStackLocal", browserStackLocal.isRunning())
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
const safariOpts = {
|
|
os: "OS X",
|
|
os_version: "Mojave",
|
|
browser: "Safari",
|
|
}
|
|
const safariLegacyOpts = {
|
|
os: "OS X",
|
|
os_version: "Sierra",
|
|
browserName: "Safari",
|
|
browser_version: "10.1",
|
|
}
|
|
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": localBrowserStackOpts.localIdentifier,
|
|
}
|
|
|
|
capabilities = {
|
|
...capabilities,
|
|
...sharedOpts,
|
|
|
|
...(isIE ? ieOpts : {}),
|
|
...(isSafari ? (LEGACY_SAFARI ? safariLegacyOpts : safariOpts) : {}),
|
|
...(isFirefox ? firefoxOpts : {}),
|
|
}
|
|
} else if (localSeleniumServer) {
|
|
console.log("Installing selenium server")
|
|
const seleniumServerMod = require(resolveFrom(globalNodeModules, "selenium-standalone"))
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
seleniumServerMod.install((err) => {
|
|
if (err) return reject(err)
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
console.log("Starting selenium server")
|
|
await new Promise<void>((resolve, reject) => {
|
|
seleniumServerMod.start((err, child) => {
|
|
if (err) return reject(err)
|
|
seleniumServer = child
|
|
resolve()
|
|
})
|
|
})
|
|
console.log("Started selenium server")
|
|
}
|
|
|
|
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) as any
|
|
firefoxOptions = firefoxOptions.headless().windowSize(screenSize)
|
|
}
|
|
|
|
if (CHROME_BIN) {
|
|
chromeOptions = chromeOptions.setChromeBinaryPath(path.resolve(CHROME_BIN))
|
|
}
|
|
|
|
let seleniumServerUrl
|
|
|
|
if (isBrowserStack) {
|
|
seleniumServerUrl = "http://hub-cloud.browserstack.com/wd/hub"
|
|
} else if (localSeleniumServer) {
|
|
seleniumServerUrl = `http://localhost:4444/wd/hub`
|
|
}
|
|
|
|
browser = new Builder()
|
|
.usingServer(seleniumServerUrl)
|
|
.withCapabilities(capabilities)
|
|
.forBrowser(browserName)
|
|
.setChromeOptions(chromeOptions)
|
|
.setFirefoxOptions(firefoxOptions)
|
|
.setSafariOptions(safariOptions)
|
|
.build()
|
|
}
|
|
|
|
async get(url: string): Promise<void> {
|
|
return browser.get(url)
|
|
}
|
|
|
|
async loadPage(url: string) {
|
|
// in chrome we use a new tab for testing
|
|
if (this.browserName === "chrome") {
|
|
const initialHandle = await browser.getWindowHandle()
|
|
|
|
await browser.switchTo().newWindow("tab")
|
|
const newHandle = await browser.getWindowHandle()
|
|
|
|
await browser.switchTo().window(initialHandle)
|
|
await browser.close()
|
|
await browser.switchTo().window(newHandle)
|
|
|
|
// clean-up extra windows created from links and such
|
|
for (const handle of await browser.getAllWindowHandles()) {
|
|
if (handle !== newHandle) {
|
|
await browser.switchTo().window(handle)
|
|
await browser.close()
|
|
}
|
|
}
|
|
await browser.switchTo().window(newHandle)
|
|
} else {
|
|
await browser.get("about:blank")
|
|
}
|
|
return browser.get(url)
|
|
}
|
|
|
|
back(): BrowserInterface {
|
|
return this.chain(() => {
|
|
return browser.navigate().back()
|
|
})
|
|
}
|
|
forward(): BrowserInterface {
|
|
return this.chain(() => {
|
|
return browser.navigate().forward()
|
|
})
|
|
}
|
|
refresh(): BrowserInterface {
|
|
return this.chain(() => {
|
|
return browser.navigate().refresh()
|
|
})
|
|
}
|
|
setDimensions({width, height}: {height: number; width: number}): BrowserInterface {
|
|
return this.chain(() => browser.manage().window().setRect({width, height, x: 0, y: 0}))
|
|
}
|
|
addCookie(opts: {name: string; value: string}): BrowserInterface {
|
|
return this.chain(() => browser.manage().addCookie(opts))
|
|
}
|
|
deleteCookies(): BrowserInterface {
|
|
return this.chain(() => browser.manage().deleteAllCookies())
|
|
}
|
|
|
|
elementByCss(selector: string) {
|
|
return this.chain(() => {
|
|
return browser.findElement(By.css(selector)).then((el: any) => {
|
|
el.selector = selector
|
|
el.text = () => el.getText()
|
|
el.getComputedCss = (prop) => el.getCssValue(prop)
|
|
el.type = (text) => el.sendKeys(text)
|
|
el.getValue = () =>
|
|
browser.executeScript(`return document.querySelector('${selector}').value`)
|
|
return el
|
|
})
|
|
})
|
|
}
|
|
|
|
elementById(sel) {
|
|
return this.elementByCss(`#${sel}`)
|
|
}
|
|
|
|
getValue() {
|
|
return this.chain((el) =>
|
|
browser.executeScript(`return document.querySelector('${el.selector}').value`),
|
|
) as any
|
|
}
|
|
|
|
text() {
|
|
return this.chain((el) => el.getText()) as any
|
|
}
|
|
|
|
type(text) {
|
|
return this.chain((el) => el.sendKeys(text))
|
|
}
|
|
|
|
moveTo() {
|
|
return this.chain((el) => {
|
|
return browser
|
|
.actions()
|
|
.move({origin: el})
|
|
.perform()
|
|
.then(() => el)
|
|
})
|
|
}
|
|
|
|
async getComputedCss(prop: string) {
|
|
return this.chain((el) => {
|
|
return el.getCssValue(prop)
|
|
}) as any
|
|
}
|
|
|
|
async getAttribute(attr) {
|
|
return this.chain((el) => el.getAttribute(attr))
|
|
}
|
|
|
|
async hasElementByCssSelector(selector: string) {
|
|
return this.eval(`!!document.querySelector('${selector}')`) as any
|
|
}
|
|
|
|
click() {
|
|
return this.chain((el) => {
|
|
return el.click().then(() => el)
|
|
})
|
|
}
|
|
|
|
elementsByCss(sel) {
|
|
return this.chain(() => browser.findElements(By.css(sel))) as any as BrowserInterface[]
|
|
}
|
|
|
|
waitForElementByCss(sel, timeout) {
|
|
return this.chain(() => browser.wait(until.elementLocated(By.css(sel)), timeout))
|
|
}
|
|
|
|
waitForCondition(condition, timeout) {
|
|
return this.chain(() =>
|
|
browser.wait(async (driver) => {
|
|
return driver.executeScript("return " + condition).catch(() => false)
|
|
}, timeout),
|
|
)
|
|
}
|
|
|
|
async eval(snippet) {
|
|
if (typeof snippet === "string" && !snippet.startsWith("return")) {
|
|
snippet = `return ${snippet}`
|
|
}
|
|
return browser.executeScript(snippet)
|
|
}
|
|
|
|
async evalAsync(snippet) {
|
|
if (typeof snippet === "string" && !snippet.startsWith("return")) {
|
|
snippet = `return ${snippet}`
|
|
}
|
|
return browser.executeAsyncScript(snippet)
|
|
}
|
|
|
|
async log() {
|
|
return this.chain(() => browser.manage().logs().get("browser")) as any
|
|
}
|
|
|
|
async url() {
|
|
return this.chain(() => browser.getCurrentUrl()) as any
|
|
}
|
|
}
|
|
|
|
export default Selenium
|