Files
next.js/test/e2e/app-dir/prefetch-true-instant/prefetch-true-instant.test.ts
Andrew ClarkandGitHub 5aa4568bf9 Ignore prefetch={true} on Links to routes with instant (#90061)
When a route has `unstable_instant` defined on any segment, setting
`prefetch={true}` on a Link to that route is now a no-op. The prefetch
behaves the same as the default auto strategy — cached content is
prefetched but dynamic content is deferred to navigation time.

A full static prefetch doesn't make sense for routes with instant
configs, since those segments use runtime prefetching. Rather than
silently doing something the developer didn't intend, we ignore the prop
entirely.

To detect this on the client, the server now sends a bitmask instead of
a boolean for the per-segment prefetch metadata. One bit tracks whether
the subtree contains any instant config, which propagates up so the
client can check it at the root without traversing.
2026-02-16 18:35:51 -05:00

130 lines
4.0 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import type * as Playwright from 'playwright'
import { createRouterAct } from 'router-act'
describe('prefetch={true} with instant route', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
if (isNextDev) {
it('is skipped', () => {})
return
}
it('does not include dynamic content in the prefetch when the target route has instant', async () => {
let page: Playwright.Page
const browser = await next.browser('/', {
beforePageLoad(p: Playwright.Page) {
page = p
},
})
const act = createRouterAct(page)
// Reveal the link to trigger a prefetch. Even though the Link has
// prefetch={true}, the route has unstable_instant defined, so the
// prefetch should be downgraded — it should include cached content
// but NOT dynamic content.
await act(async () => {
const linkToggle = await browser.elementByCss(
'input[data-link-accordion="/target-page"]'
)
await linkToggle.click()
}, [
{
includes: 'Cached content',
},
{
includes: 'Dynamic content',
block: 'reject',
},
])
// Navigate to the page. Block the navigation request so we can
// verify which parts appear instantly from the prefetch cache.
await act(async () => {
await act(
async () => {
await browser.elementByCss('a[href="/target-page"]').click()
},
{
// Temporarily block the navigation request that fetches the
// dynamic content. While blocked, the cached parts should
// already be visible on screen.
includes: 'Dynamic content',
block: true,
}
)
// The cached content should be visible immediately from the prefetch.
expect(await browser.elementById('cached-content').text()).toContain(
'Cached content'
)
// The dynamic content should NOT be on screen yet because the
// navigation response is still blocked.
const dynamicElement = await browser.elementsByCss('#dynamic-content')
expect(dynamicElement.length).toBe(0)
})
// After the navigation response is unblocked, both parts should
// be visible.
expect(await browser.elementById('cached-content').text()).toContain(
'Cached content'
)
expect(await browser.elementById('dynamic-content').text()).toEqual(
'Dynamic content'
)
})
it('also disables full prefetch when instant is on a layout, not the page', async () => {
let page: Playwright.Page
const browser = await next.browser('/', {
beforePageLoad(p: Playwright.Page) {
page = p
},
})
const act = createRouterAct(page)
// The layout has unstable_instant but the page does not. The
// SubtreeHasInstant bit should propagate up from the layout, so
// prefetch={true} on the Link should still be downgraded.
await act(async () => {
const linkToggle = await browser.elementByCss(
'input[data-link-accordion="/layout-instant"]'
)
await linkToggle.click()
}, [
{
includes: 'Cached content',
},
{
includes: 'Dynamic content',
block: 'reject',
},
])
// Navigate and verify the cached content appears instantly.
await act(async () => {
await act(
async () => {
await browser.elementByCss('a[href="/layout-instant"]').click()
},
{
includes: 'Dynamic content',
block: true,
}
)
expect(await browser.elementById('cached-content').text()).toContain(
'Cached content'
)
const dynamicElement = await browser.elementsByCss('#dynamic-content')
expect(dynamicElement.length).toBe(0)
})
expect(await browser.elementById('cached-content').text()).toContain(
'Cached content'
)
expect(await browser.elementById('dynamic-content').text()).toEqual(
'Dynamic content'
)
})
})