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(…