mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
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.
88 lines
2.9 KiB
TypeScript
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'
|
|
)
|
|
})
|
|
})
|