rsnext/test/lib/browsers/selenium.ts
Zack Tanner 0b3e366f32
fix routing bug when bfcache is hit following an mpa navigation (#54081)
When an mpa navigation takes place, we currently push the user to the new route and suspend the page indefinitely (x-ref: #49058). When navigating back, if the browser opts into using the [bfcache](https://web.dev/bfcache/), it will remain suspended and `pushRef.mpaNavigation` will be true. This means that anything that would cause the component to re-render will trigger the mpa navigation again (such as hovering over another `Link`, as reported in #53347)

This PR checks to see if bfcache is being used by observing `PageTransitionEvent.persisted` and if so, resets the router state to clear out `pushRef`. 

Closes NEXT-1511
Fixes #53347
2023-08-16 17:55:06 +00:00

362 lines
9.4 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,
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
}
export class Selenium extends BrowserInterface {
private browserName: string
// TODO: support setting locale
async setup(
browserName: string,
locale: string,
javaScriptEnabled: boolean,
headless: boolean
) {
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<T = any>(attr) {
return this.chain((el) => el.getAttribute(attr)) as T
}
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<T = any>(snippet) {
if (typeof snippet === 'string' && !snippet.startsWith('return')) {
snippet = `return ${snippet}`
}
return browser.executeScript<T>(snippet)
}
async evalAsync<T = any>(snippet) {
if (typeof snippet === 'string' && !snippet.startsWith('return')) {
snippet = `return ${snippet}`
}
return browser.executeAsyncScript<T>(snippet)
}
async log() {
return this.chain(() => browser.manage().logs().get('browser')) as any
}
async url() {
return this.chain(() => browser.getCurrentUrl()) as any
}
}