next.js/test/e2e/app-dir/use-cache-search-params/use-cache-search-params.test.ts
use-cache-search-params.test.ts239 lines8.5 KB
import { nextTestSetup } from 'e2e-utils'
import {
  waitForRedbox,
  assertNoConsoleErrors,
  waitForNoRedbox,
  getRedboxDescription,
  getRedboxSource,
} from 'next-test-utils'
import stripAnsi from 'strip-ansi'

const getExpectedErrorMessage = (route: string) =>
  `Route ${route} used \`searchParams\` inside "use cache". Accessing dynamic request data inside a cache scope is not supported. If you need some search params inside a cached function await \`searchParams\` outside of the cached function and pass only the required search params as arguments to the cached function. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache`

describe('use-cache-search-params', () => {
  const { next, isNextDev, skipped } = nextTestSetup({
    files: __dirname,
    skipDeployment: true,
    skipStart: process.env.NEXT_TEST_MODE !== 'dev',
  })

  if (skipped) {
    return
  }

  if (isNextDev) {
    let route: string

    describe('when searchParams are used inside of "use cache"', () => {
      beforeAll(() => {
        route = '/search-params-used'
      })

      it('should show an error', async () => {
        const outputIndex = next.cliOutput.length
        const browser = await next.browser(`${route}?foo=1`)

        await waitForRedbox(browser)

        const errorDescription = await getRedboxDescription(browser)
        const errorSource = await getRedboxSource(browser)
        const expectedErrorMessage = getExpectedErrorMessage(route)

        expect(errorDescription).toBe(expectedErrorMessage)

        const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))

        expect(errorSource).toMatchInlineSnapshot(`
         "app/search-params-used/page.tsx (8:17) @ Page

            6 |   searchParams: Promise<{ [key: string]: string | string[] | undefined }>
            7 | }) {
         >  8 |   const param = (await searchParams).foo
              |                 ^
            9 |
           10 |   return <p>param: {param}</p>
           11 | }"
        `)

        expect(cliOutput).toContain(`Error: ${expectedErrorMessage}
    at Page (app/search-params-used/page.tsx:8:17)`)
      })
    })

    describe('when searchParams are caught inside of "use cache"', () => {
      beforeAll(() => {
        route = '/search-params-caught'
      })

      it('should show an error', async () => {
        const outputIndex = next.cliOutput.length
        const browser = await next.browser(`${route}?foo=1`)

        await waitForRedbox(browser)

        const errorDescription = await getRedboxDescription(browser)
        const errorSource = await getRedboxSource(browser)
        const expectedErrorMessage = getExpectedErrorMessage(route)

        expect(errorDescription).toBe(expectedErrorMessage)

        const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))

        expect(errorSource).toMatchInlineSnapshot(`
         "app/search-params-caught/page.tsx (11:5) @ Page

            9 |
           10 |   try {
         > 11 |     param = (await searchParams).foo
              |     ^
           12 |   } catch {}
           13 |
           14 |   return <p>param: {param}</p>"
        `)

        expect(cliOutput).toContain(`Error: ${expectedErrorMessage}
    at Page (app/search-params-caught/page.tsx:11:5)`)
      })

      it('should also show an error after the second reload', async () => {
        // There was an obscure bug that lead to the error not being triggered
        // anymore starting with the third request. We test this scenario
        // explicitly to ensure we won't regress.
        const browser = await next.browser(`${route}?foo=1`)
        await browser.refresh()
        await browser.refresh()

        await waitForRedbox(browser)

        const errorDescription = await getRedboxDescription(browser)

        expect(errorDescription).toBe(getExpectedErrorMessage(route))
      })
    })

    describe('when searchParams are unused inside of "use cache"', () => {
      beforeAll(() => {
        route = '/search-params-unused'
      })

      it('should not show an error', async () => {
        const outputIndex = next.cliOutput.length
        const browser = await next.browser(`${route}?foo=1`)

        await waitForNoRedbox(browser)

        const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))

        expect(cliOutput).not.toContain(getExpectedErrorMessage(route))
      })
    })

    it('should show an error when searchParams are used inside of a cached generateMetadata', async () => {
      const browser = await next.browser(
        '/search-params-used-generate-metadata?title=foo'
      )

      await expect(browser).toDisplayRedbox(`
       {
         "code": "E842",
         "description": "Route /search-params-used-generate-metadata used \`searchParams\` inside "use cache". Accessing dynamic request data inside a cache scope is not supported. If you need some search params inside a cached function await \`searchParams\` outside of the cached function and pass only the required search params as arguments to the cached function. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache",
         "environmentLabel": null,
         "label": "Runtime Error",
         "source": "app/search-params-used-generate-metadata/page.tsx (9:17) @ generateMetadata
       >  9 |   const title = (await searchParams).title
            |                 ^",
         "stack": [
           "generateMetadata app/search-params-used-generate-metadata/page.tsx (9:17)",
         ],
       }
      `)
    })

    it('should show an error when searchParams are used inside of a cached generateViewport', async () => {
      const browser = await next.browser(
        '/search-params-used-generate-viewport?color=red'
      )

      await expect(browser).toDisplayRedbox(`
       {
         "code": "E842",
         "description": "Route /search-params-used-generate-viewport used \`searchParams\` inside "use cache". Accessing dynamic request data inside a cache scope is not supported. If you need some search params inside a cached function await \`searchParams\` outside of the cached function and pass only the required search params as arguments to the cached function. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache",
         "environmentLabel": null,
         "label": "Runtime Error",
         "source": "app/search-params-used-generate-viewport/page.tsx (9:17) @ generateViewport
       >  9 |   const color = (await searchParams).color
            |                 ^",
         "stack": [
           "generateViewport app/search-params-used-generate-viewport/page.tsx (9:17)",
         ],
       }
      `)
    })
  } else {
    afterEach(async () => {
      await next.stop()
    })

    it('should fail the build with errors', async () => {
      const { cliOutput } = await next.build()

      expect(cliOutput).toInclude(
        getExpectedErrorMessage('/search-params-used')
      )

      expect(cliOutput).toInclude(
        getExpectedErrorMessage('/search-params-caught')
      )

      expect(cliOutput).not.toInclude(
        getExpectedErrorMessage('/search-params-unused')
      )

      expect(cliOutput).toInclude(
        'Error occurred prerendering page "/search-params-used"'
      )

      expect(cliOutput).toInclude(
        'Error occurred prerendering page "/search-params-caught"'
      )

      expect(cliOutput).not.toInclude(
        'Error occurred prerendering page "/search-params-unused"'
      )
    })

    it('should resume a cached page that does not access search params without hydration errors', async () => {
      await next.build({
        args: ['--debug-build-paths', 'app/search-params-unused/page.tsx'],
      })

      await next.start({ skipBuild: true })

      let browser = await next.browser('/search-params-unused', {
        disableJavaScript: true,
      })

      const prerenderedPageDate = await browser.elementById('page-date').text()

      await browser.close()

      browser = await next.browser('/search-params-unused', {
        pushErrorAsConsoleLog: true,
      })

      // After hydration, the resumed page date should be the prerendered date.
      // Note: When cacheComponents is not enabled, the page is not actually
      // prerendered, but because the page is cached on the first page load, the
      // date should still be the same for the second page load.
      expect(await browser.elementById('page-date').text()).toBe(
        prerenderedPageDate
      )

      // There should also be no hydration errors due to a buildtime date being
      // replaced by a new runtime date.
      await assertNoConsoleErrors(browser)
    })
  }
})
Quest for Codev2.0.0
/
SIGN IN