Files
next.js/test/e2e/app-dir/metadata-soft-nav-cache-components/metadata-soft-nav-cache-components.test.ts
Andrew ClarkandGitHub 27e225f468 Fix metadata title dropped on soft navigation with Cache Components (#95315)
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 -->
2026-07-02 08:16:45 -04:00

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