diff --git a/test/e2e/app-dir/app-client-cache/app/page.js b/test/e2e/app-dir/app-client-cache/app/page.js
index 28215fb9129f4..c0117feac54b1 100644
--- a/test/e2e/app-dir/app-client-cache/app/page.js
+++ b/test/e2e/app-dir/app-client-cache/app/page.js
@@ -7,6 +7,11 @@ export default function HomePage() {
To Random Number - prefetch: true
+
To Random Number - prefetch: auto, slow
diff --git a/test/e2e/app-dir/app-client-cache/app/without-loading/[id]/page.js b/test/e2e/app-dir/app-client-cache/app/without-loading/[id]/page.js
new file mode 100644
index 0000000000000..f2292666f8cf3
--- /dev/null
+++ b/test/e2e/app-dir/app-client-cache/app/without-loading/[id]/page.js
@@ -0,0 +1,21 @@
+import Link from 'next/link'
+
+export default async function Page({ searchParams: { timeout } }) {
+ const randomNumber = await new Promise((resolve) => {
+ setTimeout(
+ () => {
+ resolve(Math.random())
+ },
+ timeout !== undefined ? Number.parseInt(timeout, 10) : 0
+ )
+ })
+
+ return (
+ <>
+
+ Back to Home
+
+
{randomNumber}
+ >
+ )
+}
diff --git a/test/e2e/app-dir/app-client-cache/app/without-loading/page.js b/test/e2e/app-dir/app-client-cache/app/without-loading/page.js
new file mode 100644
index 0000000000000..29c650bcbcddf
--- /dev/null
+++ b/test/e2e/app-dir/app-client-cache/app/without-loading/page.js
@@ -0,0 +1,36 @@
+import Link from 'next/link'
+
+export default function Page() {
+ return (
+ <>
+
+
+ To Random Number - prefetch: true
+
+
+
+
+ To Random Number - prefetch: true, slow
+
+
+
+ To Random Number - prefetch: auto
+
+
+
+ To Random Number 2 - prefetch: false
+
+
+
+
+ To Random Number 2 - prefetch: false, slow
+
+
+
+
+ To Random Number - prefetch: auto, slow
+
+
+ >
+ )
+}
diff --git a/test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts b/test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts
new file mode 100644
index 0000000000000..23098dcef3a7b
--- /dev/null
+++ b/test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts
@@ -0,0 +1,367 @@
+import { nextTestSetup } from 'e2e-utils'
+import { browserConfigWithFixedTime, fastForwardTo } from './test-utils'
+
+describe('app dir client cache semantics (experimental clientRouterCache)', () => {
+ describe('clientRouterCache = true', () => {
+ const { next } = nextTestSetup({
+ files: __dirname,
+ nextConfig: {
+ experimental: { clientRouterCache: true },
+ },
+ })
+
+ describe('prefetch={true}', () => {
+ test('we should get a cached version of the page every request', async () => {
+ const browser = await next.browser('/', browserConfigWithFixedTime)
+
+ const initialRandomNumber = await browser
+ .elementByCss('[href="/0?timeout=0"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/"]').click()
+
+ let newRandomNumber = await browser
+ .elementByCss('[href="/0?timeout=0"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).toBe(newRandomNumber)
+
+ await browser.eval(fastForwardTo, 2 * 60 * 60 * 1000) // fast forward 2 hours
+
+ await browser.elementByCss('[href="/"]').click()
+
+ newRandomNumber = await browser
+ .elementByCss('[href="/0?timeout=0"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).toBe(newRandomNumber)
+ })
+ })
+
+ describe('prefetch={false}', () => {
+ test('we should get a loading state before fetching the page, followed by a cached version of the page every request', async () => {
+ const browser = await next.browser('/', browserConfigWithFixedTime)
+ // verify we rendered the loading state
+ await browser
+ .elementByCss('[href="/2?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#loading')
+
+ const initialRandomNumber = await browser
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/"]').click()
+
+ await browser.eval(fastForwardTo, 2 * 60 * 60 * 1000) // fast forward 2 hours
+
+ const newRandomNumber = await browser
+ .elementByCss('[href="/2?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).toBe(newRandomNumber)
+ })
+
+ describe('without a loading boundary', () => {
+ test('we should get a cached version of the page every request', async () => {
+ const browser = await next.browser(
+ '/without-loading',
+ browserConfigWithFixedTime
+ )
+
+ const initialRandomNumber = await browser
+ .elementByCss('[href="/without-loading/2?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/without-loading"]').click()
+
+ await browser.eval(fastForwardTo, 2 * 60 * 60 * 1000) // fast forward 2 hours
+
+ const newRandomNumber = await browser
+ .elementByCss('[href="/without-loading/2?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).toBe(newRandomNumber)
+ })
+ })
+ })
+
+ describe('prefetch={undefined} - default', () => {
+ test('we should get a loading state before fetching the page, followed by a cached version of the page every request', async () => {
+ const browser = await next.browser('/', browserConfigWithFixedTime)
+
+ // verify we rendered the loading state
+ await browser
+ .elementByCss('[href="/1?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#loading')
+
+ const initialRandomNumber = await browser
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/"]').click()
+
+ await browser.eval(fastForwardTo, 2 * 60 * 60 * 1000) // fast forward 2 hours
+
+ const newRandomNumber = await browser
+ .elementByCss('[href="/1?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).toBe(newRandomNumber)
+ })
+
+ describe('without a loading boundary', () => {
+ test('we should get a cached version of the page every request', async () => {
+ const browser = await next.browser(
+ '/without-loading',
+ browserConfigWithFixedTime
+ )
+
+ const initialRandomNumber = await browser
+ .elementByCss('[href="/without-loading/1?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/without-loading"]').click()
+
+ await browser.eval(fastForwardTo, 2 * 60 * 60 * 1000) // fast forward 2 hours
+
+ const newRandomNumber = await browser
+ .elementByCss('[href="/without-loading/1?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).toBe(newRandomNumber)
+ })
+ })
+ })
+ })
+
+ describe('clientRouterCache = false', () => {
+ const { next } = nextTestSetup({
+ files: __dirname,
+ nextConfig: {
+ experimental: { clientRouterCache: false },
+ },
+ })
+
+ describe('prefetch={true}', () => {
+ test('we should get fresh data on every subsequent navigation', async () => {
+ const browser = await next.browser('/', browserConfigWithFixedTime)
+
+ const initialRandomNumber = await browser
+ .elementByCss('[href="/0?timeout=0"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/"]').click()
+
+ let newRandomNumber = await browser
+ .elementByCss('[href="/0?timeout=0"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).not.toBe(newRandomNumber)
+
+ await browser.eval(fastForwardTo, 5 * 1000) // fast forward 5 seconds
+
+ await browser.elementByCss('[href="/"]').click()
+
+ newRandomNumber = await browser
+ .elementByCss('[href="/0?timeout=0"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).not.toBe(newRandomNumber)
+ })
+
+ test('we should get a loading state before fetching the page, followed by fresh data on every subsequent navigation', async () => {
+ const browser = await next.browser('/', browserConfigWithFixedTime)
+
+ // this test introduces an artificial delay in rendering the requested page, so we verify a loading state is rendered
+ await browser
+ .elementByCss('[href="/0?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#loading')
+
+ const initialRandomNumber = await browser
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/"]').click()
+
+ await browser.eval(fastForwardTo, 5 * 1000) // fast forward 5 seconds
+
+ const newRandomNumber = await browser
+ .elementByCss('[href="/0?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).not.toBe(newRandomNumber)
+ })
+
+ describe('without a loading boundary', () => {
+ test('we should get fresh data on every subsequent navigation', async () => {
+ const browser = await next.browser(
+ '/without-loading',
+ browserConfigWithFixedTime
+ )
+
+ const initialRandomNumber = await browser
+ .elementByCss('[href="/without-loading/0?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/without-loading"]').click()
+
+ let newRandomNumber = await browser
+ .elementByCss('[href="/without-loading/0?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).not.toBe(newRandomNumber)
+
+ await browser.eval(fastForwardTo, 5 * 1000) // fast forward 5 seconds
+
+ await browser.elementByCss('[href="/without-loading"]').click()
+
+ newRandomNumber = await browser
+ .elementByCss('[href="/without-loading/0?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).not.toBe(newRandomNumber)
+ })
+ })
+ })
+
+ describe('prefetch={false}', () => {
+ test('we should get a loading state before fetching the page, followed by fresh data on every subsequent navigation', async () => {
+ const browser = await next.browser('/', browserConfigWithFixedTime)
+
+ // this test introduces an artificial delay in rendering the requested page, so we verify a loading state is rendered
+ await browser
+ .elementByCss('[href="/2?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#loading')
+
+ const initialRandomNumber = await browser
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/"]').click()
+
+ await browser.eval(fastForwardTo, 5 * 1000) // fast forward 5 seconds
+
+ const newRandomNumber = await browser
+ .elementByCss('[href="/2?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).not.toBe(newRandomNumber)
+ })
+
+ describe('without a loading boundary', () => {
+ test('we should get fresh data on every subsequent navigation', async () => {
+ const browser = await next.browser('/', browserConfigWithFixedTime)
+
+ const initialRandomNumber = await browser
+ .elementByCss('[href="/2?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/"]').click()
+
+ await browser.eval(fastForwardTo, 5 * 1000) // fast forward 5 seconds
+
+ const newRandomNumber = await browser
+ .elementByCss('[href="/2?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).not.toBe(newRandomNumber)
+ })
+ })
+ })
+
+ describe('prefetch={undefined} - default', () => {
+ test('we should get a loading state before fetching the page, followed by fresh data on every subsequent navigation', async () => {
+ const browser = await next.browser('/', browserConfigWithFixedTime)
+
+ // this test introduces an artificial delay in rendering the requested page, so we verify a loading state is rendered
+ await browser
+ .elementByCss('[href="/1?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#loading')
+
+ const initialRandomNumber = await browser
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/"]').click()
+
+ await browser.eval(fastForwardTo, 5 * 1000) // fast forward 5 seconds
+
+ const newRandomNumber = await browser
+ .elementByCss('[href="/1?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).not.toBe(newRandomNumber)
+ })
+
+ describe('without a loading boundary', () => {
+ test('we should get fresh data on every subsequent navigation', async () => {
+ const browser = await next.browser(
+ '/without-loading',
+ browserConfigWithFixedTime
+ )
+
+ const initialRandomNumber = await browser
+ .elementByCss('[href="/without-loading/1?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ await browser.elementByCss('[href="/without-loading"]').click()
+
+ const newRandomNumber = await browser
+ .elementByCss('[href="/without-loading/1?timeout=1000"]')
+ .click()
+ .waitForElementByCss('#random-number')
+ .text()
+
+ expect(initialRandomNumber).not.toBe(newRandomNumber)
+ })
+ })
+ })
+ })
+})
diff --git a/test/e2e/app-dir/app-client-cache/client-cache.test.ts b/test/e2e/app-dir/app-client-cache/client-cache.test.ts
index 99cabd73cb027..c71bd30e9e675 100644
--- a/test/e2e/app-dir/app-client-cache/client-cache.test.ts
+++ b/test/e2e/app-dir/app-client-cache/client-cache.test.ts
@@ -1,77 +1,12 @@
import { createNextDescribe } from 'e2e-utils'
import { check } from 'next-test-utils'
import { BrowserInterface } from 'test/lib/browsers/base'
-import type { Request } from 'playwright'
-
-const getPathname = (url: string) => {
- const urlObj = new URL(url)
- return urlObj.pathname
-}
-
-const browserConfigWithFixedTime = {
- beforePageLoad: (page) => {
- page.addInitScript(() => {
- const startTime = new Date()
- const fixedTime = new Date('2023-04-17T00:00:00Z')
-
- // Override the Date constructor
- // @ts-ignore
- // eslint-disable-next-line no-native-reassign
- Date = class extends Date {
- constructor() {
- super()
- // @ts-ignore
- return new startTime.constructor(fixedTime)
- }
-
- static now() {
- return fixedTime.getTime()
- }
- }
- })
- },
-}
-
-const fastForwardTo = (ms) => {
- // Increment the fixed time by the specified duration
- const currentTime = new Date()
- currentTime.setTime(currentTime.getTime() + ms)
-
- // Update the Date constructor to use the new fixed time
- // @ts-ignore
- // eslint-disable-next-line no-native-reassign
- Date = class extends Date {
- constructor() {
- super()
- // @ts-ignore
- return new currentTime.constructor(currentTime)
- }
-
- static now() {
- return currentTime.getTime()
- }
- }
-}
-
-const createRequestsListener = async (browser: BrowserInterface) => {
- // wait for network idle
- await browser.waitForIdleNetwork()
-
- let requests = []
-
- browser.on('request', (req: Request) => {
- requests.push([req.url(), !!req.headers()['next-router-prefetch']])
- })
-
- await browser.refresh()
-
- return {
- getRequests: () => requests,
- clearRequests: () => {
- requests = []
- },
- }
-}
+import {
+ browserConfigWithFixedTime,
+ createRequestsListener,
+ fastForwardTo,
+ getPathname,
+} from './test-utils'
createNextDescribe(
'app dir client cache semantics',
diff --git a/test/e2e/app-dir/app-client-cache/next.config.js b/test/e2e/app-dir/app-client-cache/next.config.js
deleted file mode 100644
index 4ba52ba2c8df6..0000000000000
--- a/test/e2e/app-dir/app-client-cache/next.config.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = {}
diff --git a/test/e2e/app-dir/app-client-cache/test-utils.ts b/test/e2e/app-dir/app-client-cache/test-utils.ts
new file mode 100644
index 0000000000000..56c1199c13d0e
--- /dev/null
+++ b/test/e2e/app-dir/app-client-cache/test-utils.ts
@@ -0,0 +1,72 @@
+import { BrowserInterface } from 'test/lib/browsers/base'
+import type { Request } from 'playwright'
+
+export const getPathname = (url: string) => {
+ const urlObj = new URL(url)
+ return urlObj.pathname
+}
+
+export const browserConfigWithFixedTime = {
+ beforePageLoad: (page) => {
+ page.addInitScript(() => {
+ const startTime = new Date()
+ const fixedTime = new Date('2023-04-17T00:00:00Z')
+
+ // Override the Date constructor
+ // @ts-ignore
+ // eslint-disable-next-line no-native-reassign
+ Date = class extends Date {
+ constructor() {
+ super()
+ // @ts-ignore
+ return new startTime.constructor(fixedTime)
+ }
+
+ static now() {
+ return fixedTime.getTime()
+ }
+ }
+ })
+ },
+}
+
+export const fastForwardTo = (ms) => {
+ // Increment the fixed time by the specified duration
+ const currentTime = new Date()
+ currentTime.setTime(currentTime.getTime() + ms)
+
+ // Update the Date constructor to use the new fixed time
+ // @ts-ignore
+ // eslint-disable-next-line no-native-reassign
+ Date = class extends Date {
+ constructor() {
+ super()
+ // @ts-ignore
+ return new currentTime.constructor(currentTime)
+ }
+
+ static now() {
+ return currentTime.getTime()
+ }
+ }
+}
+
+export const createRequestsListener = async (browser: BrowserInterface) => {
+ // wait for network idle
+ await browser.waitForIdleNetwork()
+
+ let requests = []
+
+ browser.on('request', (req: Request) => {
+ requests.push([req.url(), !!req.headers()['next-router-prefetch']])
+ })
+
+ await browser.refresh()
+
+ return {
+ getRequests: () => requests,
+ clearRequests: () => {
+ requests = []
+ },
+ }
+}
diff --git a/test/ppr-tests-manifest.json b/test/ppr-tests-manifest.json
index dd2e05e89fa70..f9429ddda79fb 100644
--- a/test/ppr-tests-manifest.json
+++ b/test/ppr-tests-manifest.json
@@ -75,7 +75,8 @@
"test/e2e/app-dir/ppr/**/*",
"test/e2e/app-dir/ppr-*/**/*",
"test/e2e/app-dir/app-prefetch*/**/*",
- "test/e2e/app-dir/searchparams-static-bailout/searchparams-static-bailout.test.ts"
+ "test/e2e/app-dir/searchparams-static-bailout/searchparams-static-bailout.test.ts",
+ "test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts"
]
}
}