+ This page is used to test various scenarios related to prefetch cache
+ staleness. In the corresponding e2e test, the links below are prefetched
+ (by toggling their visibility), time is elapsed, and then prefetched
+ again to check whether a new network request is made.
+
+
+
+
+ Page with stale time of 5 minutes
+
+
+
+
+ Page with stale time of 10 minutes
+
+
+
+ >
+ )
+}
diff --git a/test/e2e/app-dir/segment-cache/staleness/app/stale-10-minutes/page.tsx b/test/e2e/app-dir/segment-cache/staleness/app/stale-10-minutes/page.tsx
new file mode 100644
index 0000000000000..fd65b7e2a1242
--- /dev/null
+++ b/test/e2e/app-dir/segment-cache/staleness/app/stale-10-minutes/page.tsx
@@ -0,0 +1,17 @@
+import { Suspense } from 'react'
+import { unstable_cacheLife as cacheLife } from 'next/cache'
+
+async function Content() {
+ 'use cache'
+ await new Promise((resolve) => setTimeout(resolve, 0))
+ cacheLife({ stale: 10 * 60 })
+ return 'Content with stale time of 10 minutes'
+}
+
+export default function Page() {
+ return (
+
+
+
+ )
+}
diff --git a/test/e2e/app-dir/segment-cache/staleness/app/stale-5-minutes/page.tsx b/test/e2e/app-dir/segment-cache/staleness/app/stale-5-minutes/page.tsx
new file mode 100644
index 0000000000000..625e6d7b7af8c
--- /dev/null
+++ b/test/e2e/app-dir/segment-cache/staleness/app/stale-5-minutes/page.tsx
@@ -0,0 +1,17 @@
+import { Suspense } from 'react'
+import { unstable_cacheLife as cacheLife } from 'next/cache'
+
+async function Content() {
+ 'use cache'
+ await new Promise((resolve) => setTimeout(resolve, 0))
+ cacheLife({ stale: 5 * 60 })
+ return 'Content with stale time of 5 minutes'
+}
+
+export default function Page() {
+ return (
+
+
+
+ )
+}
diff --git a/test/e2e/app-dir/segment-cache/staleness/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/staleness/components/link-accordion.tsx
new file mode 100644
index 0000000000000..4b253eab3adf3
--- /dev/null
+++ b/test/e2e/app-dir/segment-cache/staleness/components/link-accordion.tsx
@@ -0,0 +1,23 @@
+'use client'
+
+import Link from 'next/link'
+import { useState } from 'react'
+
+export function LinkAccordion({ href, children }) {
+ const [isVisible, setIsVisible] = useState(false)
+ return (
+ <>
+ setIsVisible(!isVisible)}
+ data-link-accordion={href}
+ />
+ {isVisible ? (
+ {children}
+ ) : (
+ `${children} (link is hidden)`
+ )}
+ >
+ )
+}
diff --git a/test/e2e/app-dir/segment-cache/staleness/next.config.js b/test/e2e/app-dir/segment-cache/staleness/next.config.js
new file mode 100644
index 0000000000000..a74129c5a24f2
--- /dev/null
+++ b/test/e2e/app-dir/segment-cache/staleness/next.config.js
@@ -0,0 +1,12 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {
+ experimental: {
+ ppr: true,
+ dynamicIO: true,
+ clientSegmentCache: true,
+ },
+}
+
+module.exports = nextConfig
diff --git a/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts b/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts
new file mode 100644
index 0000000000000..0e6d892eb0c56
--- /dev/null
+++ b/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts
@@ -0,0 +1,82 @@
+import { nextTestSetup } from 'e2e-utils'
+import type * as Playwright from 'playwright'
+import { createRouterAct } from '../router-act'
+
+describe('segment cache (staleness)', () => {
+ const { next, isNextDev, skipped } = nextTestSetup({
+ files: __dirname,
+ skipDeployment: true,
+ })
+ if (isNextDev || skipped) {
+ test('disabled in development / deployment', () => {})
+ return
+ }
+
+ it('entry expires when its stale time has elapsed', async () => {
+ let page: Playwright.Page
+ const browser = await next.browser('/', {
+ beforePageLoad(p: Playwright.Page) {
+ page = p
+ },
+ })
+ const act = createRouterAct(page)
+
+ await page.clock.install()
+
+ // Reveal the link to trigger a prefetch
+ const toggle5MinutesLink = await browser.elementByCss(
+ 'input[data-link-accordion="/stale-5-minutes"]'
+ )
+ const toggle10MinutesLink = await browser.elementByCss(
+ 'input[data-link-accordion="/stale-10-minutes"]'
+ )
+ await act(
+ async () => {
+ await toggle5MinutesLink.click()
+ await browser.elementByCss('a[href="/stale-5-minutes"]')
+ },
+ {
+ includes: 'Content with stale time of 5 minutes',
+ }
+ )
+ await act(
+ async () => {
+ await toggle10MinutesLink.click()
+ await browser.elementByCss('a[href="/stale-10-minutes"]')
+ },
+ {
+ includes: 'Content with stale time of 10 minutes',
+ }
+ )
+
+ // Hide the links
+ await toggle5MinutesLink.click()
+ await toggle10MinutesLink.click()
+
+ // Fast forward 5 minutes and 1 millisecond
+ await page.clock.fastForward(5 * 60 * 1000 + 1)
+
+ // Reveal the links again to trigger new prefetch tasks
+ await act(
+ async () => {
+ await toggle5MinutesLink.click()
+ await browser.elementByCss('a[href="/stale-5-minutes"]')
+ },
+ // The page with a stale time of 5 minutes is requested again
+ // because its stale time elapsed.
+ {
+ includes: 'Content with stale time of 5 minutes',
+ }
+ )
+
+ await act(
+ async () => {
+ await toggle10MinutesLink.click()
+ await browser.elementByCss('a[href="/stale-10-minutes"]')
+ },
+ // The page with a stale time of 10 minutes is *not* requested again
+ // because it's still fresh.
+ 'no-requests'
+ )
+ })
+})