Files
next.js/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations-partial-prefetching.test.ts
Hendrik LiebauandGitHub 824fcd63f8 Opt Partial Prefetching routes into the runtime stage of Cached Navs (#95097)
In #95064 we added a global opt-in to the runtime stage of Cached
Navigations: setting `cachedNavigations: 'allow-runtime'` makes every
route runtime-cache, on top of the existing per-segment `prefetch =
'allow-runtime'`. This extends the opt-in: a `prefetch` config of
`'partial'`, `'unstable_eager'`, or `'allow-runtime'` all enable Partial
Prefetching for a route, and a route already doing PPR-style partial
prefetching should also runtime-cache its navigations. The next-config
`partialPrefetching`, which enables Partial Prefetching globally, opts
every route in as well.

Both runtime-prefetch spawn gates in `app-render.tsx` now broaden their
condition to include `partialPrefetching` and any segment `prefetch`
that enables Partial Prefetching, via a new
`anySegmentHasPartialPrefetchingEnabled` helper. The narrower callers
that gate metadata staging and the dev reveal stage keep using
`anySegmentHasRuntimePrefetchEnabled`, since they specifically concern
routes the client runtime-prefetches (`'allow-runtime'`), not partial
ones.

Because `partialPrefetching` has no default and is not auto-enabled by
`cacheComponents`, apps that do not set it are unaffected.

Tests add `prefetch = 'partial'` and `prefetch = 'unstable_eager'`
routes to the existing `default` fixture and a new `partial-prefetching`
fixture for the global config, each asserting the route runtime-caches
its request-derived content on a second navigation without a per-segment
`prefetch = 'allow-runtime'` export.
2026-06-23 21:42:12 +02:00

88 lines
2.9 KiB
TypeScript

import path from 'path'
import { nextTestSetup } from 'e2e-utils'
import type * as Playwright from 'playwright'
import { createRouterAct } from 'router-act'
// The `partial-prefetching` fixture enables Partial Prefetching globally via
// the next-config `partialPrefetching: true`, which opts every route into
// runtime Cached Navigations even without a per-segment `prefetch` config.
describe('cached navigations - global partialPrefetching', () => {
const { next, isNextDev } = nextTestSetup({
files: path.join(__dirname, 'partial-prefetching'),
})
if (isNextDev) {
it('is skipped', () => {})
return
}
it('runtime-caches a route that has no per-segment prefetch config', async () => {
let page: Playwright.Page
const browser = await next.browser('/', {
async beforePageLoad(p: Playwright.Page) {
page = p
await page.clock.install()
},
})
const act = createRouterAct(page)
// First navigation to /runtime-prefetchable — a route that reads request
// data but does NOT export any `prefetch` config. The link uses
// prefetch={false}, so this is a plain navigation with no prefetch.
await act(
async () => {
await browser.elementByCss('a[href="/runtime-prefetchable"]').click()
},
{ includes: 'Dynamic content' }
)
expect(await browser.elementById('cached-content').text()).toContain(
'Cached content'
)
// Navigate back to home
await browser.back()
expect(await browser.elementByCss('h1').text()).toBe('Home')
// Second navigation — under the global `partialPrefetching` config, the
// request-derived content (searchParams, cookies, headers) was
// runtime-cached from the first navigation and shows instantly, even with
// the dynamic request blocked. Only the truly dynamic connection() content
// needs a server request.
await act(async () => {
await act(
async () => {
await browser.elementByCss('a[href="/runtime-prefetchable"]').click()
},
{
includes: 'Dynamic content',
block: true,
}
)
expect(await browser.elementById('cached-content').text()).toContain(
'Cached content'
)
expect(
await browser.elementById('search-params-boundary').text()
).toContain('Search params:')
expect(await browser.elementById('cookies-boundary').text()).toContain(
'Cookie:'
)
expect(await browser.elementById('headers-boundary').text()).toContain(
'Header:'
)
// Only connection() shows a Suspense fallback — it's truly dynamic.
expect(await browser.elementById('connection-boundary').text()).toBe(
'Loading connection...'
)
})
// After unblocking, the dynamic content resolves too.
expect(await browser.elementById('connection-boundary').text()).toContain(
'Dynamic content'
)
})
})