06607e3dd1
<!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change that you're making: --> ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) @ijjk moving this here. Co-authored-by: JJ Kasper <jj@jjsweb.site>
164 lines
4.6 KiB
TypeScript
164 lines
4.6 KiB
TypeScript
import { getFullUrl } from 'next-test-utils'
|
|
import os from 'os'
|
|
import { BrowserInterface } from './browsers/base'
|
|
|
|
if (!process.env.TEST_FILE_PATH) {
|
|
process.env.TEST_FILE_PATH = module.parent.filename
|
|
}
|
|
|
|
let deviceIP: string
|
|
const isBrowserStack = !!process.env.BROWSERSTACK
|
|
;(global as any).browserName = process.env.BROWSER_NAME || 'chrome'
|
|
|
|
if (isBrowserStack) {
|
|
const nets = os.networkInterfaces()
|
|
for (const key of Object.keys(nets)) {
|
|
let done = false
|
|
|
|
for (const item of nets[key]) {
|
|
if (item.family === 'IPv4' && !item.internal) {
|
|
deviceIP = item.address
|
|
done = true
|
|
break
|
|
}
|
|
}
|
|
if (done) break
|
|
}
|
|
}
|
|
|
|
let browserQuit: () => Promise<void>
|
|
|
|
if (typeof afterAll === 'function') {
|
|
afterAll(async () => {
|
|
if (browserQuit) {
|
|
await browserQuit()
|
|
}
|
|
})
|
|
}
|
|
|
|
export const USE_SELENIUM = Boolean(
|
|
process.env.LEGACY_SAFARI ||
|
|
process.env.BROWSER_NAME === 'internet explorer' ||
|
|
process.env.SKIP_LOCAL_SELENIUM_SERVER
|
|
)
|
|
|
|
/**
|
|
*
|
|
* @param appPortOrUrl can either be the port or the full URL
|
|
* @param url the path/query to append when using appPort
|
|
* @param options.waitHydration whether to wait for react hydration to finish
|
|
* @param options.retryWaitHydration allow retrying hydration wait if reload occurs
|
|
* @param options.disableCache disable cache for page load
|
|
* @param options.beforePageLoad the callback receiving page instance before loading page
|
|
* @returns thenable browser instance
|
|
*/
|
|
export default async function webdriver(
|
|
appPortOrUrl: string | number,
|
|
url: string,
|
|
options?: {
|
|
waitHydration?: boolean
|
|
retryWaitHydration?: boolean
|
|
disableCache?: boolean
|
|
beforePageLoad?: (page: any) => void
|
|
locale?: string
|
|
}
|
|
): Promise<BrowserInterface> {
|
|
let CurrentInterface: typeof BrowserInterface
|
|
|
|
const defaultOptions = {
|
|
waitHydration: true,
|
|
retryWaitHydration: false,
|
|
disableCache: false,
|
|
}
|
|
options = Object.assign(defaultOptions, options)
|
|
const {
|
|
waitHydration,
|
|
retryWaitHydration,
|
|
disableCache,
|
|
beforePageLoad,
|
|
locale,
|
|
} = options
|
|
|
|
// we import only the needed interface
|
|
if (USE_SELENIUM) {
|
|
const { Selenium, quit } = await import('./browsers/selenium')
|
|
CurrentInterface = Selenium
|
|
browserQuit = quit
|
|
} else if (process.env.RECORD_REPLAY === 'true') {
|
|
const { Replay, quit } = await require('./browsers/replay')
|
|
CurrentInterface = Replay
|
|
browserQuit = quit
|
|
} else {
|
|
const { Playwright, quit } = await import('./browsers/playwright')
|
|
CurrentInterface = Playwright
|
|
browserQuit = quit
|
|
}
|
|
|
|
const browser = new CurrentInterface()
|
|
const browserName = process.env.BROWSER_NAME || 'chrome'
|
|
await browser.setup(browserName, locale)
|
|
;(global as any).browserName = browserName
|
|
|
|
const fullUrl = getFullUrl(
|
|
appPortOrUrl,
|
|
url,
|
|
isBrowserStack ? deviceIP : 'localhost'
|
|
)
|
|
|
|
console.log(`\n> Loading browser with ${fullUrl}\n`)
|
|
|
|
await browser.loadPage(fullUrl, { disableCache, beforePageLoad })
|
|
console.log(`\n> Loaded browser with ${fullUrl}\n`)
|
|
|
|
// Wait for application to hydrate
|
|
if (waitHydration) {
|
|
console.log(`\n> Waiting hydration for ${fullUrl}\n`)
|
|
|
|
const checkHydrated = async () => {
|
|
await browser.evalAsync(function () {
|
|
var callback = arguments[arguments.length - 1]
|
|
|
|
// if it's not a Next.js app return
|
|
if (
|
|
document.documentElement.innerHTML.indexOf('__NEXT_DATA__') === -1 &&
|
|
// @ts-ignore next exists on window if it's a Next.js page.
|
|
typeof ((window as any).next && (window as any).next.version) ===
|
|
'undefined'
|
|
) {
|
|
console.log('Not a next.js page, resolving hydrate check')
|
|
callback()
|
|
}
|
|
|
|
// TODO: should we also ensure router.isReady is true
|
|
// by default before resolving?
|
|
if ((window as any).__NEXT_HYDRATED) {
|
|
console.log('Next.js page already hydrated')
|
|
callback()
|
|
} else {
|
|
var timeout = setTimeout(callback, 10 * 1000)
|
|
;(window as any).__NEXT_HYDRATED_CB = function () {
|
|
clearTimeout(timeout)
|
|
console.log('Next.js hydrate callback fired')
|
|
callback()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
try {
|
|
await checkHydrated()
|
|
} catch (err) {
|
|
if (retryWaitHydration) {
|
|
// re-try in case the page reloaded during check
|
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
await checkHydrated()
|
|
} else {
|
|
console.error('failed to check hydration')
|
|
throw err
|
|
}
|
|
}
|
|
|
|
console.log(`\n> Hydration complete for ${fullUrl}\n`)
|
|
}
|
|
return browser
|
|
}
|