next.js/test/e2e/app-dir/app-root-params-getters/use-cache.test.ts
use-cache.test.ts320 lines13.4 KB
import { nextTestSetup } from 'e2e-utils'
import { join } from 'path'
import { assertNoConsoleErrors, retry, waitForNoRedbox } from 'next-test-utils'

describe('app-root-param-getters - cache - at runtime', () => {
  const { next, isNextDev, isNextDeploy } = nextTestSetup({
    files: join(__dirname, 'fixtures', 'use-cache-runtime'),
  })

  if (isNextDev) {
    it('should allow using root params within a "use cache" - dev', async () => {
      const browser = await next.browser('/en/us/use-cache')
      await waitForNoRedbox(browser)
      expect(await browser.elementById('param').text()).toBe('en us')
    })

    it('should error when using root params within `unstable_cache` - dev', async () => {
      const browser = await next.browser('/en/us/unstable_cache')
      await expect(browser).toDisplayRedbox(`
       {
         "code": "E1141",
         "description": "Route /[lang]/[countryCode]/unstable_cache used \`import('next/root-params').lang()\` inside \`unstable_cache\`. This is not supported. Use \`"use cache"\` instead.",
         "environmentLabel": "Server",
         "label": "Runtime Error",
         "source": "app/[lang]/[countryCode]/unstable_cache/page.tsx (33:28) @ uncachedGetParams
       > 33 |   return { lang: await lang(), countryCode: await countryCode() }
            |                            ^",
         "stack": [
           "uncachedGetParams app/[lang]/[countryCode]/unstable_cache/page.tsx (33:28)",
           "Runtime app/[lang]/[countryCode]/unstable_cache/page.tsx (17:22)",
         ],
       }
      `)
    })

    it('should error when using root params in "use cache" nested inside unstable_cache - dev', async () => {
      const browser = await next.browser('/en/us/nested-in-unstable_cache')
      await expect(browser).toDisplayRedbox(`
       {
         "code": "E1140",
         "description": "Route /[lang]/[countryCode]/nested-in-unstable_cache used \`import('next/root-params').lang()\` inside \`"use cache"\` nested within \`unstable_cache\`. Root params are not available in this context.",
         "environmentLabel": "Cache",
         "label": "Runtime Error",
         "source": "app/[lang]/[countryCode]/nested-in-unstable_cache/page.tsx (29:28) @ getCachedParams
       > 29 |   return { lang: await lang(), countryCode: await countryCode() }
            |                            ^",
         "stack": [
           "getCachedParams app/[lang]/[countryCode]/nested-in-unstable_cache/page.tsx (29:28)",
         ],
       }
      `)
    })
  } else {
    it('should allow using root params within a "use cache" - start', async () => {
      const browser = await next.browser('/en/us/use-cache')
      expect(await browser.elementById('param').text()).toBe('en us')
    })

    it('should create separate cache entries for different root params', async () => {
      const browser = await next.browser('/en/us/use-cache')
      expect(await browser.elementById('param').text()).toBe('en us')
      const enRandom = await browser.elementById('random').text()

      await browser.loadPage(next.url + '/fr/ca/use-cache')
      expect(await browser.elementById('param').text()).toBe('fr ca')
      const frRandom = await browser.elementById('random').text()

      // Different root params must produce different cache entries.
      expect(enRandom).not.toBe(frRandom)

      // Each entry must be individually cached (same random on revisit).
      await browser.loadPage(next.url + '/en/us/use-cache')
      expect(await browser.elementById('random').text()).toBe(enRandom)

      await browser.loadPage(next.url + '/fr/ca/use-cache')
      expect(await browser.elementById('random').text()).toBe(frRandom)
    })

    it('should resume with the same cached data that was prerendered', async () => {
      const browser = await next.browser('/en/us/use-cache-resume', {
        pushErrorAsConsoleLog: true,
      })

      expect(await browser.elementById('random').text()).toBeTruthy()

      await retry(async () => {
        expect(await browser.elementById('dynamic').text()).toBe('dynamic')
      })

      // If the Resume Data Cache lookup missed during the resume (e.g. due
      // to a key mismatch caused by root params), the "use cache" function
      // would re-execute and produce a different random value than the one
      // in the prerendered shell, causing a hydration error.
      await assertNoConsoleErrors(browser)
    })

    it('should handle conditional root param reads based on arguments', async () => {
      const browser = await next.browser('/en/us/maybe-reads-root-param')

      expect(await browser.elementById('with-lang-value').text()).toBe('en')
      expect(await browser.elementById('without-lang-value').text()).toBe(
        'null'
      )
      const enWithRandom = await browser.elementById('with-lang-random').text()
      const enWithoutRandom = await browser
        .elementById('without-lang-random')
        .text()

      await browser.loadPage(next.url + '/fr/ca/maybe-reads-root-param')

      expect(await browser.elementById('with-lang-value').text()).toBe('fr')
      expect(await browser.elementById('without-lang-value').text()).toBe(
        'null'
      )
      const frWithRandom = await browser.elementById('with-lang-random').text()
      const frWithoutRandom = await browser
        .elementById('without-lang-random')
        .text()

      // Different root params produce different cache entries.
      expect(frWithRandom).not.toBe(enWithRandom)

      // The without-lang call doesn't read lang, but after
      // knownRootParamsByFunctionId grows, the key includes lang too, so
      // different root params still produce different entries.
      expect(frWithoutRandom).not.toBe(enWithoutRandom)

      // Revisit to confirm cache hits.
      await browser.loadPage(next.url + '/en/us/maybe-reads-root-param')
      expect(await browser.elementById('with-lang-random').text()).toBe(
        enWithRandom
      )
      expect(await browser.elementById('without-lang-random').text()).toBe(
        enWithoutRandom
      )

      await browser.loadPage(next.url + '/fr/ca/maybe-reads-root-param')
      expect(await browser.elementById('with-lang-random').text()).toBe(
        frWithRandom
      )
      expect(await browser.elementById('without-lang-random').text()).toBe(
        frWithoutRandom
      )
    })

    it('should handle root param reads introduced after revalidation', async () => {
      const browser = await next.browser('/en/us/conditional-on-another-cache')

      // Reset the flag in case a previous test attempt left it enabled.
      if ((await browser.elementById('lang-value').text()) !== 'null') {
        await browser.elementById('disable-flag').click()
        await retry(async () => {
          expect(await browser.elementById('lang-value').text()).toBe('null')
        })
      }

      // Before the flag is enabled, lang is not read on any route.
      await browser.loadPage(next.url + '/fr/ca/conditional-on-another-cache')
      expect(await browser.elementById('lang-value').text()).toBe('null')

      // Enable the flag and revalidate (staying on the fr/ca page).
      await browser.elementById('enable-flag').click()

      await retry(async () => {
        expect(await browser.elementById('lang-value').text()).toBe('fr')
      })

      // Wait for the cache to settle by verifying the random value is
      // stable across two consecutive loads.
      let frRandom: string = ''
      await retry(async () => {
        await browser.loadPage(next.url + '/fr/ca/conditional-on-another-cache')
        frRandom = await browser.elementById('random').text()
        await browser.loadPage(next.url + '/fr/ca/conditional-on-another-cache')
        expect(await browser.elementById('random').text()).toBe(frRandom)
      })

      // Different root params must now produce different entries because
      // lang is being read.
      await browser.loadPage(next.url + '/en/us/conditional-on-another-cache')

      expect(await browser.elementById('lang-value').text()).toBe('en')
      const enRandom = await browser.elementById('random').text()
      expect(enRandom).not.toBe(frRandom)

      // Revisit fr/ca to confirm cache hit.
      await browser.loadPage(next.url + '/fr/ca/conditional-on-another-cache')
      expect(await browser.elementById('random').text()).toBe(frRandom)
    })

    it('should handle root param reads conditional on another root param value', async () => {
      // Visit en/us first — reads both lang and countryCode.
      const browser = await next.browser('/en/us/conditional-on-root-param')

      expect(await browser.elementById('lang-value').text()).toBe('en')
      expect(await browser.elementById('country-code-value').text()).toBe('us')
      const enUsRandom = await browser.elementById('random').text()

      // Visit en/gb — also reads both. Must produce a different entry because
      // countryCode differs.
      await browser.loadPage(next.url + '/en/gb/conditional-on-root-param')

      expect(await browser.elementById('lang-value').text()).toBe('en')
      expect(await browser.elementById('country-code-value').text()).toBe('gb')
      const enGbRandom = await browser.elementById('random').text()
      expect(enGbRandom).not.toBe(enUsRandom)

      // Visit fr/ca — only reads lang (not countryCode), but
      // knownRootParamsByFunctionId already includes countryCode from the en
      // visits. The key is more specific than needed, which is safe.
      await browser.loadPage(next.url + '/fr/ca/conditional-on-root-param')

      expect(await browser.elementById('lang-value').text()).toBe('fr')
      expect(await browser.elementById('country-code-value').text()).toBe(
        'null'
      )
      const frCaRandom = await browser.elementById('random').text()
      expect(frCaRandom).not.toBe(enUsRandom)
      expect(frCaRandom).not.toBe(enGbRandom)

      // Revisit all three to confirm cache hits.
      await browser.loadPage(next.url + '/en/us/conditional-on-root-param')
      expect(await browser.elementById('random').text()).toBe(enUsRandom)

      await browser.loadPage(next.url + '/en/gb/conditional-on-root-param')
      expect(await browser.elementById('random').text()).toBe(enGbRandom)

      await browser.loadPage(next.url + '/fr/ca/conditional-on-root-param')
      expect(await browser.elementById('random').text()).toBe(frCaRandom)
    })

    if (!isNextDeploy) {
      it('should error when using root params within `unstable_cache` - start', async () => {
        await next.render$('/en/us/unstable_cache')
        expect(next.cliOutput).toInclude(
          "Error: Route /[lang]/[countryCode]/unstable_cache used `import('next/root-params').lang()` inside `unstable_cache`"
        )
      })

      it('should error when using root params in "use cache" nested inside unstable_cache - start', async () => {
        await next.render$('/en/us/nested-in-unstable_cache')
        expect(next.cliOutput).toInclude(
          'Error: Route /[lang]/[countryCode]/nested-in-unstable_cache used `import(\'next/root-params\').lang()` inside `"use cache"` nested within `unstable_cache`. Root params are not available in this context.'
        )
      })
    }
  }
})

describe('app-root-param-getters - private cache', () => {
  const { next, isNextDev } = nextTestSetup({
    files: join(__dirname, 'fixtures', 'use-cache-private'),
  })

  if (isNextDev) {
    it('should allow using root params within a "use cache: private" - dev', async () => {
      const browser = await next.browser('/en/us/use-cache-private')

      await waitForNoRedbox(browser)
      expect(await browser.elementById('param').text()).toBe('en us')
    })
  } else {
    it('should allow using root params within a "use cache: private" - start', async () => {
      const browser = await next.browser('/en/us/use-cache-private')
      expect(await browser.elementById('param').text()).toBe('en us')
    })
  }
})

describe('app-root-param-getters - cache - at build', () => {
  const { next, isNextDev } = nextTestSetup({
    files: join(__dirname, 'fixtures', 'use-cache-build'),
  })

  if (isNextDev) {
    // we omit these tests in dev because they are duplicates semantically to the runtime fixture tested above
    it('noop in dev', () => {})
  } else {
    it('should allow using root params within a "use cache" at build time', async () => {
      const browser = await next.browser('/en/us/use-cache')
      expect(await browser.elementById('param').text()).toBe('en us')

      await browser.loadPage(next.url + '/es/es/use-cache')
      expect(await browser.elementById('param').text()).toBe('es es')
    })
  }
})

describe('app-root-param-getters - cache dedup with root params', () => {
  const { next, skipped } = nextTestSetup({
    files: join(__dirname, 'fixtures', 'use-cache-dedup'),
    // In deploy mode, concurrent requests could hit different lambdas.
    skipDeployment: true,
  })

  if (skipped) return

  it('should dedupe same root params and isolate different root params', async () => {
    // Three concurrent requests: ca/en, ca/fr, ca/fr.
    const [$en, $fr1, $fr2] = await Promise.all([
      next.render$('/ca/en'),
      next.render$('/ca/fr'),
      next.render$('/ca/fr'),
    ])

    const randomEn = $en('#random').text()
    const randomFr1 = $fr1('#random').text()
    const randomFr2 = $fr2('#random').text()

    expect(randomEn).toBeTruthy()
    expect(randomFr1).toBeTruthy()

    // ca/en and ca/fr should have different results (isolation).
    expect(randomEn).not.toBe(randomFr1)

    // Both ca/fr requests should have the same result (deduped).
    expect(randomFr1).toBe(randomFr2)
  })
})
Quest for Codev2.0.0
/
SIGN IN