next.js/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts
cached-navigations.test.ts948 lines31.5 KB
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import type * as Playwright from 'playwright'
import { createRouterAct } from 'router-act'

describe('cached navigations', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  if (isNextDev) {
    it('is skipped', () => {})
    return
  }

  // TODO: flaky
  it.skip('serves cached static segments instantly on the second navigation', 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 — full dynamic request, no prefetch
    await act(
      async () => {
        await browser.elementByCss('a[href="/partially-static"]').click()
      },
      { includes: 'Dynamic content' }
    )

    // Verify all content is visible
    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:'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )

    // Navigate back to home
    await browser.back()
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Fast-forward time past the short-lived runtime cache's stale time (30s)
    // but under the static cache's stale time (120s). If the stale time sent to
    // the client incorrectly used the runtime cache's value, the cached
    // segments would have expired and the second navigation wouldn't be
    // instant.
    await page.clock.fastForward(60_000)

    // Second navigation — cached static data should show immediately
    await act(async () => {
      await act(
        async () => {
          await browser.elementByCss('a[href="/partially-static"]').click()
        },
        {
          // Block the dynamic request. The cached/prefetchable content
          // should still be visible even though the dynamic data hasn't
          // arrived yet.
          includes: 'Dynamic content',
          block: true,
        }
      )

      // The static/cached part should be visible while the dynamic
      // request is still blocked
      expect(await browser.elementById('cached-content').text()).toContain(
        'Cached content'
      )

      // Runtime and dynamic content should show Suspense fallbacks
      expect(await browser.elementById('search-params-boundary').text()).toBe(
        'Loading search params...'
      )
      expect(await browser.elementById('cookies-boundary').text()).toBe(
        'Loading cookies...'
      )
      expect(await browser.elementById('headers-boundary').text()).toBe(
        'Loading headers...'
      )
      expect(await browser.elementById('connection-boundary').text()).toBe(
        'Loading connection...'
      )
    })

    // After unblocking, all content should be visible
    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:'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )

    // Navigate back to home again
    await browser.back()
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Fast-forward past the static cache's stale time (120s). The cached
    // segments should now be expired, so the third navigation should NOT
    // show cached content instantly — it should block on the full response.
    await page.clock.fastForward(120_000)

    // Third navigation — cache is stale, no cached content should be shown
    await act(async () => {
      await act(
        async () => {
          await browser.elementByCss('a[href="/partially-static"]').click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      // With stale cache, nothing from the target page should be visible
      // while the request is blocked — not even the cached content.
      const mainText = await (await browser.elementByCss('main')).innerText()
      expect(mainText).not.toContain('Cached content')
    })

    // After unblocking, all content should be visible
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )
  })

  it('serves a fully static page without any requests on the second navigation', async () => {
    let page: Playwright.Page
    const browser = await next.browser('/', {
      beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })
    const act = createRouterAct(page)

    // First navigation — full request, no prefetch
    await act(
      async () => {
        await browser.elementByCss('a[href="/fully-static"]').click()
      },
      { includes: 'Cached 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 — fully cached, should not issue any requests
    await act(async () => {
      await browser.elementByCss('a[href="/fully-static"]').click()
    }, 'no-requests')
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )
  })

  // TODO: flaky
  it.skip('caches static segments when navigating to a known route without a prefetch', 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 — seeds the route cache (stale after 5 min) and
    // segment cache (stale after 120s, from cacheLife({ stale: 120 })).
    await act(
      async () => {
        await browser.elementByCss('a[href="/partially-static"]').click()
      },
      { includes: 'Dynamic content' }
    )
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )

    // Navigate back to home
    await browser.back()
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Fast-forward past the segment cache stale time (120s) but under the
    // route cache stale time (5 min). Segment entries are now expired, but
    // the route is still known.
    await page.clock.fastForward(130_000)

    // Second navigation — the route is known but all segment entries have
    // expired, so nothing is served from the cache. The server responds
    // with fresh data including a static stage, which is written into the
    // segment cache for future navigations.
    await act(
      async () => {
        await browser.elementByCss('a[href="/partially-static"]').click()
      },
      { includes: 'Dynamic content' }
    )
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )

    // Navigate back to home again
    await browser.back()
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Fast-forward 60s — well under the 120s stale time that the segment
    // entries would have if the second navigation had cached them.
    await page.clock.fastForward(60_000)

    // Third navigation — block the dynamic request to test whether cached
    // static segments are available.
    await act(async () => {
      await act(
        async () => {
          await browser.elementByCss('a[href="/partially-static"]').click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      // The second navigation wrote the static stage into the segment
      // cache. These entries are still fresh (60s < 120s) so the cached
      // content is visible while the dynamic request is pending.
      expect(await browser.elementById('cached-content').text()).toContain(
        'Cached content'
      )

      expect(await browser.elementById('connection-boundary').text()).toBe(
        'Loading connection...'
      )
    })

    // After unblocking, all content should be visible
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )
  })

  // TODO: flaky
  it.skip('includes static params in the cached static stage', 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
    await act(
      async () => {
        await browser.elementByCss('a[href="/with-static-params/foo"]').click()
      },
      { includes: 'Dynamic content' }
    )
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )
    expect(await browser.elementById('params').text()).toContain('Param: foo')

    // Navigate back
    await browser.back()
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    await page.clock.fastForward(60_000)

    // Second navigation — params are static, so they should be included in
    // the cached static stage and visible while the dynamic request is blocked
    await act(async () => {
      await act(
        async () => {
          await browser
            .elementByCss('a[href="/with-static-params/foo"]')
            .click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      expect(await browser.elementById('cached-content').text()).toContain(
        'Cached content'
      )
      // Static params should be visible — they resolve during the static stage
      expect(await browser.elementById('params').text()).toContain('Param: foo')
      // Dynamic content should show Suspense fallback
      expect(await browser.elementById('connection-boundary').text()).toBe(
        'Loading connection...'
      )
    })

    // After unblocking, dynamic content should be visible
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )
  })

  // TODO: flaky
  it.skip('defers fallback params to the runtime stage', 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 — "foo" is not in generateStaticParams, so it's a
    // fallback param
    await act(
      async () => {
        await browser
          .elementByCss('a[href="/with-fallback-params/foo"]')
          .click()
      },
      { includes: 'Dynamic content' }
    )
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )
    expect(await browser.elementById('params-boundary').text()).toContain(
      'Param: foo'
    )

    // Navigate back
    await browser.back()
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    await page.clock.fastForward(60_000)

    // Second navigation — fallback params are deferred to the runtime stage,
    // so they should NOT be visible while the dynamic request is blocked
    await act(async () => {
      await act(
        async () => {
          await browser
            .elementByCss('a[href="/with-fallback-params/foo"]')
            .click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      expect(await browser.elementById('cached-content').text()).toContain(
        'Cached content'
      )
      // Fallback params should show Suspense fallback — deferred to runtime
      expect(await browser.elementById('params-boundary').text()).toBe(
        'Loading params...'
      )
      expect(await browser.elementById('connection-boundary').text()).toBe(
        'Loading connection...'
      )
    })

    // After unblocking, all content should be visible
    expect(await browser.elementById('params-boundary').text()).toContain(
      'Param: foo'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )
  })

  // TODO: flaky
  it.skip('caches runtime-prefetchable content from a navigation for instant second visit', 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 — full dynamic request, no prefetch
    await act(
      async () => {
        await browser.elementByCss('a[href="/runtime-prefetchable"]').click()
      },
      { includes: 'Dynamic content' }
    )

    // Verify all content is visible
    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:'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )

    // Navigate back to home
    await browser.back()
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Second navigation — no time has passed, so both the static cache
    // (stale: 120s) and the runtime cache (stale: 30s from the
    // short-lived cache entry in CookiesContent) should still be fresh.
    // With unstable_instant { prefetch: 'runtime' }, runtime-prefetchable
    // content (cookies, headers, searchParams) should be cached from the
    // first navigation and show instantly alongside the static content.
    // Only truly dynamic content (connection()) needs a server request.
    await act(async () => {
      await act(
        async () => {
          await browser.elementByCss('a[href="/runtime-prefetchable"]').click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      // Static cached content should be visible
      expect(await browser.elementById('cached-content').text()).toContain(
        'Cached content'
      )

      // Runtime-prefetchable content should also be visible (cached from
      // the first navigation's embedded runtime prefetch stream)
      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() content should show a Suspense fallback — it's
      // truly dynamic and not runtime-prefetchable
      expect(await browser.elementById('connection-boundary').text()).toBe(
        'Loading connection...'
      )
    })

    // After unblocking, all content should be visible
    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:'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )

    // Navigate back to home again
    await browser.back()
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Fast-forward past the runtime cache's stale time (30s).
    await page.clock.fastForward(60_000)

    // Third navigation — runtime cache is stale. Verify the navigation
    // blocks on a full server request (nothing is cached).
    //
    // TODO: Ideally, the static cache (120s stale) should survive and show
    // static content instantly even after the runtime cache expires. Currently
    // the runtime prefetch write (PPRRuntime) evicts the static cache entry
    // (PPR) via the fallback lookup in upsertSegmentEntry, so there's no
    // static fallback after the runtime entry expires. This needs a layered
    // cache approach where entries with different fetch strategies / stale
    // times coexist independently.
    await act(async () => {
      await act(
        async () => {
          await browser.elementByCss('a[href="/runtime-prefetchable"]').click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      // With a stale cache, nothing from the target page should be visible
      // while the request is blocked. The navigation stays on the home page.
      const mainText = await (await browser.elementByCss('main')).innerText()
      expect(mainText).not.toContain('Cached content')
      expect(mainText).not.toContain('Search params:')
      expect(mainText).not.toContain('Cookie:')
      expect(mainText).not.toContain('Header:')
      expect(mainText).not.toContain('Dynamic content')
    })

    // After unblocking, all content should be visible
    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:'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )
  })

  // TODO: flaky
  it.skip('caches runtime-prefetchable content from the initial HTML for subsequent navigations', async () => {
    let page: Playwright.Page
    // Start directly at /runtime-prefetchable — full HTML load, not a
    // client-side navigation. The RSC payload is inlined in the HTML and
    // includes an embedded runtime prefetch stream (`p` field) that the client
    // writes into the segment cache during hydration.
    const browser = await next.browser('/runtime-prefetchable', {
      async beforePageLoad(p: Playwright.Page) {
        page = p
        await page.clock.install()
      },
    })
    const act = createRouterAct(page)

    // Wait for all content to stream in (the dynamic content uses connection()
    // + setTimeout, so it arrives late).
    await retry(async () => {
      expect(await browser.elementById('connection-boundary').text()).toContain(
        'Dynamic content'
      )
    })

    // Verify runtime-prefetchable content is also visible
    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:'
    )

    // Navigate to the home page
    await act(async () => {
      await browser.elementByCss('a[href="/"]').click()
    })
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Navigate back to the runtime-prefetchable page. The static content and
    // runtime-prefetchable content (cookies, headers, searchParams) should be
    // cached from the initial HTML load. Only truly dynamic content
    // (connection()) needs a server request.
    await act(async () => {
      await act(
        async () => {
          await browser.elementByCss('a[href="/runtime-prefetchable"]').click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      // While the dynamic request is blocked, verify that runtime-prefetchable
      // content is rendered instantly from the cache.
      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:'
      )

      // The truly dynamic content (connection()) is not runtime-prefetchable
      // and should still be in its loading state.
      expect(await browser.elementById('connection-boundary').text()).toContain(
        'Loading connection...'
      )
    })

    // After the outer act completes, the blocked dynamic response is released
    // and the truly dynamic content should be visible.
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )

    // Navigate back to home again
    await browser.back()
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Fast-forward past the runtime cache's stale time (30s).
    await page.clock.fastForward(60_000)

    // Third navigation — runtime cache is stale. Verify the navigation
    // blocks on a full server request (nothing is cached).
    await act(async () => {
      await act(
        async () => {
          await browser.elementByCss('a[href="/runtime-prefetchable"]').click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      // With a stale cache, nothing from the target page should be visible
      // while the request is blocked.
      const mainText = await (await browser.elementByCss('main')).innerText()
      expect(mainText).not.toContain('Cached content')
      expect(mainText).not.toContain('Search params:')
      expect(mainText).not.toContain('Cookie:')
      expect(mainText).not.toContain('Header:')
      expect(mainText).not.toContain('Dynamic content')
    })

    // After unblocking, all content should be visible
    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:'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )
  })

  it('caches a fully static page from the initial HTML for subsequent navigations', async () => {
    let page: Playwright.Page
    // Start directly at /fully-static — full HTML load, not a client-side
    // navigation. The RSC payload is inlined in the HTML and contains only
    // static (cached) content.
    const browser = await next.browser('/fully-static', {
      async beforePageLoad(p: Playwright.Page) {
        page = p
        await page.clock.install()
      },
    })
    const act = createRouterAct(page)

    // Verify the page rendered fully via HTML
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )

    // Navigate to home
    await act(
      async () => {
        await browser.elementByCss('a[href="/"]').click()
      },
      { includes: 'Home' }
    )
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Navigate back to /fully-static. Since it was fully static and cached
    // during the initial HTML load, no server requests should be needed.
    await act(async () => {
      await browser.elementByCss('a[href="/fully-static"]').click()
    }, 'no-requests')
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )

    // Navigate back to home again
    await act(async () => {
      await browser.elementByCss('a[href="/"]').click()
    }, 'no-requests')
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Fast-forward past the stale time (120s from cacheLife({ stale: 120 })).
    // Using 180s to stay well under the 300s default — if we accidentally
    // used the default instead of the collected stale time, this would
    // not expire and the test would fail.
    await page.clock.fastForward(180_000)

    // Navigate to /fully-static again — cache is stale, so a server
    // request should be required.
    await act(
      async () => {
        await browser.elementByCss('a[href="/fully-static"]').click()
      },
      { includes: 'Cached content' }
    )
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )
  })

  // TODO: flaky
  it.skip('caches a partially static page from the initial HTML for subsequent navigations', async () => {
    let page: Playwright.Page
    // Start directly at /partially-static — full HTML load. The RSC payload
    // inlined in the HTML contains both cached and dynamic content.
    const browser = await next.browser('/partially-static', {
      async beforePageLoad(p: Playwright.Page) {
        page = p
        await page.clock.install()
      },
    })
    const act = createRouterAct(page)

    // Verify the page rendered fully via HTML. Dynamic content streams in
    // with a delay, so use retry to wait for it.
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )
    await retry(async () => {
      expect(await browser.elementById('connection-boundary').text()).toContain(
        'Dynamic content'
      )
    })

    // Navigate to home
    await act(
      async () => {
        await browser.elementByCss('a[href="/"]').click()
      },
      { includes: 'Home' }
    )
    expect(await browser.elementByCss('h1').text()).toBe('Home')

    // Navigate back to /partially-static. The static stage was cached during
    // the initial HTML load, so cached content should be available instantly
    // while the dynamic content streams in.
    await act(async () => {
      await act(
        async () => {
          await browser.elementByCss('a[href="/partially-static"]').click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      // Cached content should be visible while the dynamic request is blocked
      expect(await browser.elementById('cached-content').text()).toContain(
        'Cached content'
      )

      // Dynamic content should show Suspense fallbacks
      expect(await browser.elementById('connection-boundary').text()).toBe(
        'Loading connection...'
      )
    })

    // After unblocking, all content should be visible
    expect(await browser.elementById('cached-content').text()).toContain(
      'Cached content'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )
  })

  // TODO: flaky
  it.skip('reuses cached page segment across different fallback params after navigation', async () => {
    let page: Playwright.Page
    const browser = await next.browser('/', {
      async beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })
    const act = createRouterAct(page)

    // First navigation to /with-fallback-params/foo — seeds the segment cache.
    // Since slug is a fallback param, the page segment's varyParams is empty,
    // meaning the cached segment contains only the Suspense fallback and no
    // param-specific data.
    await act(
      async () => {
        await browser
          .elementByCss('a[href="/with-fallback-params/foo"]')
          .click()
      },
      { includes: 'Dynamic content' }
    )
    expect(await browser.elementById('params-boundary').text()).toContain(
      'Param: foo'
    )

    // Click the bar link. The page segment should be reused from cache (empty
    // varyParams), so the Suspense fallback for params should appear instantly.
    await act(async () => {
      await act(
        async () => {
          await browser
            .elementByCss('a[href="/with-fallback-params/bar"]')
            .click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      expect(await browser.elementById('cached-content').text()).toContain(
        'Cached content'
      )
      // The page segment is reused — params boundary shows Suspense fallback
      expect(await browser.elementById('params-boundary').text()).toBe(
        'Loading params...'
      )
      expect(await browser.elementById('connection-boundary').text()).toBe(
        'Loading connection...'
      )
    })

    // After unblocking, all content should be visible with the new param
    expect(await browser.elementById('params-boundary').text()).toContain(
      'Param: bar'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )
  })

  // TODO: flaky
  it.skip('reuses cached page segment across different fallback params after initial HTML load', async () => {
    let page: Playwright.Page
    // Start directly at /with-fallback-params/foo — full HTML load. The RSC
    // payload inlined in the HTML seeds the segment cache with the page
    // segment, which has empty varyParams because slug is a fallback param.
    const browser = await next.browser('/with-fallback-params/foo', {
      async beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })
    const act = createRouterAct(page)

    // Wait for all content to stream in
    await retry(async () => {
      expect(await browser.elementById('connection-boundary').text()).toContain(
        'Dynamic content'
      )
    })
    expect(await browser.elementById('params-boundary').text()).toContain(
      'Param: foo'
    )

    // Click the bar link. The page segment should be reused from the cache
    // seeded by the initial HTML load.
    await act(async () => {
      await act(
        async () => {
          await browser
            .elementByCss('a[href="/with-fallback-params/bar"]')
            .click()
        },
        {
          includes: 'Dynamic content',
          block: true,
        }
      )

      expect(await browser.elementById('cached-content').text()).toContain(
        'Cached content'
      )
      // The page segment is reused — params boundary shows Suspense fallback
      expect(await browser.elementById('params-boundary').text()).toBe(
        'Loading params...'
      )
      expect(await browser.elementById('connection-boundary').text()).toBe(
        'Loading connection...'
      )
    })

    // After unblocking, all content should be visible with the new param
    expect(await browser.elementById('params-boundary').text()).toContain(
      'Param: bar'
    )
    expect(await browser.elementById('connection-boundary').text()).toContain(
      'Dynamic content'
    )
  })
})
Quest for Codev2.0.0
/
SIGN IN