import { nextTestSetup } from 'e2e-utils' import { createRouterAct } from 'router-act' import { waitFor } from 'next-test-utils' describe('segment cache (basic tests)', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) if (isNextDev) { test('ppr is disabled', () => {}) return } it('navigate before any data has loaded into the prefetch cache', async () => { let act: ReturnType const browser = await next.browser('/', { beforePageLoad(page) { act = createRouterAct(page) }, }) await act( async () => { // Reveal the link to trigger a prefetch, but block the responses. const link = await act(async () => { const reveal = await browser.elementByCss('input[type="checkbox"]') await reveal.click() return await browser.elementByCss('a') }, 'block') // While the prefetches are blocked, navigate to the test page. await act( async () => { // Navigate to the test page await link.click() }, { includes: 'Dynamic in nav', } ) // The static and dynamic content appears simultaneously because everything // was fetched as part of the same navigation request. const nav = await browser.elementById('nav') expect(await nav.innerHTML()).toMatchInlineSnapshot( `"
Static in nav
Dynamic in nav
"` ) }, // Although the blocked prefetches are allowed to continue when we exit // the outer `act` scope, they were canceled when we navigated to the new // page. So there should be no additional requests in the outer // `act` scope. 'no-requests' ) }) it('navigate with prefetched data', async () => { let act: ReturnType const browser = await next.browser('/', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Reveal the link to trigger a prefetch, but block the responses. const link = await act(async () => { const reveal = await browser.elementByCss('input[type="checkbox"]') await reveal.click() return await browser.elementByCss('a') }) // Navigate to the test page await act( async () => { await link.click() // Because we haven't exited the `act` scope yet, no new data has been // received, but we're still able to immediately render the static // content because it was prefetched. const nav = await browser.elementById('nav') expect(await nav.innerHTML()).toMatchInlineSnapshot( `"
Static in nav
Loading... [Dynamic in nav]
"` ) }, // The dynamic data streams in after the loading state { includes: 'Dynamic in nav' } ) const nav = await browser.elementById('nav') await browser.elementByCss('[data-streaming-text-dynamic="Dynamic in nav"]') expect(await nav.innerHTML()).toMatchInlineSnapshot( `"
Static in nav
Dynamic in nav
"` ) }) // TODO(cache-components): With `cacheComponents` enabled, this test is outdated, because // we no longer put the param values in the prefetched RSC response. You'd have to opt into runtime // prefetching for this test to pass until we ship the optimization that would mark this as fully static // if you don't reference any dynamic params in the server components. it.skip('navigate to page with lazily-generated (not at build time) static param', async () => { let act: ReturnType const browser = await next.browser('/lazily-generated-params', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Reveal the link to trigger a prefetch. const reveal = await browser.elementByCss('input[type="checkbox"]') const link = await act( async () => { await reveal.click() return await browser.elementByCss('a') }, { includes: 'target-page-with-lazily-generated-param' } ) // Navigate to the test page await act( async () => { await link.click() // We should be able to render the page with the dynamic param, because // it is lazily generated const target = await browser.elementById( 'target-page-with-lazily-generated-param' ) expect(await target.innerHTML()).toMatchInlineSnapshot( `"Param: some-param-value"` ) }, // No additional requests were required, because everything was prefetched 'no-requests' ) }) it('prefetch interception route', async () => { let act: ReturnType const browser = await next.browser('/interception/feed', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Reveal the link to trigger a prefetch. const reveal = await browser.elementByCss('input[type="checkbox"]') const link = await act( async () => { await reveal.click() return await browser.elementByCss('a') }, { includes: 'intercepted-photo-page' } ) // Navigate to the test page await act( async () => { await link.click() // The page should render immediately because it was prefetched const div = await browser.elementById('intercepted-photo-page') expect(await div.innerHTML()).toBe('Intercepted photo page') }, // No additional requests were required, because everything was prefetched 'no-requests' ) }) it('prefetch interception route with params', async () => { let act: ReturnType const browser = await next.browser('/interception-with-params/feed', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Reveal the link to trigger a prefetch. const reveal = await browser.elementByCss('input[type="checkbox"]') const link = await act( async () => { await reveal.click() return await browser.elementByCss('a') }, { includes: 'intercepted-photo-page' } ) // Navigate to the test page await act( async () => { await link.click() // The page should render immediately because it was prefetched const div = await browser.elementById('intercepted-photo-page') expect(await div.innerHTML()).toBe('Intercepted photo page for id "1"') }, // No additional requests were required, because everything was prefetched 'no-requests' ) }) it('skips dynamic request if prefetched data is fully static', async () => { let act: ReturnType const browser = await next.browser('/fully-static', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Reveal the link to trigger a prefetch. const reveal = await browser.elementByCss('input[type="checkbox"]') const link = await act( async () => { await reveal.click() return await browser.elementByCss('a[href="/fully-static/target-page"]') }, { includes: 'Target' } ) await act( async () => { await link.click() // The page should render immediately because it was prefetched. const div = await browser.elementById('target-page') expect(await div.innerHTML()).toBe('Target') }, // No additional requests were required, because everything was prefetched 'no-requests' ) }) it('skips static layouts during partially static navigation', async () => { let act: ReturnType const browser = await next.browser('/partially-static', { beforePageLoad(page) { act = createRouterAct(page) }, }) const layoutMarkerId = 'static-layout' const layoutMarkerContent = 'Static layout' // Reveal the link to trigger a prefetch. const reveal = await browser.elementByCss('input[type="checkbox"]') const link = await act( async () => { await reveal.click() return await browser.elementByCss( 'a[href="/partially-static/target-page"]' ) }, // The static layout should not be included in the dynamic response, // because it was already prefetched. { includes: layoutMarkerContent } ) await act(async () => { await link.click() // The static layout and the loading state of the dynamic page should // render immediately because they were prefetched. const layoutMarker = await browser.elementById(layoutMarkerId) expect(await layoutMarker.innerHTML()).toBe('Static layout') const dynamicDiv = await browser.elementById('dynamic-page') expect(await dynamicDiv.innerHTML()).toBe('Loading...') }, [ // The dynamic page is included in the dynamic response. { includes: 'Dynamic page' }, // The static layout should not be included in the dynamic response, // because it was already prefetched. { includes: layoutMarkerContent, block: 'reject' }, ]) // The dynamic content has streamed in. const dynamicDiv = await browser.elementById('dynamic-page') expect(await dynamicDiv.innerHTML()).toBe('Dynamic page') }) it('refreshes page segments when navigating to the exact same URL as the current location', async () => { let act: ReturnType const browser = await next.browser('/same-page-nav', { beforePageLoad(page) { act = createRouterAct(page) }, }) const linkWithNoHash = await browser.elementByCss( 'a[href="/same-page-nav"]' ) const linkWithHashA = await browser.elementByCss( 'a[href="/same-page-nav#hash-a"]' ) const linkWithHashB = await browser.elementByCss( 'a[href="/same-page-nav#hash-b"]' ) async function readRandomNumberFromPage() { const randomNumber = await browser.elementById('random-number') return await randomNumber.textContent() } // Navigating to the same URL should refresh the page const randomNumber = await readRandomNumberFromPage() await act(async () => { await linkWithNoHash.click() }, [ { includes: 'random-number', }, { // Only the page segments should be refreshed, not the layouts. // TODO: We plan to change this in the future. block: 'reject', includes: 'same-page-nav-layout', }, ]) const randomNumber2 = await readRandomNumberFromPage() expect(randomNumber2).not.toBe(randomNumber) // Navigating to a different hash should *not* refresh the page await act(async () => { await linkWithHashA.click() }, 'no-requests') expect(await readRandomNumberFromPage()).toBe(randomNumber2) // Navigating to the same hash again should refresh the page await act( async () => { await linkWithHashA.click() }, { includes: 'random-number', } ) const randomNumber3 = await readRandomNumberFromPage() expect(randomNumber3).not.toBe(randomNumber2) // Navigating to a different hash should *not* refresh the page await act(async () => { await linkWithHashB.click() }, 'no-requests') expect(await readRandomNumberFromPage()).toBe(randomNumber3) }) it('does not throw an error when navigating to a page with a server action', async () => { let act: ReturnType const browser = await next.browser('/with-server-action', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Reveal the link to trigger a prefetch. const reveal = await browser.elementByCss('input[type="checkbox"]') const link = await act( async () => { await reveal.click() return await browser.elementByCss( 'a[href="/with-server-action/target-page"]' ) }, { includes: 'Target' } ) await act( async () => { await link.click() // The page should render immediately because it was prefetched, and it // should not throw an error. const form = await browser.elementById('target-page') expect(await form.innerHTML()).toBe('Target') }, // No additional requests were required, because everything was prefetched 'no-requests' ) }) it('does not cause infinite loop with cacheLife("seconds")', async () => { let requestCount = 0 const browser = await next.browser('/cache-life-seconds-test', { beforePageLoad(page) { page.on('request', (request) => { const url = request.url() if (url.includes('/cache-life-seconds') && url.includes('_rsc')) { requestCount++ } }) }, }) // Reveal the link to trigger a prefetch const reveal = await browser.elementByCss('input[type="checkbox"]') await reveal.click() // Wait for the link to appear const link = await browser.elementByCss('a[href="/cache-life-seconds"]') // Give the prefetch a moment to potentially start looping await waitFor(500) // Check that we haven't made excessive requests during prefetch expect(requestCount).toBeLessThan(10) // Now navigate to the page to ensure it works correctly await link.click() // Wait for the page to load const page = await browser.elementById('cache-life-seconds-page') const content = await page.textContent() expect(content).toContain('Cache Life Seconds Page') }) it('can handle circular references in client component props', async () => { let act: ReturnType const browser = await next.browser('/', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Reveal the link to trigger a prefetch. const link = await act( async () => { await browser .elementByCss('input[data-link-accordion="/cycle"]') .click() return browser.elementByCss('a[href="/cycle"]') }, { includes: 'testProp' } ) await act( async () => { await link.click() // The page should render immediately because it was prefetched, and it // should show the resolved cycle text. expect(await browser.elementById('cycle-check').text()).toBe( 'Cycle resolved' ) }, // No additional requests were required, because everything was // prefetched. 'no-requests' ) }) })