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/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" ] } }