mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
With `cacheComponents` + `partialPrefetching`, navigating to an already-prefetched route with a dynamic `generateMetadata` left `document.title` permanently empty until a hard reload. The prefetch cached the head as complete using the server's `isHeadPartial` flag, which is unreliable under Cache Components. Derive the head's partiality from `isResponsePartial` instead, as segment data already does, so a dynamic head is fetched and applied on navigation while a fully static head stays complete. Fixing the flag exposed a latent issue in the prefetch scheduler: during a speculative prefetch, the head was unconditionally runtime-prefetched, which previously went unnoticed because the head was mismarked as complete. The head is now runtime-prefetched only when a segment in the new part of the tree is a candidate for runtime prefetching — it rides along with that request rather than spawning a standalone one. If nothing in the new part of the tree is a runtime prefetch candidate, the head is fetched during the navigation instead. This makes the scheduler's metadata-only request path dead code, so it's removed. Fixes #95268. <!-- NEXT_JS_LLM_PR -->
91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
import { retry } from 'next-test-utils'
|
|
import type * as Playwright from 'playwright'
|
|
import { createRouterAct } from 'router-act'
|
|
|
|
// Regression test for GitHub #95268: with Cache Components + `partialPrefetching`,
|
|
// the metadata `<title>` is permanently dropped after a client-side navigation
|
|
// to a route with dynamic `generateMetadata` whose prefetch has already settled.
|
|
// The prefetch caches the route's App Shell, whose head does not include the
|
|
// dynamic title; the subsequent navigation must replace that prefetched head
|
|
// with the full head from the dynamic response. Previously it did not, so the
|
|
// title never appeared (only a hard reload fixed it).
|
|
describe('metadata-soft-nav-cache-components', () => {
|
|
const { next, isNextDev } = nextTestSetup({
|
|
files: __dirname,
|
|
})
|
|
|
|
if (isNextDev) {
|
|
test('disabled in development', () => {})
|
|
return
|
|
}
|
|
|
|
it('applies dynamic metadata after navigating to an already-prefetched route', async () => {
|
|
let page: Playwright.Page = null as any
|
|
const browser = await next.browser('/', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
page = p
|
|
},
|
|
})
|
|
const act = createRouterAct(page, { includeAppShellRequests: true })
|
|
|
|
expect(await browser.eval(() => document.title)).toBe('Home Default')
|
|
|
|
// Reveal the link and let its prefetch fully settle (act waits for the
|
|
// prefetch requests to complete). This caches the route's App Shell, whose
|
|
// head does NOT include the dynamic metadata title.
|
|
await act(async () => {
|
|
await browser.elementByCss('input[data-link-accordion="/slow"]').click()
|
|
})
|
|
|
|
// Navigate to the now-prefetched route.
|
|
await act(
|
|
async () => {
|
|
await browser.elementByCss('a[href="/slow"]').click()
|
|
},
|
|
{ includes: 'Slow content' }
|
|
)
|
|
|
|
await browser.waitForElementByCss('#slow-content')
|
|
|
|
// The title must resolve to the route's own metadata, merged with the
|
|
// layout's template — not be permanently dropped to an empty string.
|
|
await retry(async () => {
|
|
expect(await browser.eval(() => document.title)).toBe('Slow Page | Site')
|
|
})
|
|
})
|
|
|
|
it('does not blank a complete static title when navigating to a prefetched route with a dynamic body', async () => {
|
|
let page: Playwright.Page = null as any
|
|
const browser = await next.browser('/', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
page = p
|
|
},
|
|
})
|
|
const act = createRouterAct(page, { includeAppShellRequests: true })
|
|
|
|
expect(await browser.eval(() => document.title)).toBe('Home Default')
|
|
|
|
await act(async () => {
|
|
await browser
|
|
.elementByCss('input[data-link-accordion="/static-meta"]')
|
|
.click()
|
|
})
|
|
|
|
await act(
|
|
async () => {
|
|
await browser.elementByCss('a[href="/static-meta"]').click()
|
|
},
|
|
{ includes: 'Static meta content' }
|
|
)
|
|
|
|
await browser.waitForElementByCss('#static-meta-content')
|
|
|
|
await retry(async () => {
|
|
expect(await browser.eval(() => document.title)).toBe(
|
|
'Static Meta | Site'
|
|
)
|
|
})
|
|
})
|
|
})
|