2024-01-11 10:23:20 +01:00
|
|
|
import { getFullUrl, waitFor } from 'next-test-utils'
|
2021-09-13 14:36:25 +02:00
|
|
|
import os from 'os'
|
|
|
|
import { BrowserInterface } from './browsers/base'
|
|
|
|
|
2021-10-25 08:21:57 +02:00
|
|
|
if (!process.env.TEST_FILE_PATH) {
|
|
|
|
process.env.TEST_FILE_PATH = module.parent.filename
|
|
|
|
}
|
|
|
|
|
2021-09-13 14:36:25 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-14 20:14:24 +01:00
|
|
|
let browserTeardown: (() => Promise<void>)[] = []
|
|
|
|
let browserQuit: (() => Promise<void>) | undefined
|
2021-09-13 14:36:25 +02:00
|
|
|
|
|
|
|
if (typeof afterAll === 'function') {
|
|
|
|
afterAll(async () => {
|
2024-02-14 20:14:24 +01:00
|
|
|
await Promise.all(browserTeardown.map((f) => f())).catch((e) =>
|
|
|
|
console.error('browser teardown', e)
|
|
|
|
)
|
|
|
|
|
2021-09-13 14:36:25 +02:00
|
|
|
if (browserQuit) {
|
|
|
|
await browserQuit()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
2022-05-20 21:41:11 +02:00
|
|
|
* @param appPortOrUrl can either be the port or the full URL
|
2021-09-13 14:36:25 +02:00
|
|
|
* @param url the path/query to append when using appPort
|
2023-06-23 19:42:50 +02:00
|
|
|
* @param options
|
|
|
|
* @param options.waitHydration whether to wait for React hydration to finish
|
2022-02-15 19:53:45 +01:00
|
|
|
* @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
|
2023-06-23 19:42:50 +02:00
|
|
|
* @param options.locale browser locale
|
|
|
|
* @param options.disableJavaScript disable javascript
|
2023-09-26 07:35:06 +02:00
|
|
|
* @param options.ignoreHttpsErrors ignore https errors
|
2021-09-13 14:36:25 +02:00
|
|
|
* @returns thenable browser instance
|
|
|
|
*/
|
|
|
|
export default async function webdriver(
|
|
|
|
appPortOrUrl: string | number,
|
|
|
|
url: string,
|
2022-02-15 19:53:45 +01:00
|
|
|
options?: {
|
|
|
|
waitHydration?: boolean
|
|
|
|
retryWaitHydration?: boolean
|
|
|
|
disableCache?: boolean
|
|
|
|
beforePageLoad?: (page: any) => void
|
2022-04-07 13:39:49 +02:00
|
|
|
locale?: string
|
2023-05-04 07:03:09 +02:00
|
|
|
disableJavaScript?: boolean
|
2023-08-16 19:55:06 +02:00
|
|
|
headless?: boolean
|
2023-09-26 07:35:06 +02:00
|
|
|
ignoreHTTPSErrors?: boolean
|
fix inconsistent scroll restoration behavior (#59366)
### What?
While scrolled on a page, and when following a link to a new page and
clicking the browser back button or using `router.back()`, the scroll
position would sometimes restore scroll to the incorrect spot (in the
case of the test added in this PR, it'd scroll you back to the top of
the list)
### Why?
The refactor in #56497 changed the way router actions are processed:
specifically, all actions were assumed to be async, even if they could
be handled synchronously. For most actions this is fine, as most are
currently async. However, `ACTION_RESTORE` (triggered when the
`popstate` event occurs) isn't async, and introducing a small amount of
delay in the handling of this action can cause the browser to not
properly restore the scroll position
### How?
This special-cases `ACTION_RESTORE` to synchronously process the action
and call `setState` when it's received, rather than creating a promise.
To consistently reproduce this behavior, I added an option to our
browser interface that'll allow us to programmatically trigger a CPU
slowdown.
h/t to @alvarlagerlof for isolating the offending commit and sharing a
minimal reproduction.
Closes NEXT-1819
Likely addresses #58899 but the reproduction was too complex to verify.
2023-12-07 20:17:15 +01:00
|
|
|
cpuThrottleRate?: number
|
2024-01-17 12:33:45 +01:00
|
|
|
pushErrorAsConsoleLog?: boolean
|
2022-02-15 19:53:45 +01:00
|
|
|
}
|
2021-09-13 14:36:25 +02:00
|
|
|
): Promise<BrowserInterface> {
|
2023-06-23 19:42:50 +02:00
|
|
|
let CurrentInterface: new () => BrowserInterface
|
2021-09-13 14:36:25 +02:00
|
|
|
|
2022-02-15 19:53:45 +01:00
|
|
|
const defaultOptions = {
|
|
|
|
waitHydration: true,
|
|
|
|
retryWaitHydration: false,
|
|
|
|
disableCache: false,
|
|
|
|
}
|
|
|
|
options = Object.assign(defaultOptions, options)
|
2022-04-07 13:39:49 +02:00
|
|
|
const {
|
|
|
|
waitHydration,
|
|
|
|
retryWaitHydration,
|
|
|
|
disableCache,
|
|
|
|
beforePageLoad,
|
|
|
|
locale,
|
2023-05-04 07:03:09 +02:00
|
|
|
disableJavaScript,
|
2023-09-26 07:35:06 +02:00
|
|
|
ignoreHTTPSErrors,
|
2023-08-16 19:55:06 +02:00
|
|
|
headless,
|
fix inconsistent scroll restoration behavior (#59366)
### What?
While scrolled on a page, and when following a link to a new page and
clicking the browser back button or using `router.back()`, the scroll
position would sometimes restore scroll to the incorrect spot (in the
case of the test added in this PR, it'd scroll you back to the top of
the list)
### Why?
The refactor in #56497 changed the way router actions are processed:
specifically, all actions were assumed to be async, even if they could
be handled synchronously. For most actions this is fine, as most are
currently async. However, `ACTION_RESTORE` (triggered when the
`popstate` event occurs) isn't async, and introducing a small amount of
delay in the handling of this action can cause the browser to not
properly restore the scroll position
### How?
This special-cases `ACTION_RESTORE` to synchronously process the action
and call `setState` when it's received, rather than creating a promise.
To consistently reproduce this behavior, I added an option to our
browser interface that'll allow us to programmatically trigger a CPU
slowdown.
h/t to @alvarlagerlof for isolating the offending commit and sharing a
minimal reproduction.
Closes NEXT-1819
Likely addresses #58899 but the reproduction was too complex to verify.
2023-12-07 20:17:15 +01:00
|
|
|
cpuThrottleRate,
|
2024-01-17 12:33:45 +01:00
|
|
|
pushErrorAsConsoleLog,
|
2022-04-07 13:39:49 +02:00
|
|
|
} = options
|
2022-02-15 19:53:45 +01:00
|
|
|
|
2021-09-13 14:36:25 +02:00
|
|
|
// we import only the needed interface
|
2024-02-14 20:14:24 +01:00
|
|
|
if (
|
2024-01-10 10:31:32 +01:00
|
|
|
process.env.RECORD_REPLAY === 'true' ||
|
|
|
|
process.env.RECORD_REPLAY === '1'
|
|
|
|
) {
|
2022-09-29 23:45:10 +02:00
|
|
|
const { Replay, quit } = await require('./browsers/replay')
|
|
|
|
CurrentInterface = Replay
|
|
|
|
browserQuit = quit
|
2021-09-13 14:36:25 +02:00
|
|
|
} else {
|
2022-08-17 01:29:55 +02:00
|
|
|
const { Playwright, quit } = await import('./browsers/playwright')
|
|
|
|
CurrentInterface = Playwright
|
|
|
|
browserQuit = quit
|
2021-09-13 14:36:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const browser = new CurrentInterface()
|
|
|
|
const browserName = process.env.BROWSER_NAME || 'chrome'
|
2023-08-16 19:55:06 +02:00
|
|
|
await browser.setup(
|
|
|
|
browserName,
|
|
|
|
locale,
|
|
|
|
!disableJavaScript,
|
2023-09-26 07:35:06 +02:00
|
|
|
ignoreHTTPSErrors,
|
2023-08-16 19:55:06 +02:00
|
|
|
// allow headless to be overwritten for a particular test
|
|
|
|
typeof headless !== 'undefined' ? headless : !!process.env.HEADLESS
|
|
|
|
)
|
2021-09-13 14:36:25 +02:00
|
|
|
;(global as any).browserName = browserName
|
|
|
|
|
|
|
|
const fullUrl = getFullUrl(
|
|
|
|
appPortOrUrl,
|
|
|
|
url,
|
|
|
|
isBrowserStack ? deviceIP : 'localhost'
|
|
|
|
)
|
|
|
|
|
|
|
|
console.log(`\n> Loading browser with ${fullUrl}\n`)
|
|
|
|
|
fix inconsistent scroll restoration behavior (#59366)
### What?
While scrolled on a page, and when following a link to a new page and
clicking the browser back button or using `router.back()`, the scroll
position would sometimes restore scroll to the incorrect spot (in the
case of the test added in this PR, it'd scroll you back to the top of
the list)
### Why?
The refactor in #56497 changed the way router actions are processed:
specifically, all actions were assumed to be async, even if they could
be handled synchronously. For most actions this is fine, as most are
currently async. However, `ACTION_RESTORE` (triggered when the
`popstate` event occurs) isn't async, and introducing a small amount of
delay in the handling of this action can cause the browser to not
properly restore the scroll position
### How?
This special-cases `ACTION_RESTORE` to synchronously process the action
and call `setState` when it's received, rather than creating a promise.
To consistently reproduce this behavior, I added an option to our
browser interface that'll allow us to programmatically trigger a CPU
slowdown.
h/t to @alvarlagerlof for isolating the offending commit and sharing a
minimal reproduction.
Closes NEXT-1819
Likely addresses #58899 but the reproduction was too complex to verify.
2023-12-07 20:17:15 +01:00
|
|
|
await browser.loadPage(fullUrl, {
|
|
|
|
disableCache,
|
|
|
|
cpuThrottleRate,
|
|
|
|
beforePageLoad,
|
2024-01-17 12:33:45 +01:00
|
|
|
pushErrorAsConsoleLog,
|
fix inconsistent scroll restoration behavior (#59366)
### What?
While scrolled on a page, and when following a link to a new page and
clicking the browser back button or using `router.back()`, the scroll
position would sometimes restore scroll to the incorrect spot (in the
case of the test added in this PR, it'd scroll you back to the top of
the list)
### Why?
The refactor in #56497 changed the way router actions are processed:
specifically, all actions were assumed to be async, even if they could
be handled synchronously. For most actions this is fine, as most are
currently async. However, `ACTION_RESTORE` (triggered when the
`popstate` event occurs) isn't async, and introducing a small amount of
delay in the handling of this action can cause the browser to not
properly restore the scroll position
### How?
This special-cases `ACTION_RESTORE` to synchronously process the action
and call `setState` when it's received, rather than creating a promise.
To consistently reproduce this behavior, I added an option to our
browser interface that'll allow us to programmatically trigger a CPU
slowdown.
h/t to @alvarlagerlof for isolating the offending commit and sharing a
minimal reproduction.
Closes NEXT-1819
Likely addresses #58899 but the reproduction was too complex to verify.
2023-12-07 20:17:15 +01:00
|
|
|
})
|
2021-09-13 14:36:25 +02:00
|
|
|
console.log(`\n> Loaded browser with ${fullUrl}\n`)
|
|
|
|
|
2024-02-14 20:14:24 +01:00
|
|
|
browserTeardown.push(browser.close.bind(browser))
|
|
|
|
|
2021-09-13 14:36:25 +02:00
|
|
|
// 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 (
|
2023-05-02 02:07:04 +02:00
|
|
|
!document.documentElement.innerHTML.includes('__NEXT_DATA__') &&
|
2022-09-06 19:29:09 +02:00
|
|
|
// @ts-ignore next exists on window if it's a Next.js page.
|
2022-09-06 21:17:54 +02:00
|
|
|
typeof ((window as any).next && (window as any).next.version) ===
|
|
|
|
'undefined'
|
2021-09-13 14:36:25 +02:00
|
|
|
) {
|
|
|
|
console.log('Not a next.js page, resolving hydrate check')
|
|
|
|
callback()
|
|
|
|
}
|
|
|
|
|
2021-10-23 10:21:44 +02:00
|
|
|
// TODO: should we also ensure router.isReady is true
|
|
|
|
// by default before resolving?
|
2021-09-13 14:36:25 +02:00
|
|
|
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`)
|
|
|
|
}
|
2024-01-11 10:23:20 +01:00
|
|
|
|
|
|
|
// This is a temporary workaround for turbopack starting watching too late.
|
|
|
|
// So we delay file changes by 500ms to give it some time
|
|
|
|
// to connect the WebSocket and start watching.
|
|
|
|
if (process.env.TURBOPACK) {
|
|
|
|
await waitFor(1000)
|
|
|
|
}
|
2021-09-13 14:36:25 +02:00
|
|
|
return browser
|
|
|
|
}
|
2024-02-14 20:14:24 +01:00
|
|
|
|
|
|
|
export { BrowserInterface }
|