rsnext/test/e2e/app-dir/router-autoscroll/router-autoscroll.test.ts
Zack Tanner cb432eb42d
Fix scroll bailout logic when targeting fixed/sticky elements (#53873)
### What?
When navigating to a new page with fixed or sticky positioned element as the first element, we were bailing on scroll to top behavior, which often isn't expected.

### Why?
Currently, we decide to bail on scroll to top behavior on navigation if the content that is swapped into view is visible within the viewport. Since fixed/sticky positioned elements are often intended to be relative to the current viewport, it's most likely not the case that you'd want it to be considered in this heuristic. For example, if you were scrolled far down on a page, and you navigated to a page that makes use of a sticky header, you would not be scrolled to the top of the page because that sticky header is technically visible within the viewport. 

### How?
I've updated the previous implementation that was intended to skip targeting invisible elements to also skip over fixed or sticky elements. This should help by falling back to the next level of the layout tree to determine which element to scroll to.

I've deleted the `// TODO-APP` comments as I couldn't think of a scenario in which we'd need a global scrollTop handler -- if we've bailed on every element up the tree, it's likely the page wasn't scrollable.

Some additional considerations:
- Is the warning helpful or annoying?
- Is the parallel route trade-off an acceptable one? (ie, a parallel modal slot might not be considered in the content visibility check unless if it’s fixed positioned)

Closes NEXT-1393
Fixes #47475
2023-08-15 13:31:39 +00:00

235 lines
8.6 KiB
TypeScript

import webdriver from 'next-webdriver'
import { createNextDescribe } from 'e2e-utils'
import { check } from 'next-test-utils'
createNextDescribe(
'router autoscrolling on navigation',
{
files: __dirname,
},
({ next, isNextDev }) => {
type BrowserInterface = Awaited<ReturnType<typeof webdriver>>
const getTopScroll = async (browser: BrowserInterface) =>
await browser.eval('document.documentElement.scrollTop')
const getLeftScroll = async (browser: BrowserInterface) =>
await browser.eval('document.documentElement.scrollLeft')
const waitForScrollToComplete = (
browser,
options: { x: number; y: number }
) =>
check(async () => {
const top = await getTopScroll(browser)
const left = await getLeftScroll(browser)
return top === options.y && left === options.x
? 'success'
: JSON.stringify({ top, left })
}, 'success')
const scrollTo = async (
browser: BrowserInterface,
options: { x: number; y: number }
) => {
await browser.eval(`window.scrollTo(${options.x}, ${options.y})`)
await waitForScrollToComplete(browser, options)
}
describe('vertical scroll', () => {
it('should scroll to top of document when navigating between to pages without layout', async () => {
const browser = await webdriver(next.url, '/0/0/100/10000/page1')
await scrollTo(browser, { x: 0, y: 1000 })
expect(await getTopScroll(browser)).toBe(1000)
await browser.eval(`window.router.push("/0/0/100/10000/page2")`)
await waitForScrollToComplete(browser, { x: 0, y: 0 })
})
it("should scroll to top of page when scrolling to phe top of the document wouldn't have the page in the viewport", async () => {
const browser = await webdriver(next.url, '/0/1000/100/1000/page1')
await scrollTo(browser, { x: 0, y: 1500 })
expect(await getTopScroll(browser)).toBe(1500)
await browser.eval(`window.router.push("/0/1000/100/1000/page2")`)
await waitForScrollToComplete(browser, { x: 0, y: 1000 })
})
it("should scroll down to the navigated page when it's below viewort", async () => {
const browser = await webdriver(next.url, '/0/1000/100/1000/page1')
expect(await getTopScroll(browser)).toBe(0)
await browser.eval(`window.router.push("/0/1000/100/1000/page2")`)
await waitForScrollToComplete(browser, { x: 0, y: 1000 })
})
it('should not scroll when the top of the page is in the viewport', async () => {
const browser = await webdriver(next.url, '/10/1000/100/1000/page1')
await scrollTo(browser, { x: 0, y: 800 })
expect(await getTopScroll(browser)).toBe(800)
await browser.eval(`window.router.push("/10/1000/100/1000/page2")`)
await waitForScrollToComplete(browser, { x: 0, y: 800 })
})
it('should not scroll to top of document if page in viewport', async () => {
const browser = await webdriver(next.url, '/10/100/100/1000/page1')
await scrollTo(browser, { x: 0, y: 50 })
expect(await getTopScroll(browser)).toBe(50)
await browser.eval(`window.router.push("/10/100/100/1000/page2")`)
await waitForScrollToComplete(browser, { x: 0, y: 50 })
})
it('should scroll to top of document if possible while giving focus to page', async () => {
const browser = await webdriver(next.url, '/10/100/100/1000/page1')
await scrollTo(browser, { x: 0, y: 200 })
expect(await getTopScroll(browser)).toBe(200)
await browser.eval(`window.router.push("/10/100/100/1000/page2")`)
await waitForScrollToComplete(browser, { x: 0, y: 0 })
})
})
describe('horizontal scroll', () => {
it("should't scroll horizontally", async () => {
const browser = await webdriver(next.url, '/0/0/10000/10000/page1')
await scrollTo(browser, { x: 1000, y: 1000 })
expect(await getLeftScroll(browser)).toBe(1000)
expect(await getTopScroll(browser)).toBe(1000)
await browser.eval(`window.router.push("/0/0/10000/10000/page2")`)
await waitForScrollToComplete(browser, { x: 1000, y: 0 })
})
})
describe('router.refresh()', () => {
it('should not scroll when called alone', async () => {
const browser = await webdriver(next.url, '/10/10000/100/1000/page1')
await scrollTo(browser, { x: 0, y: 12000 })
expect(await getTopScroll(browser)).toBe(12000)
await browser.eval(`window.router.refresh()`)
await waitForScrollToComplete(browser, { x: 0, y: 12000 })
})
it('should not stop router.push() from scrolling', async () => {
const browser = await webdriver(next.url, '/10/10000/100/1000/page1')
await scrollTo(browser, { x: 0, y: 12000 })
expect(await getTopScroll(browser)).toBe(12000)
await browser.eval(`
window.React.startTransition(() => {
window.router.push('/10/10000/100/1000/page2')
window.router.refresh()
})
`)
await waitForScrollToComplete(browser, { x: 0, y: 10000 })
browser.quit()
})
// Test hot reloading only in development
;((global as any).isDev ? it : it.skip)(
'should not scroll the page when we hot reload',
async () => {
const browser = await webdriver(next.url, '/10/10000/100/1000/page1')
await scrollTo(browser, { x: 0, y: 12000 })
const pagePath =
'app/[layoutPaddingWidth]/[layoutPaddingHeight]/[pageWidth]/[pageHeight]/[param]/page.tsx'
await browser.eval(`window.router.refresh()`)
await next.patchFile(
pagePath,
(await next.readFile(pagePath)) +
`
\\\\ Add this meaningless comment to force refresh
`
)
await waitForScrollToComplete(browser, { x: 0, y: 12000 })
}
)
})
describe('bugs', () => {
it('Should scroll to the top of the layout when the first child is display none', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval('window.scrollTo(0, 500)')
await browser
.elementByCss('#to-invisible-first-element')
.click()
.waitForElementByCss('#content-that-is-visible')
await check(() => browser.eval('window.scrollY'), 0)
})
it('Should scroll to the top of the layout when the first child is position fixed', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval('window.scrollTo(0, 500)')
await browser
.elementByCss('#to-fixed-first-element')
.click()
.waitForElementByCss('#content-that-is-visible')
await check(() => browser.eval('window.scrollY'), 0)
if (isNextDev) {
// Check that we've logged a warning
await check(async () => {
const logs = await browser.log()
return logs.some((log) =>
log.message.includes(
'Skipping auto-scroll behavior due to `position: sticky` or `position: fixed` on element:'
)
)
? 'success'
: undefined
}, 'success')
}
})
it('Should scroll to the top of the layout when the first child is position sticky', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval('window.scrollTo(0, 500)')
await browser
.elementByCss('#to-sticky-first-element')
.click()
.waitForElementByCss('#content-that-is-visible')
await check(() => browser.eval('window.scrollY'), 0)
if (isNextDev) {
// Check that we've logged a warning
await check(async () => {
const logs = await browser.log()
return logs.some((log) =>
log.message.includes(
'Skipping auto-scroll behavior due to `position: sticky` or `position: fixed` on element:'
)
)
? 'success'
: undefined
}, 'success')
}
})
it('Should apply scroll when loading.js is used', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval('window.scrollTo(0, 500)')
await browser
.elementByCss('#to-loading-scroll')
.click()
.waitForElementByCss('#loading-component')
await check(() => browser.eval('window.scrollY'), 0)
await browser.waitForElementByCss('#content-that-is-visible')
await check(() => browser.eval('window.scrollY'), 0)
})
})
}
)