Commit Graph

70 Commits

Author SHA1 Message Date
Jamiboy Mohammad dee811ff1c test: enable verified caching deploy tests (#98523)
## Summary

Enable the same 24 previously selected deployment-test scopes across 21
caching test files, now in a stack rooted on canary. Remove 24
`@force-gate !deploy` directives and their associated TODO comments,
which are already present on canary. Other mode, bundler, middleware,
and Cache Components exclusions remain in place.

This preserves the selection with passing evidence from the previous
deployment runs. No additional candidate scopes are enabled; excluded
variants are not counted as deployment coverage.

## Verification

- All selected test registration names and assertion bodies match the
previous enabled revision, checked by AST comparison.
- Verified that the canary diff contains only the inventoried exclusions
and their obsolete skip plumbing; other exclusions are preserved.
- Formatting and lint passed; 77 gate infrastructure unit tests passed.
- Full local bootstrap was blocked by missing package-level dependencies
in the temporary worktree. Fresh deployment execution on these rewritten
commits remains to be verified in CI.

<details>
<summary>Preserved scope inventory (24)</summary>

- ID 1:
`test/e2e/app-dir/app-client-cache/client-cache.original.test.ts` —
`describe('app dir client cache semantics (30s/5min)', () => {
  const { next, isNextDev } = nextTestSetup({
    files: path.join(__dirname, 'fixtures', 'regular'),
    nextConfig: {
      experimental: { staleTimes: { dynamic: 30, static: 180 } },
    },
  })

  if (isNextDev) {
// dev doesn't support prefetch={true}, so this just performs a basic
test to make sure data is reused for 30s
it('should renew the 30s cache once the data is revalidated', async ()
=> {
      let browser = await next.browser('/', browserConfigWithFixedTime)

      // navigate to prefetch-auto page
      await browser.elementByCss('[href="/1"]').click()
      await browser.waitForElementByCss('#random-number')

let initialNumber = await browser.elementById('random-number').text()

// Navigate back to the index, and then back to the prefetch-auto page
      await browser.elementByCss('[href="/"]').click()
      await browser.waitForElementByCss('[href="/1"]')
      await browser.eval(fastForwardTo, 5 * 1000)
      await browser.elementByCss('[href="/1"]').click()
      await browser.waitForElementByCss('#random-number')

      let newNumber = await browser.elementById('random-number').text()

      // the number should be the same, as we navigated within 30s.
      expect(newNumber).toBe(initialNumber)

      // Fast forward to expire the cache
      await browser.eval(fastForwardTo, 30 * 1000)

// Navigate back to the index, and then back to the prefetch-auto page
      await browser.elementByCss('[href="/"]').click()
      await browser.waitForElementByCss('[href="/1"]')
      await browser.elementByCss('[href="/1"]').click()
      await browser.waitForElementByCss('#random-number')

      newNumber = await browser.elementById('random-number').text()

// ~35s have passed, so the cache should be expired and the number
should be different
      expect(newNumber).not.toBe(initialNumber)

// once the number is updated, we should have a renewed 30s cache for
this entry
      // store this new number so we can check that it stays the same
      initialNumber = newNumber

      await browser.eval(fastForwardTo, 5 * 1000)

// Navigate back to the index, and then back to the prefetch-auto page
      await browser.elementByCss('[href="/"]').click()
      await browser.waitForElementByCss('[href="/1"]')
      await browser.elementByCss('[href="/1"]').click()
      await browser.waitForElementByCss('#random-number')

      newNumber = await browser.elementById('random-number').text()

// the number should be the same, as we navigated within 30s (part 2).
      expect(newNumber).toBe(initialNumber)
    })
  } else {
    describe('prefetch={true}', () => {
      let browser: Playwright

      beforeEach(async () => {
        browser = await next.browser('/', browserConfigWithFixedTime)
      })

      it('should prefetch the full page', async () => {
        const { getRequests, clearRequests } =
          await createRequestsListener(browser)
        await retry(() => {
          expect(
            getRequests().some(
              ([url, didPartialPrefetch]) =>
                getPathname(url) === '/0' && !didPartialPrefetch
            )
          ).toBe(true)
        })

        clearRequests()

        await browser.elementByCss('[href="/0?timeout=0"]').click()
        await browser.waitForElementByCss('#random-number')

        await retry(() => {
          const requests = getRequests()
expect(requests.every(([url]) => getPathname(url) !== '/0')).toBe(
            true
          )
        })
      })
it('should re-use the cache for the full page, only for 5 mins', async
() => {
        await browser.elementByCss('[href="/0?timeout=0"]').click()
        await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()

        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/0?timeout=0"]')

        await browser.elementByCss('[href="/0?timeout=0"]').click()
        await browser.waitForElementByCss('#random-number')
        const number = await browser.elementById('random-number').text()

        expect(number).toBe(randomNumber)

        await browser.eval(fastForwardTo, 5 * 60 * 1000)

        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/0?timeout=0"]')

        await browser.elementByCss('[href="/0?timeout=0"]').click()
        await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()

        expect(newNumber).not.toBe(randomNumber)
      })

it('should prefetch again after 5 mins if the link is visible again',
async () => {
        const { getRequests, clearRequests } =
          await createRequestsListener(browser)

        await retry(() => {
          expect(
            getRequests().some(
              ([url, didPartialPrefetch]) =>
                getPathname(url) === '/0' && !didPartialPrefetch
            )
          ).toBe(true)
        })

        await browser.elementByCss('[href="/0?timeout=0"]').click()
        await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()

        await browser.eval(fastForwardTo, 5 * 60 * 1000)
        clearRequests()

        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/0?timeout=0"]')

        await retry(() => {
          expect(
            getRequests().some(
              ([url, didPartialPrefetch]) =>
                getPathname(url) === '/0' && !didPartialPrefetch
            )
          ).toBe(true)
        })

        await browser.elementByCss('[href="/0?timeout=0"]').click()
        await browser.waitForElementByCss('#random-number')
        const number = await browser.elementById('random-number').text()

        expect(number).not.toBe(randomNumber)
      })
    })
    describe('prefetch={false}', () => {
      let browser: Playwright

      beforeEach(async () => {
        browser = await next.browser('/', browserConfigWithFixedTime)
      })
      it('should not prefetch the page at all', async () => {
        const { getRequests } = await createRequestsListener(browser)

        await browser.elementByCss('[href="/2"]').click()
        await browser.waitForElementByCss('#random-number')

        await retry(() => {
          const requests = getRequests().filter(
            ([url]) => getPathname(url) === '/2'
          )
          expect(requests.length).toBe(1)
        })

        expect(
          getRequests().some(
            ([url, didPartialPrefetch]) =>
              getPathname(url) === '/2' && didPartialPrefetch
          )
        ).toBe(false)
      })
      it('should re-use the cache only for 30 seconds', async () => {
        await browser.elementByCss('[href="/2"]').click()
        await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()

        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/2"]')

        await browser.elementByCss('[href="/2"]').click()
        await browser.waitForElementByCss('#random-number')
        const number = await browser.elementById('random-number').text()

        expect(number).toBe(randomNumber)

        await browser.eval(fastForwardTo, 30 * 1000)

        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/2"]')

        await browser.elementByCss('[href="/2"]').click()
        await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()

        expect(newNumber).not.toBe(randomNumber)
      })
    })
    describe('prefetch={undefined} - default', () => {
      let browser: Playwright

      beforeEach(async () => {
        browser = await next.browser('/', browserConfigWithFixedTime)
      })

      it('should prefetch partially a dynamic page', async () => {
        const { getRequests, clearRequests } =
          await createRequestsListener(browser)

        await retry(() => {
          expect(
            getRequests().some(
              ([url, didPartialPrefetch]) =>
                getPathname(url) === '/1' && didPartialPrefetch
            )
          ).toBe(true)
        })

        clearRequests()

        await browser.elementByCss('[href="/1"]').click()
        await browser.waitForElementByCss('#random-number')

        await retry(() => {
          expect(
            getRequests().some(
              ([url, didPartialPrefetch]) =>
                getPathname(url) === '/1' && !didPartialPrefetch
            )
          ).toBe(true)
        })
      })
it('should re-use the full cache for only 30 seconds', async () => {
        await browser.elementByCss('[href="/1"]').click()
        await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()

        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/1"]')

        await browser.elementByCss('[href="/1"]').click()
        await browser.waitForElementByCss('#random-number')
        const number = await browser.elementById('random-number').text()

        expect(number).toBe(randomNumber)

        await browser.eval(fastForwardTo, 5 * 1000)

        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/1"]')

        await browser.elementByCss('[href="/1"]').click()
        await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()

        expect(newNumber).toBe(randomNumber)

        await browser.eval(fastForwardTo, 30 * 1000)

        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/1"]')

        await browser.elementByCss('[href="/1"]').click()
        await browser.waitForElementByCss('#random-number')
const newNumber2 = await browser.elementById('random-number').text()

        expect(newNumber2).not.toBe(newNumber)
      })

it('should renew the 30s cache once the data is revalidated', async ()
=> {
        // navigate to prefetch-auto page
        await browser.elementByCss('[href="/1"]').click()
        await browser.waitForElementByCss('#random-number')

let initialNumber = await browser.elementById('random-number').text()

// Navigate back to the index, and then back to the prefetch-auto page
        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/1"]')
        await browser.eval(fastForwardTo, 5 * 1000)
        await browser.elementByCss('[href="/1"]').click()
        await browser.waitForElementByCss('#random-number')

let newNumber = await browser.elementById('random-number').text()

        // the number should be the same, as we navigated within 30s.
        expect(newNumber).toBe(initialNumber)

        // Fast forward to expire the cache
        await browser.eval(fastForwardTo, 30 * 1000)

// Navigate back to the index, and then back to the prefetch-auto page
        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/1"]')
        await browser.elementByCss('[href="/1"]').click()
        await browser.waitForElementByCss('#random-number')

        newNumber = await browser.elementById('random-number').text()

// ~35s have passed, so the cache should be expired and the number
should be different
        expect(newNumber).not.toBe(initialNumber)

// once the number is updated, we should have a renewed 30s cache for
this entry
        // store this new number so we can check that it stays the same
        initialNumber = newNumber

        await browser.eval(fastForwardTo, 5 * 1000)

// Navigate back to the index, and then back to the prefetch-auto page
        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/1"]')
        await browser.elementByCss('[href="/1"]').click()
        await browser.waitForElementByCss('#random-number')

        newNumber = await browser.elementById('random-number').text()

// the number should be the same, as we navigated within 30s (part 2).
        expect(newNumber).toBe(initialNumber)
      })

      it('should refetch below the fold after 30 seconds', async () => {
        await browser.elementByCss('[href="/1?timeout=1000"]').click()
        await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()

        await browser.elementByCss('[href="/"]').click()
        await browser.waitForElementByCss('[href="/1?timeout=1000"]')

        await browser.eval(fastForwardTo, 30 * 1000)

        await browser.elementByCss('[href="/1?timeout=1000"]').click()
        await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()

        expect(newNumber).not.toBe(randomNumber)
      })
      it('should refetch the full page after 5 mins', async () => {
        // Wait for initial prefetch to complete before clicking
        await browser.waitForIdleNetwork()

        const randomLoadingNumber = await browser
          .elementByCss('[href="/1?timeout=1000"]')
          .click()
          .waitForElementByCss('#loading')
          .text()

        const randomNumber = await browser
          .waitForElementByCss('#random-number')
          .text()

        await browser.eval(fastForwardTo, 5 * 60 * 1000)

        await browser
          .elementByCss('[href="/"]')
          .click()
          .waitForElementByCss('[href="/1?timeout=1000"]')

// Wait for prefetch requests to complete before clicking, otherwise
// clicking during an in-flight prefetch aborts it and skips loading
state
        await browser.waitForIdleNetwork()

        const newLoadingNumber = await browser
          .elementByCss('[href="/1?timeout=1000"]')
          .click()
          .waitForElementByCss('#loading')
          .text()

        const newNumber = await browser
          .waitForElementByCss('#random-number')
          .text()

        expect(newLoadingNumber).not.toBe(randomLoadingNumber)

        expect(newNumber).not.toBe(randomNumber)
      })

it('should respect a loading boundary that returns `null`', async () =>
{
        await browser.elementByCss('[href="/null-loading"]').click()

        // the page content should disappear immediately
        await retry(async () => {
          expect(
await browser.hasElementByCssSelector('[href="/null-loading"]')
          ).toBe(false)
        })

        // the root layout should still be visible
expect(await browser.hasElementByCssSelector('#root-layout')).toBe(true)

        // the dynamic content should eventually appear
        await browser.waitForElementByCss('#random-number')
expect(await browser.hasElementByCssSelector('#random-number')).toBe(
          true
        )
      })
    })

it('should seed the prefetch cache with the fetched page data', async ()
=> {
const browser = await next.browser('/1', browserConfigWithFixedTime)

      await browser.waitForElementByCss('#random-number')
const initialNumber = await browser.elementById('random-number').text()

// Move forward a few seconds, navigate off the page and then back to it
      await browser.eval(fastForwardTo, 5 * 1000)
      await browser.elementByCss('[href="/"]').click()
      await browser.waitForElementByCss('[href="/1"]')

      await browser.waitForIdleNetwork()

      await browser.elementByCss('[href="/1"]').click()
      await browser.waitForElementByCss('#random-number')

const newNumber = await browser.elementById('random-number').text()

// The number should be the same as we've seeded it in the prefetch
cache when we loaded the full page
      expect(newNumber).toBe(initialNumber)
    })

it('should renew the initial seeded data after expiration time', async
() => {
      const browser = await next.browser(
        '/without-loading/1',
        browserConfigWithFixedTime
      )

      await browser.waitForElementByCss('#random-number')
const initialNumber = await browser.elementById('random-number').text()

      // Expire the cache
      await browser.eval(fastForwardTo, 30 * 1000)
      await browser.elementByCss('[href="/without-loading"]').click()
      await browser.waitForElementByCss('[href="/without-loading/1"]')
      await browser.elementByCss('[href="/without-loading/1"]').click()
      await browser.waitForElementByCss('#random-number')

const newNumber = await browser.elementById('random-number').text()

// The number should be different, as the seeded data has expired after
30s
      expect(newNumber).not.toBe(initialNumber)
    })
  }
})`
- ID 4: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` —
`describe('app-dir - custom-cache-handler - cjs', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
    env: {
      CUSTOM_CACHE_HANDLER: 'cache-handler.js',
    },
  })

  runTests('cjs module exports', { next, isNextDev })
})`
- ID 5: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` —
`describe('app-dir - custom-cache-handler - cjs-default-export', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
    env: {
      CUSTOM_CACHE_HANDLER: 'cache-handler-cjs-default-export.js',
    },
  })

  runTests('cjs default export', { next, isNextDev })
})`
- ID 6: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` —
`describe('app-dir - custom-cache-handler - esm', () => {
  const { next, isNextDev } = nextTestSetup({
    files: {
      app: new FileRef(__dirname + '/app'),
'cache-handler-esm.js': new FileRef(__dirname +
'/cache-handler-esm.js'),
      'next.config.js': originalNextConfig.replace(
        'module.exports = ',
        'export default '
      ),
    },
    packageJson: {
      type: 'module',
    },
    env: {
      CUSTOM_CACHE_HANDLER: 'cache-handler-esm.js',
    },
  })

  runTests('esm default export', { next, isNextDev })
})`
- ID 7: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` —
`describe('app-dir - custom-cache-handler - esm import.meta.resolve', ()
=> {
  const { next, isNextDev } = nextTestSetup({
    files: {
      app: new FileRef(__dirname + '/app'),
'cache-handler-esm.js': new FileRef(__dirname +
'/cache-handler-esm.js'),
      'next.config.js': importMetaResolveNextConfig,
    },
    packageJson: {
      type: 'module',
    },
  })

  runTests('esm default export', { next, isNextDev })
})`
- ID 9: `test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts`
— `describe('app dir - prefetching (custom staleTime)', () => {
  const { next, isNextDev } = nextTestSetup({
    files: {
      app: new FileRef(join(__dirname, 'app')),
    },
    nextConfig: {
      experimental: {
        staleTimes: {
static: 30, // Minimum enforced by clientSegmentCache is 30 seconds
          dynamic: 5,
        },
      },
    },
  })

  if (isNextDev) {
    it('should skip next dev for now', () => {})
    return
  }

it('should not fetch again when a static page was prefetched when
navigating to it twice', async () => {
    let act: ReturnType<typeof createRouterAct>
    const browser = await next.browser('/', {
      beforePageLoad(page) {
        act = createRouterAct(page)
      },
    })

    // Reveal the link to trigger prefetch and wait for it to complete
    const link = await act(
      async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
        await reveal.click()
        return browser.elementByCss('#to-static-page')
      },
      { includes: 'Static Page [prefetch-sentinel]' }
    )

// Navigate to static page - should use prefetched data with no
additional requests
    await act(async () => {
      await link.click()
const staticPageText = await browser.elementByCss('#static-page').text()
      expect(staticPageText).toBe('Static Page [prefetch-sentinel]')
    }, 'no-requests')

    // Reveal the "to-home" link and navigate back
// Note: Not using act() here because behavior differs between cache
models.
// With clientSegmentCache, revealing may trigger a prefetch. Without
it, home is already
// cached so no prefetch occurs. Either way, navigation works with
cached data.
    const reveal = await browser.elementByCss('#accordion-to-home')
    await reveal.click()
    const homeLink = await browser.waitForElementByCss('#to-home')
    await homeLink.click()
    await browser.waitForElementByCss('#accordion-to-static-page')

// Reveal the static page link again since accordion is hidden after
navigation
    await browser.elementByCss('#accordion-to-static-page').click()
    await browser.waitForElementByCss('#to-static-page')

// Navigate to static page again using the accordion - should still use
cached data with no additional requests
    const staticPageText = await act(async () => {
      await browser.elementByCss('#to-static-page').click()
      return browser.elementByCss('#static-page').text()
    }, 'no-requests')

    expect(staticPageText).toBe('Static Page [prefetch-sentinel]')
  })

it('should fetch again when a static page was prefetched when navigating
to it after the stale time has passed', async () => {
    let act: ReturnType<typeof createRouterAct>
    const timeController = createTimeController()
    const browser = await next.browser('/', {
      beforePageLoad(page) {
        act = createRouterAct(page)
      },
    })

    // Install time controller
    await timeController.install(browser)

// Reveal the static-page link to trigger prefetch and wait for it to
complete
    let link = await act(
      async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
        await reveal.click()
        return browser.elementByCss('#to-static-page')
      },
      { includes: 'Static Page [prefetch-sentinel]' }
    )

// Navigate to static page - should use prefetched data with no
additional requests
    await act(async () => {
      await link.click()
      await browser.waitForElementByCss('#static-page')
    }, 'no-requests')

    // Reveal the "to-home" link and navigate back
    const reveal = await browser.elementByCss('#accordion-to-home')
    await reveal.click()
    const homeLink = await browser.waitForElementByCss('#to-home')
    await homeLink.click()
    await browser.waitForElementByCss('#accordion-to-static-page')

    // Advance time past the stale time
    await timeController.advance(browser, 31000)

// Reveal the static-page link to trigger prefetch and wait for it to
complete
    link = await act(
      async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
        await reveal.click()
        return browser.elementByCss('#to-static-page')
      },
      { includes: 'Static Page [prefetch-sentinel]' }
    )

// Navigate to static page - should use prefetched data with no
additional requests
    await act(async () => {
      await link.click()
      await browser.waitForElementByCss('#static-page')
    }, 'no-requests')
  })

  // FIXME: Flaky test - investigate and re-enable
it.skip('should not re-fetch cached data when navigating back to a route
group', async () => {
    let act: ReturnType<typeof createRouterAct>
// Just installing so that the page doesn't automatically move past
dynamic stale time
    createTimeController()
    const browser = await next.browser('/prefetch-auto-route-groups', {
      beforePageLoad(page) {
        act = createRouterAct(page)
      },
    })

// Once the page has loaded, we expect a data fetch (initial page load)
    expect(await browser.elementById('count').text()).toBe('1')

    // Navigate to a sub-page - this will trigger a data fetch
    await act(async () => {
      await browser
        .elementByCss("[href='/prefetch-auto-route-groups/sub/foo']")
        .click()
    })

// Navigate back to the route group page - should use cached data with
no additional fetch
    await act(async () => {
await
browser.elementByCss("[href='/prefetch-auto-route-groups']").click()
// Confirm that the dashboard page is still rendering the stale fetch
count, as it should be cached
    }, 'no-requests')

    expect(await browser.elementById('count').text()).toBe('1')

    // Navigate to a new sub-page - this will trigger another data fetch
    await act(async () => {
      await browser
        .elementByCss("[href='/prefetch-auto-route-groups/sub/bar']")
        .click()
    })

// Finally, go back to the route group page - should use cached data
with no additional fetch
    await act(async () => {
await
browser.elementByCss("[href='/prefetch-auto-route-groups']").click()
    }, 'no-requests')

// Confirm that the dashboard page is still rendering the stale fetch
count, as it should be cached
    expect(await browser.elementById('count').text()).toBe('1')

    // Reload the page to get the accurate total number of fetches
    await browser.refresh()

// The initial fetch, 2 sub-page fetches, and a final fetch when
reloading the page
    expect(await browser.elementById('count').text()).toBe('4')
  })

it('should fetch again when the initially visited static page is visited
after the stale time has passed', async () => {
    let act: ReturnType<typeof createRouterAct>
    const timeController = createTimeController()
    const browser = await next.browser('/static-page-no-prefetch', {
      beforePageLoad(page) {
        act = createRouterAct(page)
      },
    })

    // Install time controller
    await timeController.install(browser)

// Wait for the page to load (initial navigation request happened during
browser load)
    await browser.waitForElementByCss('#static-page-no-prefetch')

// Reveal the home link and wait for prefetch to complete, then navigate
    const homeLink = await act(
      async () => {
        const reveal = await browser.elementByCss('#accordion-to-home')
        await reveal.click()
        return browser.elementByCss('#to-home')
      },
      { includes: 'Home Page [prefetch-sentinel]' }
    )

// Navigate to home - no additional requests since we just prefetched
    await homeLink.click()
    await browser.waitForElementByCss('#accordion-to-static-page')

    // Advance time past the stale time
    await timeController.advance(browser, 31000)

    // Reveal the link to static-page-no-prefetch and wait for prefetch
    const link = await act(
      async () => {
        const reveal = await browser.elementByCss(
          '#accordion-to-static-page-no-prefetch'
        )
        await reveal.click()
        return browser.elementByCss('#to-static-page-no-prefetch')
      },
      { includes: 'Static Page No Prefetch [prefetch-sentinel]' }
    )

// Navigate back to static-page-no-prefetch - should use the fresh
prefetch data
    const staticPageText = await act(async () => {
      await link.click()
      return browser.elementByCss('#static-page-no-prefetch').text()
    }, 'no-requests')
expect(staticPageText).toBe('Static Page No Prefetch
[prefetch-sentinel]')
  })

it('should renew the stale time after refetching expired RSC data',
async () => {
    let act: ReturnType<typeof createRouterAct>
    const timeController = createTimeController()
    const browser = await next.browser('/', {
      beforePageLoad(page) {
        act = createRouterAct(page)
      },
    })

    // Install time controller
    await timeController.install(browser)

// Reveal the static-page link to trigger prefetch and wait for it to
complete
    let link = await act(
      async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
        await reveal.click()
        return browser.elementByCss('#to-static-page')
      },
      { includes: 'Static Page [prefetch-sentinel]' }
    )

// Navigate to static page (should use prefetched data with no
additional requests)
    await act(async () => {
      await link.click()
      await browser.waitForElementByCss('#static-page')
    }, 'no-requests')

    // Reveal the "to-home" link and navigate back
// Note: Not using act() here because behavior differs between cache
models.
// With clientSegmentCache, revealing may trigger a prefetch. Without
it, home is already
// cached so no prefetch occurs. Either way, navigation works with
cached data.
    const reveal = await browser.elementByCss('#accordion-to-home')
    await reveal.click()
    const homeLink = await browser.waitForElementByCss('#to-home')
    await homeLink.click()
    await browser.waitForElementByCss('#accordion-to-static-page')

    // Advance time past the stale time
    await timeController.advance(browser, 31000)

// Reveal the static-page link to trigger prefetch and wait for it to
complete
    link = await act(
      async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
        await reveal.click()
        return browser.elementByCss('#to-static-page')
      },
      { includes: 'Static Page [prefetch-sentinel]' }
    )

// Navigate to static page again (should use freshly prefetched data
with no additional requests)
    await act(async () => {
      await link.click()
      await browser.waitForElementByCss('#static-page')
    }, 'no-requests')

    // Go back to home (reveal the link and navigate)
// Note: Not using act() here because behavior differs between cache
models.
    const reveal2 = await browser.elementByCss('#accordion-to-home')
    await reveal2.click()
    const homeLink2 = await browser.waitForElementByCss('#to-home')
    await homeLink2.click()
    await browser.waitForElementByCss('#accordion-to-static-page')

// Advance time but not past the stale time (20 seconds < 30 second
stale time - should still be fresh)
    await timeController.advance(browser, 20000)

// Reveal the static-page link to trigger prefetch (should use cached
data, not refetch)
    link = await act(async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
      await reveal.click()
      return browser.elementByCss('#to-static-page')
    }, 'no-requests')

// Navigate to static page again (should NOT refetch - stale time should
be renewed)
// If this assertion passes, it means the stale time was properly
renewed after the refetch
    const staticPageText = await act(async () => {
      await link.click()
      return browser.elementByCss('#static-page').text()
    }, 'no-requests')
    expect(staticPageText).toBe('Static Page [prefetch-sentinel]')
  })
})`
- ID 10: `test/e2e/app-dir/app-root-params-getters/use-cache.test.ts` —
`describe('app-root-param-getters - cache dedup with root params', () =>
{
  const { next, isNextDev } = nextTestSetup({
    files: join(__dirname, 'fixtures', 'use-cache-dedup'),
  })

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)
  })

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

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

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

// Different root params produce different entries, in dev and
production.
    expect(randomEn).not.toBe(randomFr1)

    if (isNextDev) {
// In dev, private caches are persisted and participate in cross-request
// deduplication keyed by root params, so the two ca/fr requests join
one
      // in-flight invocation and share a single fill.
      expect(randomFr1).toBe(randomFr2)
    } else {
// In production, private caches are not persisted and are never deduped
      // across requests, so each ca/fr request generates its own value.
      expect(randomFr1).not.toBe(randomFr2)
    }
  })
})`
- ID 21: `test/e2e/app-dir/cache-components-errors/module-scope.test.ts`
— `describe('Lazy Module Init', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname + '/fixtures/lazy-module-init',
    skipStart: true,
  })

  if (isNextDev) {
    it('does not run in dev', () => {})
    return
  }

it('should build statically even if module scope uses sync APIs like
current time and random', async () => {
    try {
      await next.start()
    } catch {
throw new Error('expected build not to fail for fully static project')
    }

    expect(next.cliOutput).toContain('○ /server')
    expect(next.cliOutput).toContain('○ /client')
    expect(next.cliOutput).toContain('○ /client-page')
    expect(next.cliOutput).toContain('◐ /[dyn]')
    let $

    $ = await next.render$('/server')
    expect($('#id').text().length).toBeGreaterThan(0)

    $ = await next.render$('/client')
    expect($('#id').text().length).toBeGreaterThan(0)

    $ = await next.render$('/client-page')
    expect($('#id').text().length).toBeGreaterThan(0)

    $ = await next.render$('/foo')
    expect($('#id').text().length).toBeGreaterThan(0)

    $ = await next.render$('/serial-client-sync-io')
    expect($('#id').text().length).toBeGreaterThan(0)
  })
})`
- ID 27:
`test/e2e/app-dir/cache-components/cache-components.connection.test.ts`
— `describe('cache-components', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

it('should partially prerender pages that use connection', async () => {
let $ = await next.render$('/connection/static-behavior/boundary', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#foo').text()).toBe('foo')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#foo').text()).toBe('foo')
    }
  })

it('should be able to pass connection as a promise to another component
and trigger an intermediate Suspense boundary', async () => {
const $ = await next.render$('/connection/static-behavior/pass-deeply')
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
// In dev, whether or not the fallback appears in the HTML is unreliable
      // and depends on timing, so we don't assert on its presence
      // (if we want to assert on it, we should use a browser test)
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#fallback').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at runtime')
    }
  })
})`
- ID 28:
`test/e2e/app-dir/cache-components/cache-components.cookies.test.ts` —
`describe('cache-components', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  it('should partially prerender pages that use cookies', async () => {
    let $ = await next.render$('/cookies/static-behavior', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#x-sentinel').text()).toBe('hello')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#x-sentinel').text()).toBe('hello')
    }
  })

it('should be able to pass cookies as a promise to another component and
trigger an intermediate Suspense boundary', async () => {
    const $ = await next.render$('/cookies/static-behavior/pass-deeply')
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#fallback').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#fallback').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at runtime')
    }
  })

  it('should be able to access cookie properties', async () => {
    let $ = await next.render$('/cookies/exercise', {})
    let cookieWarnings = next.cliOutput
      .split('\n')
      .filter((l) => l.includes('Route "/cookies/exercise'))

    expect(cookieWarnings).toHaveLength(0)

    // For...of iteration
    expect($('#for-of-x-sentinel').text()).toContain('hello')

expect($('#for-of-x-sentinel-path').text()).toContain('/cookies/exercise')

expect($('#for-of-x-sentinel-rand').text()).toContain('x-sentinel-rand')

    // ...spread iteration
    expect($('#spread-x-sentinel').text()).toContain('hello')

expect($('#spread-x-sentinel-path').text()).toContain('/cookies/exercise')

expect($('#spread-x-sentinel-rand').text()).toContain('x-sentinel-rand')

    // cookies().size
expect(parseInt($('#size-cookies').text())).toBeGreaterThanOrEqual(3)

    // cookies().get('...') && cookies().getAll('...')
    expect($('#get-x-sentinel').text()).toContain('hello')
expect($('#get-x-sentinel-path').text()).toContain('/cookies/exercise')
expect($('#get-x-sentinel-rand').text()).toContain('x-sentinel-rand')

    // cookies().has('...')
    expect($('#has-x-sentinel').text()).toContain('true')
    expect($('#has-x-sentinel-foobar').text()).toContain('false')

    // cookies().set('...', '...')
    expect($('#set-result-x-sentinel').text()).toContain(
      'Cookies can only be modified in a Server Action'
    )
    expect($('#set-value-x-sentinel').text()).toContain('hello')

    // cookies().delete('...', '...')
    expect($('#delete-result-x-sentinel').text()).toContain(
      'Cookies can only be modified in a Server Action'
    )
    expect($('#delete-value-x-sentinel').text()).toContain('hello')

    // cookies().clear()
    expect($('#clear-result').text()).toContain(
      'Cookies can only be modified in a Server Action'
    )
    expect($('#clear-value-x-sentinel').text()).toContain('hello')

    // cookies().toString()
    expect($('#toString').text()).toContain('x-sentinel=hello')
    expect($('#toString').text()).toContain('x-sentinel-path')
    expect($('#toString').text()).toContain('x-sentinel-rand=')
  })
})`
- ID 29:
`test/e2e/app-dir/cache-components/cache-components.date.test.ts` —
`describe('cache-components', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  it('should not have route specific errors', async () => {
    expect(next.cliOutput).not.toMatch('Error: Route "/')
expect(next.cliOutput).not.toMatch('Error occurred prerendering page')
  })

it('should prerender pages with cached `Date.now()` calls', async () =>
{
    let $ = await next.render$('/date/now/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#value').text()).toMatch(/^\d+$/)
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#value').text()).toMatch(/^\d+$/)
    }
  })

  it('should prerender pages with cached `Date()` calls', async () => {
    let $ = await next.render$('/date/date/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#value').text()).toContain('GMT')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#value').text()).toContain('GMT')
    }
  })

it('should prerender pages with cached `new Date()` calls', async () =>
{
    let $ = await next.render$('/date/new-date/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#value').text()).toContain('GMT')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#value').text()).toContain('GMT')
    }
  })

it('should prerender pages with cached static Date instances like `new
Date(0)`', async () => {
    let $ = await next.render$('/date/static-date/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#value').text()).toContain('GMT')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#value').text()).toContain('GMT')
    }
  })

it('should not prerender pages with uncached static Date instances like
`new Date(0)`', async () => {
    let $ = await next.render$('/date/static-date/uncached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#value').text()).toContain('GMT')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#value').text()).toContain('GMT')
    }
  })
})`
- ID 30:
`test/e2e/app-dir/cache-components/cache-components.draft-mode.test.ts`
— `describe('cache-components', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  let cliIndex = 0
  beforeEach(() => {
    cliIndex = next.cliOutput.length
  })
  function getLines(containing: string): Array<string> {
    const warnings = next.cliOutput
      .slice(cliIndex)
      .split('\n')
      .filter((l) => l.includes(containing))

    cliIndex = next.cliOutput.length
    return warnings
  }

  it('should fully prerender pages that use draftMode', async () => {
    expect(getLines('Route "/draftmode')).toEqual([])
    let $ = await next.render$('/draftmode', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#draft-mode').text()).toBe('false')
      expect(getLines('Route "/draftmode')).toEqual([])
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#draft-mode').text()).toBe('false')
      expect(getLines('Route "/draftmode')).toEqual([])
    }
  })

  if (!isNextDev) {
it('should stream Suspense fallbacks when draft mode is enabled', async
() => {
      const draftRes = await next.fetch('/draftmode/toggle')
      const setCookie = draftRes.headers.get('set-cookie')
      const cookieHeader = { Cookie: setCookie?.split(';', 1)[0] }

      expect(cookieHeader.Cookie).toBeTruthy()

      const $ = await next.render$('/draftmode/streaming', undefined, {
        headers: cookieHeader,
      })

      expect($('#draft-mode').text()).toBe('true')
      expect($('#delayed-runtime-fallback').text()).toBe(
        'Loading draft content...'
      )
    })
  }
})`
- ID 32:
`test/e2e/app-dir/cache-components/cache-components.node-crypto.test.ts`
— `describe('cache-components', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  it('should not have route specific errors', async () => {
    expect(next.cliOutput).not.toMatch('Error: Route "/')
expect(next.cliOutput).not.toMatch('Error occurred prerendering page')
  })

it("should prerender pages with cached
`require('node:crypto').getRandomValues(...)` calls", async () => {
let $ = await next.render$('/node-crypto/get-random-values/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })

it("should prerender pages with cached
`require('node:crypto').randomUUID()` calls", async () => {
    let $ = await next.render$('/node-crypto/random-uuid/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })

it("should prerender pages with cached
`require('node:crypto').randomBytes(size)` calls", async () => {
    let $ = await next.render$('/node-crypto/random-bytes/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })

it("should prerender pages with cached
`require('node:crypto').randomFillSync(buffer)` calls", async () => {
let $ = await next.render$('/node-crypto/random-fill-sync/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })

it("should prerender pages with cached
`require('node:crypto').randomInt(max)` calls", async () => {
let $ = await next.render$('/node-crypto/random-int/up-to/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })

it("should prerender pages with cached
`require('node:crypto').randomInt(min, max)` calls", async () => {
let $ = await next.render$('/node-crypto/random-int/between/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })

it("should prerender pages with cached
`require('node:crypto').generatePrimeSync(size, options)` calls", async
() => {
let $ = await next.render$('/node-crypto/generate-prime-sync/cached',
{})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })

it("should prerender pages with cached
`require('node:crypto').generateKeyPairSync(type, options)` calls",
async () => {
let $ = await next.render$('/node-crypto/generate-key-pair-sync/cached',
{})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })

it("should prerender pages with cached
`require('node:crypto').generateKeySync(type, options)` calls", async ()
=> {
let $ = await next.render$('/node-crypto/generate-key-sync/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })
})`
- ID 33:
`test/e2e/app-dir/cache-components/cache-components.params.test.ts` —
`describe('cache-components', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  let cliIndex = 0
  beforeEach(() => {
    cliIndex = next.cliOutput.length
  })
  function getLines(containing: string): Array<string> {
    const warnings = next.cliOutput
      .slice(cliIndex)
      .split('\n')
      .filter((l) => l.includes(containing))

    cliIndex = next.cliOutput.length
    return warnings
  }

  describe('Params', () => {
it('should partially prerender pages that await params in a server
components', async () => {
      expect(getLines('Route "/params')).toEqual([])

      let $ = await next.render$(
        '/params/semantics/one/build/layout-access/server'
      )
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')

        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('build')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('build')
      }

$ = await next.render$('/params/semantics/one/run/layout-access/server')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')

        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('run')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('run')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/build/page-access/server')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')

        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('build')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('build')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/page-access/server')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')

        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('run')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('run')
        expect(getLines('Route "/params')).toEqual([])
      }
    })

// Since #85155, we intentionally omit search params from client
segments
    // if the page is otherwise static, and resume using a client fetch
// instead. So it's expected that the value is missing pre-hydration.
// There are separate tests that verify that it is eventually hydrated.
    // TODO: Rewrite or update this test.
it.skip('should partially prerender pages that use params in a client
components', async () => {
      expect(getLines('Route "/params')).toEqual([])

      let $ = await next.render$(
        '/params/semantics/one/build/layout-access/client'
      )
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('build')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('build')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/layout-access/client')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')

        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('run')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('run')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/build/page-access/client')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('build')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('build')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/page-access/client')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('run')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-lowcard').text()).toBe('one')
        expect($('#param-highcard').text()).toBe('run')
        expect(getLines('Route "/params')).toEqual([])
      }
    })

it('should fully prerender pages that check individual param keys after
awaiting params in a server component', async () => {
      expect(getLines('Route "/params')).toEqual([])
      let $ = await next.render$(
        '/params/semantics/one/build/layout-has/server'
      )
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/build/page-has/server')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/layout-has/server')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      } else {
// With PPR fallbacks the first visit is still partially prerendered
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/page-has/server')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      } else {
// With PPR fallbacks the first visit is still partially prerendered
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      }
    })

// Since #85155, we intentionally omit search params from client
segments
    // if the page is otherwise static, and resume using a client fetch
// instead. So it's expected that the value is missing pre-hydration.
// There are separate tests that verify that it is eventually hydrated.
    // TODO: Rewrite or update this test.
it.skip('should fully prerender pages that check individual param keys
after `use`ing params in a client component', async () => {
      expect(getLines('Route "/params')).toEqual([])
      let $ = await next.render$(
        '/params/semantics/one/build/layout-has/client'
      )
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/build/page-has/client')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/layout-has/client')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      } else {
// With PPR fallbacks the first visit is still partially prerendered
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/page-has/client')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      } else {
// With PPR fallbacks the first visit is still partially prerendered
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-has-lowcard').text()).toBe('true')
        expect($('#param-has-highcard').text()).toBe('true')
        expect($('#param-has-foo').text()).toBe('false')
        expect(getLines('Route "/params')).toEqual([])
      }
    })

it('should partially prerender pages that spread awaited params in a
server component', async () => {
      expect(getLines('Route "/params')).toEqual([])
      let $ = await next.render$(
        '/params/semantics/one/build/layout-spread/server'
      )
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('build')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('build')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/build/page-spread/server')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('build')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('build')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/layout-spread/server')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('run')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('run')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/page-spread/server')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('run')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('run')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      }
    })

// Since #85155, we intentionally omit search params from client
segments
    // if the page is otherwise static, and resume using a client fetch
// instead. So it's expected that the value is missing pre-hydration.
// There are separate tests that verify that it is eventually hydrated.
    // TODO: Rewrite or update this test.
it.skip('should partially prerender pages that spread `use`ed params in
a client component', async () => {
      expect(getLines('Route "/params')).toEqual([])
      let $ = await next.render$(
        '/params/semantics/one/build/layout-spread/client'
      )
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('build')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('build')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/build/page-spread/client')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('build')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at buildtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('build')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/layout-spread/client')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('run')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('run')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/semantics/one/run/page-spread/client')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#lowcard').text()).toBe('at runtime')
        expect($('#highcard').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('run')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#lowcard').text()).toBe('at buildtime')
        expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-copied-lowcard').text()).toBe('one')
        expect($('#param-copied-highcard').text()).toBe('run')
        expect($('#param-key-count').text()).toBe('2')
        expect(getLines('Route "/params')).toEqual([])
      }
    })
  })

  describe('Param Shadowing', () => {
it('should correctly allow param names like then, value, and status when
awaiting params in a server component', async () => {
      expect(getLines('Route "/params')).toEqual([])
      let $ = await next.render$(
        '/params/shadowing/foo/bar/baz/qux/layout/server'
      )
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-dyn').text()).toBe('foo')
        expect($('#param-then').text()).toBe('bar')
        expect($('#param-value').text()).toBe('baz')
        expect($('#param-status').text()).toBe('qux')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-dyn').text()).toBe('foo')
        expect($('#param-then').text()).toBe('bar')
        expect($('#param-value').text()).toBe('baz')
        expect($('#param-status').text()).toBe('qux')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/shadowing/foo/bar/baz/qux/page/server')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-dyn').text()).toBe('foo')
        expect($('#param-then').text()).toBe('bar')
        expect($('#param-value').text()).toBe('baz')
        expect($('#param-status').text()).toBe('qux')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-dyn').text()).toBe('foo')
        expect($('#param-then').text()).toBe('bar')
        expect($('#param-value').text()).toBe('baz')
        expect($('#param-status').text()).toBe('qux')
        expect(getLines('Route "/params')).toEqual([])
      }
    })

// Since #85155, we intentionally omit search params from client
segments
    // if the page is otherwise static, and resume using a client fetch
// instead. So it's expected that the value is missing pre-hydration.
// There are separate tests that verify that it is eventually hydrated.
    // TODO: Rewrite or update this test.
it.skip('should correctly allow param names like then, value, and status
when `use`ing params in a client component', async () => {
      expect(getLines('Route "/params')).toEqual([])
      let $ = await next.render$(
        '/params/shadowing/foo/bar/baz/qux/layout/client'
      )
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-dyn').text()).toBe('foo')
        expect($('#param-then').text()).toBe('bar')
        expect($('#param-value').text()).toBe('baz')
        expect($('#param-status').text()).toBe('qux')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-dyn').text()).toBe('foo')
        expect($('#param-then').text()).toBe('bar')
        expect($('#param-value').text()).toBe('baz')
        expect($('#param-status').text()).toBe('qux')
        expect(getLines('Route "/params')).toEqual([])
      }

$ = await next.render$('/params/shadowing/foo/bar/baz/qux/page/client')
      if (isNextDev) {
        expect($('#layout').text()).toBe('at runtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-dyn').text()).toBe('foo')
        expect($('#param-then').text()).toBe('bar')
        expect($('#param-value').text()).toBe('baz')
        expect($('#param-status').text()).toBe('qux')
        expect(getLines('Route "/params')).toEqual([])
      } else {
        expect($('#layout').text()).toBe('at buildtime')
        expect($('#page').text()).toBe('at runtime')
        expect($('#param-dyn').text()).toBe('foo')
        expect($('#param-then').text()).toBe('bar')
        expect($('#param-value').text()).toBe('baz')
        expect($('#param-status').text()).toBe('qux')
        expect(getLines('Route "/params')).toEqual([])
      }
    })
  })

  if (!isNextDev) {
    describe('generateStaticParams', () => {
// This test is skipped as the previous workaround of using
`fetch-cache` will no longer be supported with DIO.
it.skip('should have cacheComponents semantics inside
generateStaticParams', async () => {
// This test is named what we want but our current implementation is not
actually correct yet.
// We are asserting current behavior and will update the test when we
land the correct behavior

        const lines: Array<string> = next.cliOutput.split('\n')
        let i = 0
        while (true) {
          const line = lines[i++]
          if (typeof line !== 'string') {
            throw new Error(
'Could not find expected route output for
/params/generate-static-params/[slug]/page/...'
            )
          }

          if (
            line.startsWith('├') &&
            line.includes('/params/generate-static-params/[slug]')
          ) {
            let nextLine = lines[i++]
            // we expect the fallback shell first
expect(nextLine).toContain('/params/generate-static-params/[slug]')
            nextLine = lines[i++]

            expect(nextLine).toMatch(
              /\/params\/generate-static-params\/\d+\/page/
            )
            nextLine = lines[i++]
// Because we force-cache we only end up with one prebuilt page.
// When cacheComponents semantics are fully respected we will end up
with two.
            expect(nextLine).not.toMatch(
              /\/params\/generate-static-params\/\d+\/page/
            )
            break
          }
        }
      })
    })
  }
})`
- ID 34:
`test/e2e/app-dir/cache-components/cache-components.random.test.ts` —
`describe('cache-components', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  it('should not have route specific errors', async () => {
    expect(next.cliOutput).not.toMatch('Error: Route "/')
expect(next.cliOutput).not.toMatch('Error occurred prerendering page')
  })

it('should prerender pages with cached Math.random() calls', async () =>
{
    let $ = await next.render$('/random/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
    }
  })
})`
- ID 35:
`test/e2e/app-dir/cache-components/cache-components.routes.test.ts` —
`describe('cache-components', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  let cliIndex = 0
  beforeEach(() => {
    cliIndex = next.cliOutput.length
  })
  function getLines(containing: string): Array<string> {
    const warnings = next.cliOutput
      .slice(cliIndex)
      .split('\n')
      .filter((l) => l.includes(containing))

    cliIndex = next.cliOutput.length
    return warnings
  }

it('should not prerender GET route handlers that use dynamic APIs',
async () => {
    let str = await next.render('/routes/dynamic-cookies', {})
    let json = JSON.parse(str)

    expect(json.value).toEqual('at runtime')
    expect(json.type).toEqual('cookies')

    str = await next.render('/routes/dynamic-headers', {})
    json = JSON.parse(str)

    expect(json.value).toEqual('at runtime')
    expect(json.type).toEqual('headers')

    str = await next.render('/routes/dynamic-stream', {})
    json = JSON.parse(str)

    expect(json.value).toEqual('at runtime')
    expect(json.message).toEqual('dynamic stream')

    str = await next.render('/routes/dynamic-url?foo=bar', {})
    json = JSON.parse(str)

    expect(json.value).toEqual('at runtime')
    expect(json.search).toEqual('?foo=bar')
  })

it('should prerender GET route handlers that have entirely cached io
(fetches)', async () => {
    let str = await next.render('/routes/fetch-cached', {})
    let json = JSON.parse(str)

    let random1 = json.random1
    let random2 = json.random2

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(typeof random1).toBe('string')
      expect(typeof random2).toBe('string')
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(typeof random1).toBe('string')
      expect(typeof random2).toBe('string')
    }

    str = await next.render('/routes/fetch-cached', {})
    json = JSON.parse(str)

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(random1).toEqual(json.random1)
      expect(random2).toEqual(json.random2)
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(random1).toEqual(json.random1)
      expect(random2).toEqual(json.random2)
    }
  })

it('should not prerender GET route handlers that have some uncached io
(fetches)', async () => {
    let str = await next.render('/routes/fetch-mixed', {})
    let json = JSON.parse(str)

    let random1 = json.random1
    let random2 = json.random2

    expect(json.value).toEqual('at runtime')
    expect(typeof random1).toBe('string')
    expect(typeof random2).toBe('string')

    str = await next.render('/routes/fetch-mixed', {})
    json = JSON.parse(str)

    expect(json.value).toEqual('at runtime')
    expect(random1).toEqual(json.random1)
    expect(random2).not.toEqual(json.random2)
  })

it('should prerender GET route handlers that have entirely cached io
(unstable_cache)', async () => {
    let str = await next.render('/routes/io-cached', {})
    let json = JSON.parse(str)

    let message1 = json.message1
    let message2 = json.message2

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(typeof message1).toBe('string')
      expect(typeof message2).toBe('string')
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(typeof message1).toBe('string')
      expect(typeof message2).toBe('string')
    }

    str = await next.render('/routes/io-cached', {})
    json = JSON.parse(str)

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(message1).toEqual(json.message1)
      expect(message2).toEqual(json.message2)
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(message1).toEqual(json.message1)
      expect(message2).toEqual(json.message2)
    }
  })

it('should prerender GET route handlers that have entirely cached io
("use cache")', async () => {
    let str = await next.render('/routes/use_cache-cached', {})
    let json = JSON.parse(str)

    let message1 = json.message1
    let message2 = json.message2

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(typeof message1).toBe('string')
      expect(typeof message2).toBe('string')
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(typeof message1).toBe('string')
      expect(typeof message2).toBe('string')
    }

    str = await next.render('/routes/use_cache-cached', {})
    json = JSON.parse(str)

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(message1).toEqual(json.message1)
      expect(message2).toEqual(json.message2)
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(message1).toEqual(json.message1)
      expect(message2).toEqual(json.message2)
    }
  })

it('should not prerender GET route handlers that have some uncached io
(unstable_cache)', async () => {
    let str = await next.render('/routes/io-mixed', {})
    let json = JSON.parse(str)

    let message1 = json.message1
    let message2 = json.message2

    expect(json.value).toEqual('at runtime')
    expect(typeof message1).toBe('string')
    expect(typeof message2).toBe('string')

    str = await next.render('/routes/io-mixed', {})
    json = JSON.parse(str)

    expect(json.value).toEqual('at runtime')
    expect(message1).toEqual(json.message1)
    expect(message2).not.toEqual(json.message2)
  })

it('should prerender GET route handlers that complete synchronously or
in a microtask', async () => {
    let str = await next.render('/routes/microtask', {})
    let json = JSON.parse(str)

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(json.message).toBe('microtask')
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(json.message).toBe('microtask')
    }

    str = await next.render('/routes/static-stream-sync', {})
    json = JSON.parse(str)

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(json.message).toBe('stream response')
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(json.message).toBe('stream response')
    }

    str = await next.render('/routes/static-stream-async', {})
    json = JSON.parse(str)

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(json.message).toBe('stream response')
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(json.message).toBe('stream response')
    }

    str = await next.render('/routes/static-string-sync', {})
    json = JSON.parse(str)

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(json.message).toBe('string response')
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(json.message).toBe('string response')
    }

    str = await next.render('/routes/static-string-async', {})
    json = JSON.parse(str)

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(json.message).toBe('string response')
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(json.message).toBe('string response')
    }
  })

it('should not prerender GET route handlers that complete in a new
Task', async () => {
    let str = await next.render('/routes/task', {})
    let json = JSON.parse(str)

    expect(json.value).toEqual('at runtime')
    expect(json.message).toBe('task')
  })

it('should prerender GET route handlers when accessing params', async ()
=> {
    expect(getLines('Route "/routes/[dyn]')).toEqual([])
    let str = await next.render('/routes/1', {})
    let json = JSON.parse(str)

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(json.type).toBe('dynamic params')
      expect(json.param).toBe('1')
      expect(getLines('Route "/routes/[dyn]')).toEqual([])
    } else {
      expect(json.value).toEqual('at buildtime')
      expect(json.type).toBe('dynamic params')
      expect(json.param).toBe('1')
      expect(getLines('Route "/routes/[dyn]')).toEqual([])
    }

    str = await next.render('/routes/2', {})
    json = JSON.parse(str)

    if (isNextDev) {
      expect(json.value).toEqual('at runtime')
      expect(json.type).toBe('dynamic params')
      expect(json.param).toBe('2')
      expect(getLines('Route "/routes/[dyn]')).toEqual([])
    } else {
      expect(json.value).toEqual('at runtime')
      expect(json.type).toBe('dynamic params')
      expect(json.param).toBe('2')
      expect(getLines('Route "/routes/[dyn]')).toEqual([])
    }
  })
})`
- ID 36:
`test/e2e/app-dir/cache-components/cache-components.search.test.ts` —
`describe('cache-components', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

it('should partially prerender pages that await searchParams in a server
component', async () => {
    let $ = await next.render$('/search/server/await?sentinel=hello')
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#value').text()).toBe('hello')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('main').text()).toContain('inner loading...')
      expect($('main').text()).not.toContain('outer loading...')
      expect($('#value').text()).toBe('hello')
      expect($('#page').text()).toBe('at runtime')
    }
  })

it('should partially prerender pages that `use` searchParams in a server
component', async () => {
    let $ = await next.render$('/search/server/use?sentinel=hello')
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#value').text()).toBe('hello')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('main').text()).toContain('inner loading...')
      expect($('main').text()).not.toContain('outer loading...')
      expect($('#value').text()).toBe('hello')
      expect($('#page').text()).toBe('at runtime')
    }
  })

it('should partially prerender pages that `use` searchParams in a client
component', async () => {
    let $ = await next.render$('/search/client/use?sentinel=hello')
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#value').text()).toBe('hello')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('main').text()).toContain('inner loading...')
      expect($('main').text()).not.toContain('outer loading...')
// Since #85155, we intentionally omit search params from client
segments
// if the page is otherwise static, and resume using a client fetch
// instead. So it's expected that the value is missing pre-hydration.
// There are separate tests that verify that it is eventually hydrated.
      // TODO: Rewrite or update this test.
      // expect($('#value').text()).toBe('hello')
      // expect($('#page').text()).toBe('at runtime')
      expect($('#value').text()).toBe('')
      expect($('#page').text()).toBe('')
    }
  })
})`
- ID 37: `test/e2e/app-dir/cache-components/cache-components.test.ts` —
`describe('cache-components', () => {
  const { next, isNextDev, isNextStart } = nextTestSetup({
    files: __dirname,
  })

  it('should not have route specific errors', async () => {
    expect(next.cliOutput).not.toMatch('Error: Route "/')
expect(next.cliOutput).not.toMatch('Error occurred prerendering page')
  })

  if (isNextDev) {
    it('should not log not-found errors', async () => {
      const cliOutputLength = next.cliOutput.length
      await next.browser('/cases/not-found')
      const cliOutput = next.cliOutput.slice(cliOutputLength)
expect(cliOutput).not.toMatch('Error: NEXT_HTTP_ERROR_FALLBACK;404')
      expect(cliOutput).not.toMatch('unhandledRejection')
    })
  } else {
it('should not warn about potential memory leak for even listeners on
AbortSignal', async () => {
      expect(next.cliOutput).not.toMatch('MaxListenersExceededWarning')
    })
  }

  it('should prerender fully static pages', async () => {
    let $ = await next.render$('/cases/static', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
    }

    $ = await next.render$('/cases/static_async', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
    }
  })

  it('should prerender static not-found pages', async () => {
// Using `browser` instead of `render$` because error pages must be
hydrated
    // apparently.
    const browser = await next.browser('/cases/not-found')

    if (isNextDev) {
expect(await browser.elementById('layout').text()).toBe('at runtime')
expect(await browser.elementById('page').text()).toBe('at runtime')
    } else {
expect(await browser.elementById('layout').text()).toBe('at buildtime')
expect(await browser.elementById('page').text()).toBe('at buildtime')
    }
  })

it('should render not-found with Suspense in layout without connection
errors', async () => {
    const browser = await next.browser('/cases/not-found-suspense')

    // The custom not-found component should render
    expect(await browser.elementById('not-found-text').text()).toBe(
      'Custom 404 - Not Found'
    )

    // The async Suspense content in the layout should also render
    expect(await browser.elementById('async-data').text()).toBe(
      'Async Data Loaded'
    )
  })

  it('should prerender pages that render in a microtask', async () => {
    let $ = await next.render$('/cases/microtask', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
    }

    $ = await next.render$('/cases/microtask_deep_tree', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
    }
  })

it('should partially prerender pages that take longer than a task to
render', async () => {
    let $ = await next.render$('/cases/task', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#inner').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      // The inner slot is computed during the prerender but is hidden
      // it gets revealed when the resume happens
      expect($('#inner').text()).toBe('at buildtime')
    }
  })

it('should prerender pages that only use cached fetches', async () => {
    const $ = await next.render$('/cases/fetch_cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
    }
  })

it('should partially prerender pages that use at least one fetch without
cache', async () => {
    let $ = await next.render$('/cases/fetch_mixed', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#inner').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#inner').text()).toBe('at buildtime')
    }
  })

it('should prerender pages that only use cached (unstable_cache) IO',
async () => {
    const $ = await next.render$('/cases/io_cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
    }
  })

it('should prerender pages that only use cached ("use cache") IO', async
() => {
    const $ = await next.render$('/cases/use_cache_cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
    }
  })

  it('should prerender pages that cached the whole page', async () => {
    const $ = await next.render$('/cases/full_cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
    }
  })

it('should partially prerender pages that do any uncached IO', async ()
=> {
    let $ = await next.render$('/cases/io_mixed', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#inner').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#inner').text()).toBe('at buildtime')
    }
  })

it('should partially prerender pages that do any uncached IO (use
cache)', async () => {
    let $ = await next.render$('/cases/use_cache_mixed', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#inner').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#inner').text()).toBe('at buildtime')
    }
  })

it('should partially prerender pages that use `cookies()`', async () =>
{
    let $ = await next.render$('/cases/dynamic_api_cookies', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#inner').text()).toBe('at runtime')
      expect($('#value').text()).toBe('hello')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#inner').text()).toBe('at buildtime')
      expect($('#value').text()).toBe('hello')
    }
  })

it('should partially prerender pages that use `headers()`', async () =>
{
    let $ = await next.render$('/cases/dynamic_api_headers')
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#inner').text()).toBe('at runtime')
      expect($('#value').text()).toBe('hello')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#inner').text()).toBe('at buildtime')
      expect($('#value').text()).toBe('hello')
    }
  })

it('should fully prerender pages that use `unstable_noStore()`', async
() => {
    let $ = await next.render$('/cases/dynamic_api_no_store', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#inner').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#inner').text()).toBe('at buildtime')
    }
  })

it('should partially prerender pages that use `searchParams` in Server
Components', async () => {
    let $ = await next.render$(
      '/cases/dynamic_api_search_params_server?sentinel=my+sentinel',
      {}
    )
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#inner').text()).toBe('at runtime')
      expect($('#value').text()).toBe('my sentinel')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#inner').text()).toBe('at buildtime')
      expect($('#value').text()).toBe('my sentinel')
    }
  })

it('should partially prerender pages that use `searchParams` in Client
Components', async () => {
    let $ = await next.render$(
      '/cases/dynamic_api_search_params_client?sentinel=my+sentinel',
      {}
    )
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#inner').text()).toBe('at runtime')
      expect($('#value').text()).toBe('my sentinel')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
// The second component renders before the first one aborts so we end up
      // capturing the static value during buildtime
      expect($('#inner').text()).toBe('at buildtime')
// Since there was no dynamic data access on this page, the search
params
      // are completely ommitted from the HTML document and filled in by
      // the client
      expect($('#value').text()).toBe('')
      expect($('#fallback-component-one-').text()).toBe('loading...')
    }
  })

it('can prerender pages with parallel routes that are static', async ()
=> {
    const $ = await next.render$('/cases/parallel/static', {})

    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page-slot').text()).toBe('at runtime')
      expect($('#page-children').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page-slot').text()).toBe('at buildtime')
      expect($('#page-children').text()).toBe('at buildtime')
    }
  })

it('can prerender pages with parallel routes that resolve in a
microtask', async () => {
    const $ = await next.render$('/cases/parallel/microtask', {})

    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page-slot').text()).toBe('at runtime')
      expect($('#page-children').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page-slot').text()).toBe('at buildtime')
      expect($('#page-children').text()).toBe('at buildtime')
    }
  })

it('does not prerender pages with parallel routes that resolve in a
task', async () => {
    const $ = await next.render$('/cases/parallel/task', {})

    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page-slot').text()).toBe('at runtime')
      expect($('#page-children').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page-slot').text()).toBe('at runtime')
      expect($('#page-children').text()).toBe('at buildtime')
    }
  })

it('does not prerender pages with parallel routes that uses a dynamic
API', async () => {
    let $ = await next.render$('/cases/parallel/no-store', {})

    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page-slot').text()).toBe('at runtime')
      expect($('#page-children').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page-slot').text()).toBe('at buildtime')
      expect($('#page-children').text()).toBe('at buildtime')
    }

    $ = await next.render$('/cases/parallel/cookies', {})

    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page-slot').text()).toBe('at runtime')
      expect($('#page-children').text()).toBe('at runtime')
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page-slot').text()).toBe('at runtime')
      expect($('#page-children').text()).toBe('at buildtime')
    }
  })

  if (isNextStart) {
it('should ignore late setHeader calls for direct RSC handlers after
headers are sent', async () => {
      const pageModulePath = path.join(
        next.testDir,
        '.next',
        'server',
        'app',
        'cases',
        'static',
        'page.js'
      )
      const previousCwd = process.cwd()
      const port = await findPort()
      let server: http.Server | undefined
      let handlerError: unknown
      let lateHeaderAttempted = false
      let lateHeaderError: unknown
      let resolveHandled: (() => void) | undefined
      const handled = new Promise<void>((resolve) => {
        resolveHandled = resolve
      })

      try {
        process.chdir(next.testDir)

        const { handler } = require(pageModulePath) as {
          handler: (
            req: http.IncomingMessage,
            res: http.ServerResponse,
            ctx: {
              requestMeta?: Record<string, unknown>
              waitUntil?: (promise: Promise<void>) => void
            }
          ) => Promise<void>
        }

        server = http.createServer(async (req, res) => {
          const originalWriteHead = res.writeHead.bind(res)
          res.writeHead = ((...args: any[]) => {
            const result = originalWriteHead(...args)

            if (!lateHeaderAttempted) {
              lateHeaderAttempted = true

              try {
                res.setHeader('x-test-late', '1')
              } catch (error) {
                lateHeaderError = error
              }
            }

            return result
          }) as typeof res.writeHead

          try {
            await handler(req, res, {
              waitUntil: () => {},
              requestMeta: {
                initURL: `https://localhost:${port}${req.url ?? '/'}`,
                minimalMode: true,
                relativeProjectDir: '.',
              },
            })
          } catch (error) {
            handlerError = error

            if (!res.writableEnded) {
              if (!res.headersSent) {
                res.statusCode = 500
              }
              res.end()
            }
          } finally {
            resolveHandled?.()
          }
        })

        await new Promise<void>((resolve, reject) => {
          server.listen(port, () => {
            resolve()
          })
          server.once('error', reject)
        })

        const stateTree = JSON.stringify(['', {}])
const requestUrl = new URL('/cases/static', `http://localhost:${port}`)
        const cacheBustingParam = await computeCacheBustingSearchParam(
          undefined,
          undefined,
          stateTree,
          undefined
        )

        if (cacheBustingParam) {
          requestUrl.searchParams.set('_rsc', cacheBustingParam)
        }

        const res = await fetchViaHTTP(
          port,
          requestUrl.pathname + requestUrl.search,
          undefined,
          {
            headers: {
              rsc: '1',
              'next-router-state-tree': stateTree,
            },
            redirect: 'manual',
          }
        )
        const flight = await res.text()

        expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('text/x-component')
        await handled

        expect(handlerError).toBeUndefined()
        expect(lateHeaderAttempted).toBe(true)
        expect(lateHeaderError).toBeUndefined()
        expect(flight.length).toBeGreaterThan(0)
      } finally {
        process.chdir(previousCwd)

        if (server) {
          await new Promise<void>((resolve, reject) => {
            server.close((error) => {
              if (error) {
                reject(error)
                return
              }

              resolve()
            })
          })
        }
      }
    })
  }

it('should not resume when client components are dynamic but the RSC
render was static', async () => {
    let html = await next.render('/cases/static-rsc-dynamic-client', {})
    const $ = cheerio.load(html)

    // Confirm the HTML document was sent completely
    expect(html).toContain('</body></html>')

    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      // In dev we SSR the time
      expect($('#time').length).toBe(1)
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
// Confirm the time span is not part of the completed HTML document
      expect($('#time').length).toBe(0)
    }

const browser = await next.browser('/cases/static-rsc-dynamic-client')

    const now = new Date()

    if (isNextDev) {
expect(await browser.elementById('layout').text()).toBe('at runtime')
expect(await browser.elementById('page').text()).toBe('at runtime')
      // Assert that we rendered a time within the last couple seconds.
      const inPageDate = new Date(
        await browser.waitForElementByCss('#time').text()
      )
      expect(inPageDate.getTime() - now.getTime()).toBeLessThan(2000)
    } else {
expect(await browser.elementById('layout').text()).toBe('at buildtime')
expect(await browser.elementById('page').text()).toBe('at buildtime')
      // Assert that we rendered a time within the last 2 seconds.
      const inPageDate = new Date(
        await browser.waitForElementByCss('#time').text()
      )
      expect(inPageDate.getTime() - now.getTime()).toBeLessThan(2000)
    }
  })
})`
- ID 38:
`test/e2e/app-dir/cache-components/cache-components.web-crypto.test.ts`
— `describe('cache-components', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  it('should not have route specific errors', async () => {
    expect(next.cliOutput).not.toMatch('Error: Route "/')
expect(next.cliOutput).not.toMatch('Error occurred prerendering page')
  })

it('should prerender pages with cached `crypto.getRandomValues(...)`
calls', async () => {
let $ = await next.render$('/web-crypto/get-random-values/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })

it('should prerender pages with cached `crypto.randomUUID()` calls',
async () => {
    let $ = await next.render$('/web-crypto/random-uuid/cached', {})
    if (isNextDev) {
      expect($('#layout').text()).toBe('at runtime')
      expect($('#page').text()).toBe('at runtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    } else {
      expect($('#layout').text()).toBe('at buildtime')
      expect($('#page').text()).toBe('at buildtime')
      expect($('#first').text()).not.toEqual($('#second').text())
    }
  })
})`
- ID 40:
`test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts`
— `describe('instant validation - opting out of static shells', () => {
  const { next, isNextDev } = nextTestSetup({
    files: join(__dirname, 'fixtures', 'valid'),
  })

// NOTE: if something's wrong in build, we'll fail before any tests run.
  // Visiting the pages is mostly just a sanity check.

it('does not require a static shell if a root layouts is configured as
blocking', async () => {
    const browser = await next.browser('/blocking-root-layout')
    await browser.elementByCss('main')
    if (isNextDev) await waitForNoErrorToast(browser)
  })
it('does not require a static shell if a layout is configured as
blocking', async () => {
    const browser = await next.browser('/blocking-layout')
    await browser.elementByCss('main')
    if (isNextDev) await waitForNoErrorToast(browser)
  })
it('does not require a static shell if a page is configured as
blocking', async () => {
    const browser = await next.browser('/blocking-page')
    await browser.elementByCss('main')
    if (isNextDev) await waitForNoErrorToast(browser)
  })
})`
- ID 42:
`test/e2e/app-dir/non-rsc-router-prefetch/non-rsc-router-prefetch.test.ts`
— `describe('non-rsc-router-prefetch', () => {
  const { next } = nextTestSetup({
    files: __dirname,
  })

  beforeAll(async () => {
    const res = await next.fetch('/')
    await res.text()
  })

it('ignores the router prefetch header for HTML requests', async () => {
    const res = await next.fetch('/', {
      headers: {
        [NEXT_ROUTER_PREFETCH_HEADER]: '1',
      },
      signal: AbortSignal.timeout(5_000),
    })
    const html = await res.text()

    expect(res.status).toBe(200)
    expect(res.headers.get('content-type')).toContain('text/html')
    expect(html).toContain('hello world')
  })

  it('honors the router prefetch header for RSC requests', async () => {
    const res = await next.fetch('/', {
      headers: {
        [RSC_HEADER]: '1',
        [NEXT_ROUTER_PREFETCH_HEADER]: '1',
      },
    })

    expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('text/x-component')
  })
})`
- ID 56:
`test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts`
— `describe('use-cache-infinity-profile', () => {
  const { next, isNextStart } = nextTestSetup({
    files: __dirname,
  })

it('caches forever with a configured profile using Infinity revalidate
and expire', async () => {
    const $ = await next.render$('/')
    const initialValue = $('#value').text()
    expect(initialValue).toMatch(uuidRegExp)

// An infinite cache life must not degrade into a dynamic cache life, so
    // the value stays the same across requests instead of regenerating.
    const $second = await next.render$('/')
    expect($second('#value').text()).toBe(initialValue)

    if (isNextStart) {
      // The page must be fully prerendered at build time.
const prerendered = await next.readFile('.next/server/app/index.html')
      expect(prerendered).toContain(initialValue)
    }
  })

it('serves an inline Infinity cache life from a JSON-backed cache
handler across requests', async () => {
    const $ = await next.render$('/inline?key=a')
    const initialValue = $('#value').text()
    expect(initialValue).toMatch(uuidRegExp)

// The second request reads the entry back from the cache handler. If
the
// infinite cache life doesn't survive the handler's JSON round trip,
the
// entry is treated as immediately expired and the value regenerates.
    const $second = await next.render$('/inline?key=a')
    expect($second('#value').text()).toBe(initialValue)
  })
})`
- ID 57:
`test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts`
— `describe('use-cache-og-image-top-level-await', () => {
  const { next, isNextStart } = nextTestSetup({
    files: __dirname,
    skipStart: true,
  })

  if (isNextStart) {
    beforeAll(async () => {
await next.build({ args: ['--experimental-build-mode', 'compile'] })
    })

it('should prerender a page whose opengraph image uses a top-level
await', async () => {
      const { exitCode, cliOutput } = await next.build({
        args: [
          '--experimental-build-mode',
          'generate',
          '--debug-build-paths',
          'app/[slug]/page.tsx,app/[slug]/opengraph-image.tsx',
        ],
      })

      expect(cliOutput).not.toContain(
        'Unexpected cache miss after cache warming phase'
      )
      expect(cliOutput).not.toContain(
'Next.js encountered uncached or runtime data in `generateMetadata()`'
      )
      expect(exitCode).toBe(0)

// The image route uses generateStaticParams, so the build is expected
      // to prerender it for each param.
      expect(cliOutput).toMatch(/● \/first-post\/opengraph-image/)
      expect(cliOutput).toMatch(/● \/second-post\/opengraph-image/)
    })
  } else {
    beforeAll(async () => {
      await next.start()
    })

it('should render a page whose opengraph image uses a top-level await',
async () => {
      const $ = await next.render$('/first-post')
      expect($('article').text()).toBe('First Post')

      const res = await next.fetch('/first-post/opengraph-image')
      expect(res.status).toBe(200)
      expect(res.headers.get('content-type')).toBe('image/png')
    })
  }
})`
- ID 58:
`test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts`
— `describe('use-cache-output-export', () => {
  const { next, isNextStart } = nextTestSetup({
    files: __dirname,
    skipStart: process.env.NEXT_TEST_MODE !== 'dev',
  })

  if (process.env.__NEXT_CACHE_COMPONENTS === 'true') {
    return it.skip('for PPR', () => {
      // PPR is not compatible with `output: 'export'`.
    })
  }

  it('should work', async () => {
    let html: string
    let server: Server | undefined

    if (isNextStart) {
      const { cliOutput } = await next.build()

      expect(cliOutput).not.toInclude(
        'Server Actions are not supported with static export.'
      )

      server = await startCleanStaticServer(join(next.testDir, 'out'))
      const { port } = server.address() as AddressInfo
      html = await renderViaHTTP(port, '/')
    } else {
      html = await next.render('/')
    }

    expect(html).toMatch(/<p>[0,1]\.\d+<\/p>/)

    if (server) {
      await new Promise((resolve) => server.close(resolve))
    }
  })
})`

</details>

<details>
<summary>Deployment evidence for the additional scopes</summary>

- `test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts` —
`describe('app dir - prefetching (custom staleTime)', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710370);
Cache Components excluded by manifest.
- `test/e2e/app-dir/cache-components-errors/module-scope.test.ts` —
`describe('Lazy Module Init', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710410);
Cache Components excluded by manifest.
- `test/e2e/app-dir/cache-components/cache-components.params.test.ts` —
`describe('cache-components', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710449);
Cache Components excluded by manifest.
-
`test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts`
— `describe('instant validation - opting out of static shells', () =>
{`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710456),
[cache](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710457).
-
`test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts`
— `describe('use-cache-output-export', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710496),
[cache](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710503);
Cache Components explicitly skipped for PPR.

</details>

<!-- NEXT_JS_LLM -->
2026-09-15 10:29:05 -07:00
Josh Story e0966df114 test: migrate caching deploy exclusions to force gates (#98154)
## Summary

- migrate Cache Components, PPR, prefetching, prerendering,
revalidation, and SSG deployment exclusions to `@force-gate`
- remove the corresponding `skipDeployment` options and `skipped`
control flow while preserving each suite's rationale

This keeps caching-related exclusions together so their deployed
semantics can be reviewed by the same owners.

## Verification

- verified on the combined top-of-stack tree with `pnpm typescript`
- compared ordinary-mode collection before and after: all 4,670 existing
test names matched
- collected all 510 affected files in deploy mode: 4,578 skipped tests,
zero failures

<!-- NEXT_JS_LLM -->
2026-09-09 08:25:36 -07:00
Benjamin Woodruff 390eff3b86 [ci] Share a single browser instance across all test suites in a single job (#95589)
The idea here is that we waste a bunch of CPU setting up each test suite
by spawning an entirely new Chrome process, but playwright can share the
same browser across many tests. Tests are still isolated at the browser
level, just not at the OS process level.

Hoping to see some performance improvement in e2e tests. Compare to:
https://github.com/vercel/next.js/pull/95617

Somewhat unscientific (single sample) results:

```
Comparing the head-commit build-and-test runs — PR #95589 run [28981702219 ](https://github.com/vercel/next.js/actions/runs/28981702219)vs PR #95617 run [28981686692](https://github.com/vercel/next.js/actions/runs/28981686692). Both ran 101 jobs and both succeeded.

  ┌─────────────────────────┬────────────────┬───────────────────────┬───────────────────┐
  │         Metric          │ #95617 control │ #95589 shared browser │       Delta       │
  ├─────────────────────────┼────────────────┼───────────────────────┼───────────────────┤
  │ Raw wall-time sum       │ 616.2 min      │ 571.2 min             │ −45.0 min (−7.3%) │
  ├─────────────────────────┼────────────────┼───────────────────────┼───────────────────┤
  │ Billable (ceil per job) │ 658 min        │ 617 min               │ −41 min (−6.2%)   │
  └─────────────────────────┴────────────────┴───────────────────────┴───────────────────┘

  The shared-browser experiment is cheaper, and the savings land almost exactly where you'd expect — the browser-driven test suites — while non-browser jobs (rust, lint, unit, windows) are flat within noise:

  ┌─────────────────────────────────┬─────────┬────────┬───────┐
  │          Job category           │ control │ shared │ delta │
  ├─────────────────────────────────┼─────────┼────────┼───────┤
  │ test prod                       │   130.7 │  110.5 │ −20.2 │
  ├─────────────────────────────────┼─────────┼────────┼───────┤
  │ cache components dev            │    51.5 │   42.9 │  −8.7 │
  ├─────────────────────────────────┼─────────┼────────┼───────┤
  │ turbopack dev                   │    75.1 │   69.5 │  −5.5 │
  ├─────────────────────────────────┼─────────┼────────┼───────┤
  │ turbopack production            │    86.3 │   81.1 │  −5.2 │
  ├─────────────────────────────────┼─────────┼────────┼───────┤
  │ test dev                        │   110.4 │  105.5 │  −4.9 │
  ├─────────────────────────────────┼─────────┼────────┼───────┤
  │ cache components prod           │    48.7 │   49.0 │  +0.3 │
  ├─────────────────────────────────┼─────────┼────────┼───────┤
  │ firefox and safari              │     5.3 │    7.3 │  +2.0 │
  ├─────────────────────────────────┼─────────┼────────┼───────┤
  │ flake-detection jobs (combined) │     ~15 │    ~21 │   +~6 │
  └─────────────────────────────────┴─────────┴────────┴───────┘

  Takeaways:
  - Net savings of ~41 billable minutes per run (~6%), concentrated in the Chromium-driven test prod/dev, turbopack, and cache components suites — consistent with reusing one browser process instead of spawning per-suite.
  - The small regressions are in firefox and safari and the "new/changed tests for flakes" jobs (+~8 min combined). Worth a glance, though they may just be run-to-run variance.

  Caveat: this is a single run per PR, so there's real variance run-to-run. That said, the fact that the deltas track the browser-heavy jobs specifically — and not the Rust/lint/unit jobs — is a good signal the effect is genuine rather than noise. If you want a firmer number, re-running each PR 2–3× and averaging would tighten it up.
```
2026-07-21 01:36:10 +00:00
Zack Tanner a430936c7f Strengthen _rsc cache-busting param (#92755)
This updates `_rsc` cache-busting param generation to use a stronger
digest so different RSC request variants are much less likely to produce
the same value.

It also updates the related request-validation and injected
resume/instant-test paths so they continue to generate and validate the
same `_rsc` values across client, server, and edge-reachable codepaths.
2026-04-21 13:55:58 -07:00
Hendrik Liebau c53f5863e3 [test] Unflake app prefetch test (#91734)
[Flakiness
metric](https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%22fail%22%20%40test.name%3A%22app%20dir%20-%20prefetching%20should%20not%20unintentionally%20modify%20the%20requested%20prefetch%20by%20escaping%20the%20uri%20encoded%20query%20params%22&agg_m=count&agg_m_source=base&agg_t=count&fromUser=false&index=citest&start=1773424617562&end=1774029417562&paused=false)

Another victim of the Playwright upgrade. Adding a `retry` to the
assertion should fix the flakiness, as it seems that the element is not
always immediately visible after the click.
2026-03-23 11:27:50 +01:00
Andrew Clark df247d1b51 Move prefetch hints from CacheNodeSeedData to FlightRouterState (#90066)
## Summary

Prefetch hints were previously stored in CacheNodeSeedData — the payload
the server sends for each segment's React content. This moves them to
FlightRouterState, the route tree structure that the server sends
alongside the seed data. FlightRouterState is a better home because
prefetch hints describe properties of the route structure (loading
boundaries, instant configs) rather than properties of the rendered
content. It also ensures that hints are always available during
navigations, not just prefetches.

Since this changes the FlightRouterState tuple layout anyway, it also
folds the existing `isRootLayout` boolean and `HasLoadingBoundary` enum
into the same bitmask field at FlightRouterState[4].

## Test plan

- Existing unit tests updated for new format
(`flight-data-helpers.test.ts`, `types.test.ts`)
- MPA navigation test validates root layout change detection via bitmask
- `prefetch-true-instant` e2e test validates Link `prefetch={true}`
behavior with instant routes
2026-02-17 16:39:18 -05:00
Andrew Clark d3de9bce9b Embed static sibling info in route tree (#88692)
This change adds infrastructure for a planned client-side optimization
where the router can infer route structure from previously prefetched
routes, avoiding redundant route tree requests.

When the client prefetches /blog/post-1 and learns the route structure
is /blog/[slug], it could potentially reuse that knowledge for
/blog/post-2 without another server roundtrip. However, if a static
route like /blog/featured exists at the same level, the client needs to
know NOT to apply the dynamic template for that URL.

staticSiblings provides this information. When a dynamic segment like
[slug] has static siblings (e.g., "featured", "archive"), the server
includes them in the prefetch response. The client can then check
whether an incoming URL matches a known static sibling, and if so,
recognize that the previously learned route structure does not apply.
2026-01-20 22:50:49 -05:00
Sebastian "Sebbie" Silbermann bbfd5fb29c [test] Disable flaky prefetching.stale-times test (#86299) 2025-11-19 16:39:54 +01:00
Sebastian "Sebbie" Silbermann bfe2ab445b [test] Deflake prefetching.stale-times (#85733) 2025-11-03 16:03:23 +01:00
Sebastian "Sebbie" Silbermann cb01c9ea8f [test] Separate act and assertions (#85508)
Moves the assertions out of `act` and removes redundant waiting for
elements. Similar to https://github.com/vercel/next.js/pull/85417
2025-10-30 13:18:28 +00:00
Sebastian "Sebbie" Silbermann 721eb61551 [test] Speed up prefetching suite (#85417) 2025-10-27 23:06:41 +00:00
Zack Tanner dd5e3ad611 [ci]: move router-act to top level testing utils (#84653)
We're starting to use this pattern in more places, so this moves `router-act` into our top level testing utils.
2025-10-08 17:01:40 -07:00
Zack Tanner 9c053332cc [test]: convert prefetching tests to be segmentCache compatible (#84521)
Refactors some older prefetching tests to be compatible with
`clientSegmentCache`. The old tests were too tightly coupled to router
implementation details. Some of the ones that don't make sense to
convert were removed (for ex, asserting on prefetch cache seeding
behavior, or fetch priority of "high", as fetch priority is managed
differently with the new router).

This refactors the tests to leverage the `act` util from `segment-cache`
test suites, and implemented the accordion behavior to more granularly
control when prefetches are dispatched. I copied those implementations
for now since we don't have a global util for it.
2025-10-06 12:12:11 -07:00
Janka Uryga ee284b8ba1 refactor: lowercase app router header values (#82169)
we define most of the app router headers with capital letters, like
this:
```ts
export const NEXT_ROUTER_PREFETCH_HEADER = 'Next-Router-Prefetch' as const
```
which means we have to keep lowercasing the header everywhere when
checking for its presence, because node and other intermediate layers
normalize all headers to lowercase:
```ts
if (req.headers[NEXT_ROUTER_PREFETCH_HEADER.toLowerCase()]) { ... }
```
this seems pretty pointless -- we can just define the headers in
lowercase form, and skip all those `toLowerCase()` calls.

(all the above sentences have been normalized to lowercase to match the
spirit of this pr)
2025-07-30 13:42:16 +02:00
Jude Gao 5c25087d57 Fix validateRSCRequestHeaders incorrect redirect (#82119)
Found during dogfooding on Vercel that sometimes the RSC validation
fails incorrectly. The root cause was that headers like
`Next-Router-Prefetch` were absent from the header and transferred to
request meta. Eventually we should improve how we retrieve these fields
consistently.
2025-07-28 15:18:20 -04:00
Zack Tanner 65e0b7b757 bugfix: static resources staleTime should be renewed once refetched (#81771)
When the `x-nextjs-staletime` header is sent from the server, the client
router was discarding all of its staleTime heuristics (such as
lastUsedTime and "reusable" cache entries). This caused an issue where
after the static staleTime window expired and the prefetch data was
renewed, the static staleTime itself was not being renewed.

As a result, once the static staleTime expired, the client would
continuously refetch the resource on every navigation, even though it
had just been renewed.

This removes the `x-nextjs-staleTime` logic from this heuristic because
it was only intended to be considered for the new `clientSegmentCache` &
cache components work
2025-07-17 13:31:55 -07:00
Zack Tanner 45a5405de2 bugfix: propagate staleTime to seeded prefetch entry (#81263)
In #71280, we hooked up the server-side `staleTime` header to the
client-side prefetch cache. This means that if the server responds with
a staleTime value, the client router will use that rather than the
previous heuristic when determining if a prefetch entry can be reused.

However, there was missing functionality to set a proper staleTime value
for the initially seeded prefetch entry (aka the first page visit). In
the case of customizing staleTimes, this meant that navigations back to
the original page would defer back to the old stale heuristic, which
would differ from subsequent navigations that were properly seeded with
a staleTime.

Separately, I believe that it's problematic that if the `staleTime`
header is present, that we honor that and only set a fresh/stale cache
status, because the newer heuristic doesn't consider the `reusable`
cache status. This should probably be flagged to the clientSegmentCache
flag. However, to minimize the changes here, I've only addressed this
particular case.
2025-07-03 14:58:27 -07:00
Jude Gao 6fd6ed1ea2 Check cache busting search params on all RSC requests (#80669)
For all RSC requests, we require the `_rsc` search param to match the
hash of its corresponding RSC headers. In the case where the match
fails, we respond with a redirect to the correct search param. This hash
check only applies to `next start` mode for now.
2025-06-25 12:46:29 -04:00
Sebastian "Sebbie" Silbermann 100881542a [test] Use NEXT_TEST_CI when forking test in CI (#79354) 2025-05-21 20:29:19 +02:00
Hendrik Liebau b42b442ead Client router should discard stale prefetch entries for static pages (#79309)
When navigating to a static page that was previously prefetched, the
client router should discard a stale prefetch entry, and fetch the page
again, if the stale time has passed.

The selected stale time is either the default value of 5 minutes, a
custom value set via `experimental.staleTimes.static`, or a stale value
set via a `cacheLife` profile when using `"use cache"`.

This also fixes an issue where the client router interpreted the stale
time value as milliseconds, whereas the server sends it in seconds. This
was previously already fixed with `clientSegmentCache` enabled in
#74759.

fixes #74272
fixes #79093
Closes NEXT-4109
2025-05-16 22:54:56 +02:00
Janka Uryga 4282bc6f91 remove BrowserInterface (#78308)
rip. you served a purpose once, but now you're just getting in the way. this improves typesafety quite a bit, because `BrowserInterface` had a whole bunch of random `any`s everywhere

also
- removes `evalAsync`. no idea why that was needed, but we're happily using promises in normal eval, so it can be dropped
- adds more safety to `chain`
2025-04-18 11:16:15 -07:00
Jude Gao 60eb702f45 fix: enhance a11y, prevent double firing in error rating (#74563)
Improved a11y (aria-live, aria-label) and simplified loading/error state
to prevent double firing when feedback buttons get clicked multiple
times while loading.
2025-01-06 21:13:00 +00:00
Jude Gao bd96b91dbf Fix: Preserve intentional percent encoding in search param for client nav (#74473) 2025-01-03 09:03:30 -05:00
Hendrik Liebau e08fe27ab7 Restore RSC fetch error handling after navigating back (#73985)
As a follow-up to #73975, we're now restoring the error handling for
failed RSC fetch calls when the user navigates back after a hard
navigation. In this case the browser uses the bfcache to restore the
previous JavaScript execution context, and thus the abort controller
will still be in the aborted state. To take this into account, we're now
creating a new `AbortController` instance on `'pageshow'` events.

In addition, the abort controller's `signal` is now actually passed to
the `fetch` call, and not only used for the error handling, so that the
requests are aborted on `'pagehide'`. This was an oversight in the
original PR. With that, it's even more important to create a fresh abort
controller, otherwise RSC fetching would be disabled after back
navigation.

The added e2e test can only run in headed mode unfortunately, as the
bfcache is not available in headless mode. (Using the same approach as
in #54081.)
2024-12-16 20:32:52 +00:00
Hendrik Liebau ac89400a2a Ignore RSC fetch errors after hard navigation (#73975)
When the user reloads the page or navigates away while RSC fetch
requests are still in flight – most notably background prefetches – we
currently log the following error:

```
Failed to fetch RSC payload for <URL>. Falling back to browser navigation.
```

This unnecessarily clutters the DevTools console and creates noise in
remote error reporting tools.

When the browser cancels a `fetch` request due to navigation, it throws
`TypeError: Failed to fetch`. We shouldn't universally suppress this
error since it may also occur for other valid reasons where we do want
to log it.

To suppress the error only in genuine navigation scenarios, we introduce
an `AbortController` and pass its signal to the `fetch` call. On
`'pagehide'` events, we abort the controller. This approach ensures that
error log suppression is limited solely to these navigation-related
cases.

[x-ref](https://vercel.slack.com/archives/C0676QZBWKS/p1734009715952909)
fixes #60549
2024-12-16 13:14:33 +01:00
Zack Tanner 1734e91ac8 fix loading bailout condition & server patch mismatch tolerance (#72227)
Pre-PPR on client navigations, we are currently determining whether or
not to render the underlying React components based on the presence (or
lack thereof) of `loading.js`. We'll render components up to the first
segment containing `loading.js`, and then stop at that level.

Similarly, if there's no `loading.js` in the tree at all, we'll return a
mostly empty prefetch. The underlying reason for this is that we cannot
assume a particular tree/subtree doesn't contain expensive invocations:
`loading.js` is a signal we use to detect where some sort of dynamic
call will happen. This heuristic is largely improved with PPR + DIO but
we still need to support this case.

Currently there's a bug that will lead to overfetching. When determining
if we should skip the component tree, we check the presence of loading
at that level, as well as the entire tree. This means that during
partial navigations when the server starts rendering deeper within the
tree rather than starting from the root, the presence of `loading.js` in
an earlier segment would incorrectly tell the router it should render
the page.

This updates the handling to check for a loading segment only within the
partial tree that's being diffed. That way when we recursively call
this, it won't look at earlier segments when deciding if there's a
loading segment deeper in the tree.

This PR also adjusts the server-patch reducer to be more resilient to
mismatching trees. Previously we triggered a hard navigation but this
actually can become a fairly common case if the RSC response doesn't
return initial bytes quickly enough, such as in `dynamicIO` when we're
warming the dev caches prior to a navigation. Instead we'll return the
state in-place and effectively discard the patch because the assumption
is the tree was patched elsewhere.
2024-11-04 15:09:47 -08:00
Jiachi Liu f91cdec44d test: migrate rest async api usage in tests (#71663) 2024-10-23 02:36:20 +02:00
Zack Tanner 98ffc0e179 initially visited static pages should respect static staletime (#70640)
This provides information to the initial SSR render that the client
leverages when seeding the prefetch cache, to determine if a page was a
fully static prerender.

The side effect of not doing this is that if we seeded a static page
during SSR, and then navigated to it, the router would make a request
for data that it didn't need. In other words, it didn't honor the static
staletime, like other navigations would.
2024-10-01 14:01:11 -07:00
Zack Tanner 0249bdf3c1 bugfix: only replace with full prefetch if existing data was partial (#70650)
By default we are storing the seeded prefetch entry (from the SSR
render) as an "auto" prefetch. The prefetch cache is used because every
navigation checks against the prefetch cache to determine if it has the
necessary data to render the requested segment(s).

We have a router heuristic that will re-fetch a page that's already in
the prefetch cache if the one that is stored is an "auto" prefetch, and
the newly requested prefetch is a "full" prefetch. This is because the
assumption is the "auto" prefetch would contain partial data
(potentially only `loading.js` data) while the "full" prefetch has
everything, so we'd want to replace the cache entry with the most up to
date information.

Currently when we seed the prefetch cache with the initially SSRed page,
we set a cache status of "auto". This is because a "full" prefetch has
staleTime implications: a full prefetch will be client cached for 5
minutes by default (the `static` staleTime), and since we have no
prefetch intent during SSR, we should fallback to the automatic caching
heuristics to avoid caching a page longer than what was intended.
However, this will trigger the above mentioned logic to switch to a
"full" prefetch if the page you're currently on has a link to itself,
with `prefetch={true}`, resulting in a wasted request.

This refactors the logic responsible for switching a prefetch entry to a
"full" prefetch only once we confirm the response payload from the
existing cache entry wasn't a response that started rendering from the
root. If we started rendering from the root, we know that we have the
full data already, so it'd be wasteful to fetch again.

Fixes #70535
2024-10-01 07:51:03 -07:00
Josh Story 05f159dffc [Breaking] Update Dynamic APIs to be async (#68812)
Next.js has a number of dynamic APIs that aren't available during
prerendering. What happens when you access these APIs might differ
depending on the mode you are using, for instance if you have PPR turned
on or the newly introduced `dynamicIO` experimental mode. But regardless
of the mode the underlying API represents accessing something that might
only be available at render time (dynamic rendering) rather than
prerender time (build and revalidate rendering)

Unfortunately our current dynamic APIs make certain kinds of modeling
tricky because they are all synchronous. For instance if we wanted to
add a feature to Next.js where we started a dynamic render before a
Request even hits the server it would be interesting to be able to start
working on everything that does not rely on any dynamic data and then
once a real Request arrives we can continue the render and provide the
associated Request context through our dynamic APIs.

If our dynamic APIs were all async we could build something like this
because they represnt a value that will eventually resolve to some
Request value. This PR updates most existing dynamic APIs to be async
rather than sync. This is a breaking change and will need to be paired
with codemods to realistically adopt. Additionally since this change is
so invasive I have implemented it in a way to maximize backward
compatibility by still allowing most synchronous access. The combination
of codemods, typescript updates, and backward compat functionality
should make it possible for projects to upgrade to the latest version
with minimal effort and then follow up with a complete conversion over
time.

#### `cookies()`
`cookies()` now returns `Promise<ReadonlyRequestCookies>`. Synchronous
access to the underlying RequestCookies object is still supported to
facilitate migration.
```tsx
// ------------ preferred usage

// async Server Component
const token = (await cookies()).get('token')

// sync Server Component
import { use } from 'react'
//...
const token = use(cookies()).get('token')

// ------------ temporarily allowed usage

// javascript, dev warning at runtime
const token = cookies().get('token')

// typescript, dev warning at runtime
import { type UnsafeUnwrappedCookies } from 'next/headers'
// ...
const token = (cookies() as unknown as UnsafeUnwrappedCookies).get('token')
```

#### `headers()`
`headers()` now returns `Promise<ReadonlyHeaders>`. Synchronous access
to the underlying Headers object is still supported to facilitate
migration.
```tsx
// ------------ preferred usage

// async Server Component
const header = (await headers()).get('x-foo')

// sync Server Component
import { use } from 'react'
//...
const header = use(headers()).get('x-foo')

// ------------ temporarily allowed usage

// javascript, dev warning at runtime
const header = headers().get('x-foo')

// typescript, dev warning at runtime
import { type UnsafeUnwrappedHeaders } from 'next/headers'
// ...
const header = (headers() as unknown as UnsafeUnwrappedHeaders).get('x-foo')
```


#### `draftMode()`
`draftMode()` now returns `Promise<DraftMode>`. Synchronous access to
the underlying DraftMode object is still supported to facilitate
migration.
```tsx
// ------------ preferred usage

// async Server Component
if ((await draftMode()).isEnabled) { ... }

// sync Server Component
import { use } from 'react'
//...
if (use(draftMode()).isEnabled) { ... }

// ------------ temporarily allowed usage

// javascript, dev warning at runtime
if (draftMode().isEnabled) { ... }

// typescript, dev warning at runtime
import { type UnsafeUnwrappedDraftMode} from 'next/headers'
// ...
if ((draftMode() as unknown as UnsafeUnwrappedDraftMode).isEnabled) { ... }
```

#### `searchParams`
`searchParams` is now a `Promise<{...}>`. Synchronous access to the
underlying search params is still supported to facilitate migration.
```tsx
// ------------ preferred usage

// async Page Component
export default async function Page({
  searchParams
}: {
  searchParams: Promise<{ foo: string }>
}) {
  const fooSearchParam = (await searchParams).foo
}

// sync Page Component
import { use } from 'react'
export default function Page({
  searchParams
}: {
  searchParams: Promise<{ foo: string }>
}) {
  const fooSearchParam = use(searchParams).foo
}

// ------------ temporarily allowed usage

// javascript, dev warning at runtime
export default async function Page({ searchParams}) {
  const fooSearchParam = searchParams.foo
}

// typescript, dev warning at runtime
import { type UnsafeUnwrappedSearchParams } from 'next/server'
export default async function Page({
  searchParams
}: {
  searchParams: Promise<{ foo: string }>
}) {
  const syncSearchParams = (searchParams as unknown as UnsafeUnwrappedSearchParams<typeof searchParams>)
  const fooSearchParam = syncSearchParams.foo
}
```



#### `params`
`params` is now a `Promise<{...}>`. Synchronous access to the underlying
params is still supported to facilitate migration. It should be noted
that while params are not usually dynamic there are certain modes where
they can be such as fallback prerenders for PPR.
```tsx
// ------------ preferred usage

// async Segment Component
export default async function Layout({
  params
}: {
  params: Promise<{ foo: string }>
}) {
  const fooParam = (await params).foo
}

// sync Segment Component
import { use } from 'react'
export default function Layout({
  params
}: {
  params: Promise<{ foo: string }>
}) {
  const fooParam = use(params).foo
}

// ------------ temporarily allowed usage

// javascript, dev warning at runtime
export default async function Layout({ params}) {
  const fooParam = params.foo
}

// typescript, dev warning at runtime
import { type UnsafeUnwrappedParams } from 'next/headers'
export default async function Layout({
  params
}: {
  params: Promise<{ foo: string }>
}) {
  const syncParams = (params as unknown as UnsafeUnwrappedParams<typeof params>)
  const fooSearchParam = syncParams.foo
}
```

### Typescript Changes
When using typescript with Next.js currently it is up to you to author
types for Pages, Layouts and other Segment components that recieve props
like `params` and `searchParams`.

Next comes with some build-time type checking to ensure you have not
improperly typed various top level module exports however the current
type assertions for `params` and `searchParams` is `any`. This isn't
very helpful because it allows you to erroneously type these props.

`searchParams` is tricky because while the default type is a dictionary
object parsed using node.js url parsing it is possible to customize when
running a custom Next.js server. However we can ensure that you
correctly type the prop as a Promise so with this change the validated
type for `searchParams` will be `Promise<any>`.

In the long run we will look at updating the `searchParams` underlying
type to be URLSearchParams so we can move away from supporting
customized parsing during rendering and we can get even more explicit
about valid types.

`params` is more straight forward because the framework controls the
actual `params` prop implementation and no customization is possible. In
the long run we want to enforce you are only typing params that are
valid for the Layout level your file is located in but for now we are
updating the allowed type to be `Promise<{[key: string]: string |
string[] | undefined }>`.

These new type restrictions may also require fixes before being able to
successfully build a project that updates to include these breaking
changes. These changes will also not always be codemodable because it is
valid to type the entire component using an opaque type like `Props`
which our codemods may not have an ability to introspect or modify.
2024-09-25 15:44:37 -07:00
Zack Tanner cc92654396 fix app router prefetch deduping (#68642)
The `Link` component has built in prefetch deduping behavior that was
added for pages router, since pages only supports a single kind of
prefetch.

Both pages & app routers share the `Link` component, meaning the
deduping behavior was enabled for both. However, app router supports
different [prefetch
types](https://nextjs.org/docs/app/api-reference/components/link#prefetch).
This means that if a page contains a link to a URL with auto
prefetching, followed by a URL with `prefetch={true}`, the full prefetch
would not be applied because it was short-circuited by `Link`.

Closes NDX-124
2024-08-07 18:35:34 -07:00
Zack Tanner 8daa59eb7a prerendered pages should use static staleTime (#67868)
### What
Prefetches for static pages currently aren't distinguished from dynamic
pages insofar as their client router cache TTL is concerned. This meant
pre v15, both static and dynamic pages leveraged the 30s cache. In v15,
this meant it would refetch the static data every navigation, since we
changed the `dynamic` staleTime value to `0`. The only way to force a
prefetch to use the static staletime was via `prefetch={true}` on a link
(or `router.prefetch`).

### Why
The router had no way of distinguishing between static or dynamic
responses. The only difference was the RSC payload between the two,
since a static response would contain the full tree, and a dynamic
response would potentially be short-circuited with `loading.js` or have
a fairly minimal response for just basic metadata.

### How
This introduces a header the client can leverage to determine if the
response is a prerender. If it is, we mark the "auto" prefetch entry as
"full" so that it respects the `static` staleTime value (current default
is 5min).

We use a new header, rather than piggybacking on `x-nextjs-cache` /
`x-vercel-cache`, because a static page can technically have a `MISS`
status if it's revalidating.

Fixes #66513

Closes NDX-65
2024-07-18 10:58:28 -07:00
Zack Tanner 2fbdac6f82 fix prefetch bailout detection for nested loading segments (#67358)
### What
When PPR is off, app router prefetches will render the component tree up
until it encounters a `loading` segment, at which point it'll just
return some metadata about the segment and won't do any further
rendering. This is an optimization to ensure prefetches are lightweight
and don't potentially invoke expensive dynamic subtrees. However,
there's a bug in this logic that is causing it to bail unexpectedly if a
segment deeper in the tree contained a `loading.js` file.

This would mean the loading state wouldn't be triggered until the second
request for the full RSC data is initiated, resulting in an unintended
delta between when a link is clicked and the loading state is shown.

### Why
The `shouldSkipComponentTree` flag was incorrectly being set to `true`
even if the `loading.js` segment appeared deeper in the tree. Prefetch
requests from the client will always contain `FlightRouterState`, so the
logic to check for loading deeper in the tree will always be missed.


### How
This removes the `flightRouterState` check as it doesn't make sense:
prefetches will currently _always_ include the `flightRouterState`,
causing this to always short-circuit. I believe that this check is
vestigial from when we were generating static `prefetch.rsc` outputs
which is no longer the case.

This mirrors a [similar
check](https://github.com/vercel/next.js/blob/b87d8fc49983a3be568517d7ae14087749bb8ce3/packages/next/src/server/app-render/create-component-tree.tsx#L393)
when determining if parallel route(s) should be rendered.
2024-07-01 16:11:09 -07:00
Zack Tanner aa832c3b2d use fetch priority for app prefetches (#67356)
This leverages [fetch
priority](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#priority)
to ensure automatic prefetching as a result of visiting a page is sent
with "low" priority, to signal to the browser it can prioritize more
important work necessary for rendering the page.

A "temporary" prefetch (ie one that is created when there wasn't an
existing prefetch cache entry on navigation) will use a "high" priority
because it's critical to the navigation event.

All other cases will be "auto" which is the current default.
2024-07-01 14:47:56 -07:00
Janka Uryga 0fee50ed6e fix: app-router prefetch crash when an invalid URL is passed to Link (#66755)
Closes [#66650](https://github.com/vercel/next.js/issues/66650)
Closes NEXT-3520

### What?

- Make Link not throw during prefetch if it received an invalid `href`
(see [#66650](https://github.com/vercel/next.js/issues/66650))
- Throw in dev mode if an invalid link was passed to `router.prefetch`
-- this matches current prod behavior
- (previously, we'd immediately exit out of `router.prefetch`, so the
user would see no indication that this'd fail in prod)

### Why?

If an invalid URL was passed to `<Link>`, the whole app would crash and
show "A client-side exception occurred". A failed prefetch should not
bring down the whole app.

Note that This preserves the current behavior of explicit
`router.prefetch(url)` throwing an error if `url` is invalid. We may
want to adjust this in the future, but that could be considered a
breaking change, so I'm leaving it out for now. This only affects
`Link`, which was intended to catch any errors thrown from
`router.prefetch`, but that bit was accidentally broken.
2024-06-11 21:36:27 +02:00
Zack Tanner 1f545aff05 [Breaking] disable client router cache for page segments (#66039)
This configures the default client router cache `staleTime.dynamic`
value to be `0`.

This means that:
- Navigating between pages will always fire off a network request to get
RSC data for the page segment, rather than restoring from router cache
- Loading states will remain cached for 5 minutes (or whatever
`config.experimental.staleTimes.static` is set to)
- Shared layout data will continue to remain cached due to [partial
rendering](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering)
- Back/forward behavior will still restore from cache to ensure the
browser can restore scroll position.

It's possible to opt-out of this, and into the previous behavior, by
setting the
[`staleTimes`](https://nextjs.org/docs/app/api-reference/next-config-js/staleTimes)
config in `next.config.js`:

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30
    },
  },
}

module.exports = nextConfig
```

<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->
2024-05-21 13:54:23 -07:00
Wyatt Johnson c6320ed87a Replace createNextDescribe with nextTestSetup (#64817)
<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->

I took some time and [wrote a
codemod](https://gist.github.com/wyattjoh/0d4464427506cb02062a4729ca906b62)
that replaces the old usage of the `createNextDescribe` with the new
`nextTestSetup`. You'll likely have to turn on hiding of whitespace in
order to review, but this should primarily introduce no changes to the
test structure other than using the new mechanism now.

Closes NEXT-3178
2024-04-25 12:06:12 -06:00
Zack Tanner e9aeb9e1b9 memoize layout router context (#64575)
Since `AppRouterState` is promise-based (so we can `use` the values and
suspend in render), the state itself isn't stable between renders. Each
action corresponds with a new Promise object. When a link is hovered, a
prefetch action is dispatched, even if the prefetch never happens (for
example, if there's already a prefetch entry in the cache, and it
doesn't need to prefetch again). In other words, the prefetch action
will be dispatched but won't necessarily change the state.

This means that these no-op actions that don't actually change the state
values will trigger a re-render. Most of the context providers in
`AppRouter` are memoized with the exception of `LayoutRouter` context.
This PR memoizes those values so that consumers are only notified of
meaningful updates.

Fixes #63159


Closes NEXT-3127
2024-04-16 16:23:39 -07:00
Zack Tanner b9861fd2cd only prefix prefetch cache entries if they vary based on Next-URL (#61235)
### What
Prefetches to pages within a shared layout would frequently cache miss
despite having the data available. This causes the "instant navigation"
behavior (with the 30s/5min TTL) to not be effective on these pages.

### Why
In #59861, `nextUrl` was added as a prefetch cache key prefix to ensure
multiple interception routes that correspond to the same URL wouldn't
clash in the prefetch cache. However this causes a problem in the case
where you're navigating between sub-pages. To illustrate the issue,
consider the case where you load `/foo`. This will populate the prefetch
cache with an entry of `{foo: <PrefetchCacheNode}`. Navigating to
`/foo/bar`, with a link that prefetches back to `/foo`, will now result
in a new cache node: `{foo: <PrefetchCacheNode>, /foo/bar%/foo:
<PrefetchCacheNode>}` (where `Next-URL` is `/foo/bar`). Now we have a
cache entry for the full data, as well as a cache entry for a partial
prefetch up to the nearest loading boundary. Now when we navigate back
to `/foo`, the router will see that it's missing data, and need to
lazy-fetch the data triggering the loading boundary.

This was especially noticeable in the case where you have a route group
with it's own loading.js file because it creates a level of hierarchy in
the React tree, and suspending on the data fetch would result in the
group's loading boundary to be triggered. In the non-route group
scenario, there's still a bug here but it would stall on the data fetch
rather than triggering a boundary.

### How
In #61794 we conditionally send `Next-URL` as part of the `Vary` header
if we detect it could be intercepted. We use this information when
creating the prefetch entry to prefix it, in case it corresponds with an
intercepted route.

Closes NEXT-2193
2024-02-13 15:03:37 +00:00
Zack Tanner 59f7ca85c2 remove additional static prefetch code (#59313)
This is a continuation from https://github.com/vercel/next.js/pull/58783
to remove the remaining code related to static prefetching.
2023-12-05 21:29:23 -08:00
Zack Tanner 3043feef44 disable static prefetching behavior for dynamic segments (#58609)
### What?
When a layout segment forces dynamic rendering (such as with
`force-dynamic` or `revalidate: 0`), navigating to sub-pages of that
layout will attempt to re-render the layout, also resulting in side
effects re-running. This means if your layout relies on a data fetch and
you render the result of that data in the layout, it will unexpectedly
change when navigating between sub-paths, as described in #57326.

As a separate issue (but caused by the same underlying mechanism), when
using `searchParams` on a dynamic page, changes to those search params
will be erroneously ignored when navigating, as described in #57075

### Why?
As a performance optimization we generate static prefetch files for
dynamic segments ([original
PR](https://github.com/vercel/next.js/pull/54403)). This makes it so
that when prefetching is turned on, the prefetch can be served quickly
from the edge without needing to invoke unnecessarily. We're able to
eagerly serve things that can be safely prefetched. This is nice for
cases where a path has a `loading.js` that we can eagerly render while
waiting for the dynamic data to be loaded.

This causes a problem with layouts that opt into dynamic rendering: when
the page loads and a prefetch is kicked off for the sub-page, it'll load
the static prefetch, which won't be generated with the same router state
as the dynamically rendered page. This causes a mismatch between the two
trees, and when navigating within the same segment, a refetch will be
added to the router because it thinks that it's navigating to a new
layout.

This also causes issues for dynamic pages that use `searchParams`. The
static prefetch will be generated without any knowledge of search
params, and when the prefetch occurs, we still match to the prefetch
generated without search params. This will make the router think that no
change occurs, and the UI will not update to reflect the change.

### How?
There's ongoing work by @acdlite to refactor the client router.
Hopefully it will be easier to re-land this once that work is finished.
For now, I've reverted the behavior as it doesn't seem to be worth the
bugs it currently causes. I've also added tests so that when we do
re-land this behavior, we can catch these subtleties.

Fixes #57326
Fixes #57075

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-11-21 16:55:44 -08:00
Wyatt Johnson 07c434d54b Web Streams cleanup (#56819)
This updates some code related to web streams and encoding.

- Removes some unused code related to base64 encoding/decoding (Edge runtime currently supports it natively via `Buffer`)
- Prefer readable stream `pull` versus `.on("data", (chunk) => { ... })` event handlers (simplifies execution)
- Utilize `pipeTo` and `pipeThrough` on web streams to remove custom code related to stream pumping
- Updates pipe readable function to utilize web streams first class rather than relying on manual pumping + stream management
  - This also takes advantage of the `AbortController` when piping so that the response can use it to cancel the stream
2023-10-18 21:38:28 +00:00
Tim Neutkens 5a49b88fb9 Use consistent name for App Router tests (#56352)
Ensures all App Router tests have a unique name and similar reference
(with `-`).

<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->

---------

Co-authored-by: Jiachi Liu <inbox@huozhi.im>
2023-10-06 11:06:06 +02:00
Zack Tanner e970e050a9 Reland static prefetches & fix prefetch bailout behavior (#56228)
Reland #54403

Also modifies the implementation of #55950 to not change the prefetch behavior when there is flight router state - we should only check the entire loader tree in the static prefetch case, otherwise we're inadvertently rendering the component tree for prefetches that match the current flight router state segment. ([slack x-ref](https://vercel.slack.com/archives/C03S8ED1DKM/p1695862974745849))

This includes a few other misc fixes for static prefetch generation:
- `next start` was not serving them (which also meant tests weren't catching a few small bugs)
- the router cache key casing can differ between build and runtime, resulting in a bad cache lookup which results suspending indefinitely during navigation
- We cannot generate static prefetches for pages that opt into `searchParams`, as the prefetch response won't have the right cache key in the RSC payload
- Layouts that use headers/cookies shouldn't use a static prefetch because it can result in unexpected behavior (ie, being redirected to a login page, if the prefetch contains redirect logic for unauthed users)

Closes NEXT-1665
Closes NEXT-1643
2023-10-02 17:12:55 +00:00
Max Leiter 4c3b7f9b7a Export RedirectType from next/navigation (#54729)
A continuation of #51864 with RedirectType used in an e2e test and some
ts-ignores removed from the codebase. Happy to split it into two PRs but
I left comments in the two consequential files

---------
2023-08-29 11:05:44 -07:00
akfm 1494283a74 fix: Add Next-Url to http vary in consideration of intercept routes. (#52746)
### Why

We calculate the “next url” depending on the router state and the previous router state so that when you navigate to a route, the proxy matches with that header and returns you the intercepted route if matching

### What 
- Fixes #52745




Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
2023-07-22 21:38:23 +00:00
Jiachi Liu aa3e043bbf Add unique search query for RSC requests to be cacable on CDN (#50970)
Adding a `_rsc` query for RSC payload requests so that they can be
differentiated on resources level for CDN cache for the ones that didn't
fully respect to VARY header.

Also stripped them for node/edge servers so that they won't show up in
the url

x-ref:
https://github.com/vercel/next.js/pull/49140#issuecomment-1549405009

Closes #49140
Closes NEXT-1268
2023-06-12 11:52:36 +02:00
Tim Neutkens 25a9547cad Remove experimental config from create-next-app (#49241)
## What?

Removes `experimental.appDir` this was leftover from when I flipped the
switch.

Kept the config file as in the future we might add future flags and
such. It also helps that it has the types comment included so you always
get types.

<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation or adding/fixing Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md



## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->

---------

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2023-05-05 00:22:28 -07:00
Jiachi Liu 7de1a4070d Reland "app-router: new client-side cache semantics" (#48695)
Reverts vercel/next.js#48688
fix NEXT-1011
2023-04-22 10:41:08 +00:00
Jiachi Liu 8089d0a3bb Revert "Reland app-router: new client-side cache semantics" (#48688)
Reverts vercel/next.js#48685

Temporary Revert again to investigate the hang job
fix NEXT-1011
2023-04-21 22:36:28 +02:00