next.js/test/e2e/app-dir/router-autoscroll/router-autoscroll.test.ts
router-autoscroll.test.ts294 lines10.3 KB
import type { Playwright } from 'next-webdriver'
import { nextTestSetup } from 'e2e-utils'
import { check, assertNoConsoleErrors, retry } from 'next-test-utils'

const enableNewScrollHandler =
  process.env.__NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER === 'true'

describe('router autoscrolling on navigation', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  const getTopScroll = async (browser: Playwright) =>
    await browser.eval('document.documentElement.scrollTop')

  const getLeftScroll = async (browser: Playwright) =>
    await browser.eval('document.documentElement.scrollLeft')

  const waitForScrollToComplete = async (
    browser: Playwright,
    options: { x: number; y: number }
  ) => {
    await retry(async function expectScrolledTo() {
      const top = await getTopScroll(browser)
      const left = await getLeftScroll(browser)
      expect({ top, left }).toEqual({ top: options.y, left: options.x })
    })
    await assertNoConsoleErrors(browser)
  }

  const scrollTo = async (
    browser: Playwright,
    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 next.browser('/0/0/100/10000/page1')

      await scrollTo(browser, { x: 0, y: 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 next.browser('/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 next.browser('/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 next.browser('/10/1000/100/1000/page1')

      await scrollTo(browser, { x: 0, y: 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 next.browser('/10/100/100/1000/page1')

      await scrollTo(browser, { x: 0, y: 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 next.browser('/10/100/100/1000/page1')

      await scrollTo(browser, { x: 0, y: 200 })

      await browser.eval(`window.router.push("/10/100/100/1000/page2")`)
      await waitForScrollToComplete(browser, { x: 0, y: 0 })
    })

    it('should scroll to top of document with new metadata', async () => {
      const browser = await next.browser('/')

      // scroll to bottom
      await browser.eval(
        `window.scrollTo(0, ${await browser.eval('document.documentElement.scrollHeight')})`
      )
      // Just need to scroll by something
      expect(await getTopScroll(browser)).toBeGreaterThan(0)

      await browser.elementByCss('[href="/new-metadata"]').click()
      expect(
        await browser.eval('document.documentElement.scrollHeight')
      ).toBeGreaterThan(0)
      await waitForScrollToComplete(browser, { x: 0, y: 0 })
    })
  })

  describe('horizontal scroll', () => {
    it("should't scroll horizontally", async () => {
      const browser = await next.browser('/0/0/10000/10000/page1')

      await scrollTo(browser, { x: 1000, y: 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 next.browser('/10/10000/100/1000/page1')

      await scrollTo(browser, { x: 0, y: 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 next.browser('/10/10000/100/1000/page1')

      await scrollTo(browser, { x: 0, y: 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.close()
    })

    // Test hot reloading only in development
    ;(isNextDev ? it : it.skip)(
      'should not scroll the page when we hot reload',
      async () => {
        const browser = await next.browser('/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()`)
        let originalContent: string
        await next.patchFile(pagePath, (content) => {
          originalContent = content
          return (
            content +
            `
      // Add this meaningless comment to force refresh
      `
          )
        })
        await waitForScrollToComplete(browser, { x: 0, y: 12000 })
        await next.patchFile(pagePath, originalContent)
      }
    )
  })

  describe('server action refresh', () => {
    it('should not scroll when refresh() is called from a server action', async () => {
      const browser = await next.browser('/server-action-refresh')

      const initialTimestamp = await browser
        .elementByCss('#server-timestamp')
        .text()

      // Scroll down past the first spacer div
      await scrollTo(browser, { x: 0, y: 1000 })

      // Click the refresh button which calls refresh() via a server action
      await browser.elementByCss('#refresh-button').click()

      // Wait for the action to complete by checking the server timestamp
      await retry(async () => {
        const newTimestamp = await browser
          .elementByCss('#server-timestamp')
          .text()
        expect(newTimestamp).not.toBe(initialTimestamp)
      })

      // Scroll position should be preserved
      await waitForScrollToComplete(browser, { x: 0, y: 1000 })
    })
  })

  describe('bugs', () => {
    it('Should scroll to the top of the layout when the first child is display none', async () => {
      const browser = await next.browser('/')
      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 next.browser('/')
      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)
    })

    it('Should scroll to the top of the layout when the first child is position sticky', async () => {
      const browser = await next.browser('/')
      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)
    })

    it('Should apply scroll when loading.js is used', async () => {
      const browser = await next.browser('/')
      await browser.eval('window.scrollTo(0, 500)')
      await browser.elementByCss('#to-loading-scroll').click()
      await browser.waitForElementByCss('#loading-component')
      await check(() => browser.eval('window.scrollY'), 0)
      await browser.waitForElementByCss('#content-that-is-visible')
      await check(() => browser.eval('window.scrollY'), 0)
    })

    it('should scroll to top when navigating to same page with different search params', async () => {
      const browser = await next.browser('/loading-scroll?skipSleep=1')

      await retry(async () => {
        // scroll to the links at the bottom of the page
        await browser.eval(`document.getElementById("pages").scrollIntoView()`)

        // grab the current scroll position
        const scrollY = await browser.eval(`window.scrollY`)

        // sanity check: we should not be scrolled to the top
        expect(scrollY).not.toBe(0)
      })

      // click a link
      await browser.elementByCss("a[href='?page=2&skipSleep=1']").click()

      // assert the new page id has been committed
      expect(await browser.elementById('current-page').text()).toBe('2')

      await retry(async () => {
        // we should have scrolled to the top
        expect(await browser.eval(`window.scrollY`)).toBe(0)
      })
    })
  })

  it('should scroll to top even if React hoists children', async () => {
    const browser = await next.browser('/')

    // scroll to bottom
    await browser.eval(
      `window.scrollTo(0, ${await browser.eval('document.documentElement.scrollHeight')})`
    )
    // Just need to scroll by something
    expect(await getTopScroll(browser)).toBeGreaterThan(0)

    await browser.elementByCss('[href="/hoisted"]').click()
    expect(
      await browser.eval('document.documentElement.scrollHeight')
    ).toBeGreaterThan(0)
    if (enableNewScrollHandler) {
      await waitForScrollToComplete(browser, { x: 0, y: 0 })
    } else {
      await expect(
        waitForScrollToComplete(browser, { x: 0, y: 0 })
      ).rejects.toThrow()
    }
  })
})
Quest for Codev2.0.0
/
SIGN IN