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: true, slow
+
+
To Random Number - prefetch: auto
@@ -15,6 +20,11 @@ export default function HomePage() {
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..d9bb243eae461
--- /dev/null
+++ b/test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts
@@ -0,0 +1,233 @@
+import { nextTestSetup } from 'e2e-utils'
+import { BrowserInterface } from 'test/lib/browsers/base'
+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 },
+ },
+ })
+
+ let browser: BrowserInterface
+
+ beforeEach(async () => {
+ browser = (await next.browser(
+ '/',
+ browserConfigWithFixedTime
+ )) as BrowserInterface
+ })
+
+ describe('prefetch={true}', () => {
+ test('the prefetched data should remain the same "indefinitely"', async () => {
+ 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 same data "indefinitely"', async () => {
+ // 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('prefetch={undefined} - default', () => {
+ test('we should get a loading state before fetching the page, followed by same data "indefinitely"', async () => {
+ // 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('clientRouterCache = false', () => {
+ const { next } = nextTestSetup({
+ files: __dirname,
+ nextConfig: {
+ experimental: { clientRouterCache: false },
+ },
+ })
+
+ let browser: BrowserInterface
+
+ beforeEach(async () => {
+ browser = (await next.browser(
+ '/',
+ browserConfigWithFixedTime
+ )) as BrowserInterface
+ })
+
+ describe('prefetch={true}', () => {
+ test('the prefetch data should be new each navigation', async () => {
+ 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 () => {
+ // 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('prefetch={false}', () => {
+ test('we should get a loading state before fetching the page, followed by fresh data on every subsequent navigation', async () => {
+ // 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('prefetch={undefined} - default', () => {
+ test('we should get a loading state before fetching the page, followed by fresh data on every subsequent navigation', async () => {
+ // 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)
+ })
+ })
+ })
+})
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 170ead9c5e1ef..dc1c494083996 100644
--- a/test/ppr-tests-manifest.json
+++ b/test/ppr-tests-manifest.json
@@ -76,7 +76,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"
]
}
}