first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,350 @@
import { nextTestSetup } from 'e2e-utils'
import { check, retry } from 'next-test-utils'
import { Playwright } from 'next-webdriver'
import {
browserConfigWithFixedTime,
createRequestsListener,
fastForwardTo,
getPathname,
} from './test-utils'
import path from 'path'
describe('app dir client cache semantics (default semantics)', () => {
const { next, isNextDev } = nextTestSetup({
files: path.join(__dirname, 'fixtures', 'regular'),
})
if (isNextDev) {
// dev doesn't support prefetch={true}, so this just performs a basic test to make sure data is reused for 30s
it('should return fresh data every navigation', async () => {
let browser = await next.browser('/', browserConfigWithFixedTime)
// navigate to prefetch-auto page
await browser.elementByCss('[href="/1"]').click()
let initialNumber = await browser.elementById('random-number').text()
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/1"]').click()
let newNumber = await browser.elementById('random-number').text()
expect(newNumber).not.toBe(initialNumber)
})
} else {
describe('prefetch={true}', () => {
let browser: Playwright
beforeEach(async () => {
browser = await next.browser('/', browserConfigWithFixedTime)
})
it('should prefetch the full page', async () => {
const { getRequests, clearRequests } =
await createRequestsListener(browser)
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
clearRequests()
await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
expect(
getRequests().every(([url]) => getPathname(url) !== '/0')
).toEqual(true)
})
it('should re-use the cache for the full page, only for 5 mins', async () => {
const randomNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
const number = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber).not.toBe(randomNumber)
})
it('should prefetch again after 5 mins if the link is visible again', async () => {
const { getRequests, clearRequests } =
await createRequestsListener(browser)
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
const randomNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
clearRequests()
await browser.elementByCss('[href="/"]').click()
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
const number = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).not.toBe(randomNumber)
})
})
describe('prefetch={false}', () => {
let browser: Playwright
beforeEach(async () => {
browser = await next.browser('/', browserConfigWithFixedTime)
})
it('should not prefetch the page at all', async () => {
const { getRequests } = await createRequestsListener(browser)
await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
expect(
getRequests().filter(([url]) => getPathname(url) === '/2')
).toHaveLength(1)
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/2' && didPartialPrefetch
)
).toBe(false)
})
it('should not re-use the page segment cache', async () => {
const randomNumber = await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
const number = await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).not.toBe(randomNumber)
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber = await browser
.elementByCss('[href="/2"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber).not.toBe(randomNumber)
})
})
describe('prefetch={undefined} - default', () => {
let browser: Playwright
beforeEach(async () => {
browser = await next.browser('/', browserConfigWithFixedTime)
})
it('should prefetch partially a dynamic page', async () => {
const { getRequests, clearRequests } =
await createRequestsListener(browser)
await check(() => {
return getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/1' && didPartialPrefetch
)
? 'success'
: 'fail'
}, 'success')
clearRequests()
await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/1' && !didPartialPrefetch
)
).toBe(true)
})
it('should not re-use the page segment cache', async () => {
const randomNumber = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
const number = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(number).not.toBe(randomNumber)
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber).not.toBe(randomNumber)
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber2 = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber2).not.toBe(newNumber)
})
it('should refetch the full page after 5 mins', async () => {
// Wait for initial prefetch to complete before clicking
await browser.waitForIdleNetwork()
const randomLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const randomNumber = await browser
.waitForElementByCss('#random-number')
.text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser
.elementByCss('[href="/"]')
.click()
.waitForElementByCss('[href="/1?timeout=1000"]')
// Wait for prefetch requests to complete before clicking, otherwise
// clicking during an in-flight prefetch aborts it and skips loading state
await browser.waitForIdleNetwork()
const newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const newNumber = await browser
.waitForElementByCss('#random-number')
.text()
expect(newLoadingNumber).not.toBe(randomLoadingNumber)
expect(newNumber).not.toBe(randomNumber)
})
it('should respect a loading boundary that returns `null`', async () => {
await browser.elementByCss('[href="/null-loading"]').click()
// the page content should disappear immediately
await retry(async () => {
expect(
await browser.hasElementByCssSelector('[href="/null-loading"]')
).toBe(false)
})
// the root layout should still be visible
expect(await browser.hasElementByCssSelector('#root-layout')).toBe(true)
// the dynamic content should eventually appear
await browser.waitForElementByCss('#random-number')
expect(await browser.hasElementByCssSelector('#random-number')).toBe(
true
)
})
})
it('should renew the initial seeded data after expiration time', async () => {
const browser = await next.browser(
'/without-loading/1',
browserConfigWithFixedTime
)
const initialNumber = await browser.elementById('random-number').text()
// Expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/without-loading"]').click()
await browser.elementByCss('[href="/without-loading/1"]').click()
const newNumber = await browser.elementById('random-number').text()
// The number should be different, as the seeded data has expired after 30s
expect(newNumber).not.toBe(initialNumber)
})
}
})

View File

@@ -0,0 +1,457 @@
import { nextTestSetup } from 'e2e-utils'
import { browserConfigWithFixedTime, fastForwardTo } from './test-utils'
import { findAllTelemetryEvents } from 'next-test-utils'
import path from 'path'
describe('app dir client cache semantics (experimental staleTimes)', () => {
describe('dynamic: 0', () => {
const { next, isNextDev, isNextDeploy } = nextTestSetup({
files: path.join(__dirname, 'fixtures', 'regular'),
nextConfig: {
experimental: { staleTimes: { dynamic: 0 } },
},
env: {
NEXT_TELEMETRY_DEBUG: '1',
},
})
if (isNextDev) {
// dev doesn't support prefetch={true}, so this just performs a basic test to make sure data is fresh on each navigation
it('should trigger a loading state before fetching the page, followed by fresh data on every subsequent navigation', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
// Wait for initial prefetch to complete before clicking
await browser.waitForIdleNetwork()
// 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', () => {
it('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)
})
})
return
}
describe('prefetch={true}', () => {
it('should re-use the cache for 5 minutes (default "static" time)', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
// Wait for the prefetch to complete before clicking
await browser.waitForIdleNetwork()
let 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, 30 * 1000) // fast forward 30 seconds
await browser.elementByCss('[href="/"]').click()
newRandomNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(initialRandomNumber).toBe(newRandomNumber)
await browser.eval(fastForwardTo, 5 * 60 * 1000) // fast forward 5 minutes
await browser.elementByCss('[href="/"]').click()
newRandomNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(initialRandomNumber).not.toBe(newRandomNumber)
})
})
describe('prefetch={false}', () => {
it('should trigger a loading state before fetching the page, followed by fresh data on every subsequent navigation', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
// Wait for initial prefetch to complete before clicking
await browser.waitForIdleNetwork()
// 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', () => {
it('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', () => {
it('should trigger a loading state before fetching the page, followed by fresh data on every subsequent navigation', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
// Wait for initial prefetch to complete before clicking
await browser.waitForIdleNetwork()
// 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', () => {
it('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)
})
})
})
if (!isNextDeploy) {
describe('telemetry', () => {
it('should send staleTimes feature usage event', async () => {
const events = findAllTelemetryEvents(
next.cliOutput,
'NEXT_CLI_SESSION_STARTED'
)
expect(events).toEqual(
expect.arrayContaining([
expect.objectContaining({
staticStaleTime: null,
dynamicStaleTime: 0,
}),
])
)
})
})
}
})
describe('static: 180', () => {
const { next, isNextDev, isNextDeploy } = nextTestSetup({
files: path.join(__dirname, 'fixtures', 'regular'),
nextConfig: {
experimental: { staleTimes: { static: 180 } },
},
env: {
NEXT_TELEMETRY_DEBUG: '1',
},
})
if (isNextDev) {
// since the router behavior is different in development mode (no viewport prefetching + liberal revalidation)
// we only check the production behavior
it('should skip dev', () => {})
return
}
describe('prefetch={true}', () => {
it('should use the custom static override time (3 minutes)', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
// Wait for the prefetch to complete before clicking
await browser.waitForIdleNetwork()
let 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, 30 * 1000) // fast forward 30 seconds
await browser.elementByCss('[href="/"]').click()
newRandomNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(initialRandomNumber).toBe(newRandomNumber)
await browser.eval(fastForwardTo, 3 * 60 * 1000) // fast forward 3 minutes
await browser.elementByCss('[href="/"]').click()
newRandomNumber = await browser
.elementByCss('[href="/0?timeout=0"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(initialRandomNumber).not.toBe(newRandomNumber)
})
})
describe('prefetch={undefined} - default', () => {
it('should re-use the loading boundary for the custom static override time (3 minutes)', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
// Wait for initial prefetch to complete before clicking
await browser.waitForIdleNetwork()
const loadingRandomNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
await browser.eval(fastForwardTo, 2 * 60 * 1000) // fast forward 2 minutes
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1?timeout=1000"]')
await browser.waitForIdleNetwork()
let newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
expect(loadingRandomNumber).toBe(newLoadingNumber)
await browser.eval(fastForwardTo, 2 * 60 * 1000) // fast forward 2 minutes
await browser.elementByCss('[href="/"]').click()
// Wait for link to be visible (triggers prefetch), then wait for prefetch to complete
await browser.waitForElementByCss('[href="/1?timeout=1000"]')
await browser.waitForIdleNetwork()
newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
expect(loadingRandomNumber).not.toBe(newLoadingNumber)
})
})
if (!isNextDeploy) {
describe('telemetry', () => {
it('should send staleTimes feature usage event', async () => {
const events = findAllTelemetryEvents(
next.cliOutput,
'NEXT_CLI_SESSION_STARTED'
)
expect(events).toEqual(
expect.arrayContaining([
expect.objectContaining({
staticStaleTime: 180,
dynamicStaleTime: null,
}),
])
)
})
})
}
})
describe('dynamic: 0, static: 0', () => {
const { next } = nextTestSetup({
files: path.join(__dirname, 'fixtures', 'regular'),
nextConfig: {
experimental: { staleTimes: { dynamic: 0, static: 0 } },
},
env: {
NEXT_TELEMETRY_DEBUG: '1',
},
})
// dev doesn't support prefetch={true}, so this just performs a basic test to make sure data is fresh on each navigation
it('should trigger a loading state before fetching the page, followed by fresh data on every subsequent navigation', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
// Wait for initial prefetch to complete before clicking
await browser.waitForIdleNetwork()
// 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', () => {
it('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)
})
})
})
})

View File

@@ -0,0 +1,467 @@
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import { Playwright } from 'next-webdriver'
import {
browserConfigWithFixedTime,
createRequestsListener,
fastForwardTo,
getPathname,
} from './test-utils'
import path from 'path'
// This preserves existing tests for the 30s/5min heuristic (previous router defaults)
describe('app dir client cache semantics (30s/5min)', () => {
const { next, isNextDev } = nextTestSetup({
files: path.join(__dirname, 'fixtures', 'regular'),
nextConfig: {
experimental: { staleTimes: { dynamic: 30, static: 180 } },
},
})
if (isNextDev) {
// dev doesn't support prefetch={true}, so this just performs a basic test to make sure data is reused for 30s
it('should renew the 30s cache once the data is revalidated', async () => {
let browser = await next.browser('/', browserConfigWithFixedTime)
// navigate to prefetch-auto page
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
let initialNumber = await browser.elementById('random-number').text()
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
let newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s.
expect(newNumber).toBe(initialNumber)
// Fast forward to expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
newNumber = await browser.elementById('random-number').text()
// ~35s have passed, so the cache should be expired and the number should be different
expect(newNumber).not.toBe(initialNumber)
// once the number is updated, we should have a renewed 30s cache for this entry
// store this new number so we can check that it stays the same
initialNumber = newNumber
await browser.eval(fastForwardTo, 5 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s (part 2).
expect(newNumber).toBe(initialNumber)
})
} else {
describe('prefetch={true}', () => {
let browser: Playwright
beforeEach(async () => {
browser = await next.browser('/', browserConfigWithFixedTime)
})
it('should prefetch the full page', async () => {
const { getRequests, clearRequests } =
await createRequestsListener(browser)
await retry(() => {
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
).toBe(true)
})
clearRequests()
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
await retry(() => {
const requests = getRequests()
expect(requests.every(([url]) => getPathname(url) !== '/0')).toBe(
true
)
})
})
it('should re-use the cache for the full page, only for 5 mins', async () => {
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/0?timeout=0"]')
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
const number = await browser.elementById('random-number').text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/0?timeout=0"]')
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
expect(newNumber).not.toBe(randomNumber)
})
it('should prefetch again after 5 mins if the link is visible again', async () => {
const { getRequests, clearRequests } =
await createRequestsListener(browser)
await retry(() => {
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
).toBe(true)
})
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
clearRequests()
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/0?timeout=0"]')
await retry(() => {
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
).toBe(true)
})
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
const number = await browser.elementById('random-number').text()
expect(number).not.toBe(randomNumber)
})
})
describe('prefetch={false}', () => {
let browser: Playwright
beforeEach(async () => {
browser = await next.browser('/', browserConfigWithFixedTime)
})
it('should not prefetch the page at all', async () => {
const { getRequests } = await createRequestsListener(browser)
await browser.elementByCss('[href="/2"]').click()
await browser.waitForElementByCss('#random-number')
await retry(() => {
const requests = getRequests().filter(
([url]) => getPathname(url) === '/2'
)
expect(requests.length).toBe(1)
})
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/2' && didPartialPrefetch
)
).toBe(false)
})
it('should re-use the cache only for 30 seconds', async () => {
await browser.elementByCss('[href="/2"]').click()
await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/2"]')
await browser.elementByCss('[href="/2"]').click()
await browser.waitForElementByCss('#random-number')
const number = await browser.elementById('random-number').text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/2"]')
await browser.elementByCss('[href="/2"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
expect(newNumber).not.toBe(randomNumber)
})
})
describe('prefetch={undefined} - default', () => {
let browser: Playwright
beforeEach(async () => {
browser = await next.browser('/', browserConfigWithFixedTime)
})
it('should prefetch partially a dynamic page', async () => {
const { getRequests, clearRequests } =
await createRequestsListener(browser)
await retry(() => {
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/1' && didPartialPrefetch
)
).toBe(true)
})
clearRequests()
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
await retry(() => {
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/1' && !didPartialPrefetch
)
).toBe(true)
})
})
it('should re-use the full cache for only 30 seconds', async () => {
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
const number = await browser.elementById('random-number').text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
expect(newNumber).toBe(randomNumber)
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber2 = await browser.elementById('random-number').text()
expect(newNumber2).not.toBe(newNumber)
})
it('should renew the 30s cache once the data is revalidated', async () => {
// navigate to prefetch-auto page
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
let initialNumber = await browser.elementById('random-number').text()
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
let newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s.
expect(newNumber).toBe(initialNumber)
// Fast forward to expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
newNumber = await browser.elementById('random-number').text()
// ~35s have passed, so the cache should be expired and the number should be different
expect(newNumber).not.toBe(initialNumber)
// once the number is updated, we should have a renewed 30s cache for this entry
// store this new number so we can check that it stays the same
initialNumber = newNumber
await browser.eval(fastForwardTo, 5 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s (part 2).
expect(newNumber).toBe(initialNumber)
})
it('should refetch below the fold after 30 seconds', async () => {
await browser.elementByCss('[href="/1?timeout=1000"]').click()
await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1?timeout=1000"]')
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/1?timeout=1000"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
expect(newNumber).not.toBe(randomNumber)
})
it('should refetch the full page after 5 mins', async () => {
// Wait for initial prefetch to complete before clicking
await browser.waitForIdleNetwork()
const randomLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const randomNumber = await browser
.waitForElementByCss('#random-number')
.text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser
.elementByCss('[href="/"]')
.click()
.waitForElementByCss('[href="/1?timeout=1000"]')
// Wait for prefetch requests to complete before clicking, otherwise
// clicking during an in-flight prefetch aborts it and skips loading state
await browser.waitForIdleNetwork()
const newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const newNumber = await browser
.waitForElementByCss('#random-number')
.text()
expect(newLoadingNumber).not.toBe(randomLoadingNumber)
expect(newNumber).not.toBe(randomNumber)
})
it('should respect a loading boundary that returns `null`', async () => {
await browser.elementByCss('[href="/null-loading"]').click()
// the page content should disappear immediately
await retry(async () => {
expect(
await browser.hasElementByCssSelector('[href="/null-loading"]')
).toBe(false)
})
// the root layout should still be visible
expect(await browser.hasElementByCssSelector('#root-layout')).toBe(true)
// the dynamic content should eventually appear
await browser.waitForElementByCss('#random-number')
expect(await browser.hasElementByCssSelector('#random-number')).toBe(
true
)
})
})
it('should seed the prefetch cache with the fetched page data', async () => {
const browser = await next.browser('/1', browserConfigWithFixedTime)
await browser.waitForElementByCss('#random-number')
const initialNumber = await browser.elementById('random-number').text()
// Move forward a few seconds, navigate off the page and then back to it
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.waitForIdleNetwork()
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
// The number should be the same as we've seeded it in the prefetch cache when we loaded the full page
expect(newNumber).toBe(initialNumber)
})
it('should renew the initial seeded data after expiration time', async () => {
const browser = await next.browser(
'/without-loading/1',
browserConfigWithFixedTime
)
await browser.waitForElementByCss('#random-number')
const initialNumber = await browser.elementById('random-number').text()
// Expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/without-loading"]').click()
await browser.waitForElementByCss('[href="/without-loading/1"]')
await browser.elementByCss('[href="/without-loading/1"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
// The number should be different, as the seeded data has expired after 30s
expect(newNumber).not.toBe(initialNumber)
})
}
})

View File

@@ -0,0 +1,134 @@
import { nextTestSetup } from 'e2e-utils'
import { createRouterAct } from 'router-act'
import path from 'path'
import { Playwright } from 'next-webdriver'
import type { Page as PlaywrightPage } from 'playwright'
describe('app dir client cache with parallel routes', () => {
const { next, isNextDev } = nextTestSetup({
files: path.join(__dirname, 'fixtures', 'parallel-routes'),
})
if (isNextDev) {
// dev doesn't support prefetch={true}
it('should skip dev', () => {})
return
}
async function reveal(browser: Playwright, href: string) {
// Get the reveal element and scroll it into view.
const reveal = await browser.elementByCss(`[data-link-accordion="${href}"]`)
await reveal.scrollIntoViewIfNeeded()
// Click the reveal element to reveal the content.
await reveal.click()
// Return the anchor link element.
return browser.elementByCss(`a[href="${href}"]`)
}
describe('prefetch={true}', () => {
it('should prefetch the full page', async () => {
let page: PlaywrightPage
const browser = await next.browser('/', {
async beforePageLoad(p) {
page = p
await page.clock.install()
},
})
const act = createRouterAct(page)
// Reveal the link to trigger prefetch and wait for it to complete
const link = await act(
async () => {
return reveal(browser, '/0')
},
{ includes: 'random-number' }
)
// Navigate to /0 - should not make additional requests
await act(async () => {
await link.click()
await browser.waitForElementByCss('#random-number')
}, 'no-requests')
})
it('should re-use the cache for the full page, only for 5 mins', async () => {
let page: PlaywrightPage
const browser = await next.browser('/', {
async beforePageLoad(p) {
page = p
await page.clock.install()
},
})
const act = createRouterAct(page)
// Toggle the link, assert on the prefetch content
const link = await act(
async () => {
await reveal(browser, '/0')
return browser.elementByCss('[href="/0"]')
},
{ includes: 'random-number' }
)
// Navigate to the page, assert no requests are made
const randomNumber = await act(async () => {
await link.click()
await browser.waitForElementByCss('#random-number')
return browser.elementByCss('#random-number').text()
}, 'no-requests')
// Toggle the home link, assert on the homepage content
const homeLink = await act(
async () => {
return reveal(browser, '/')
},
{ includes: 'home-page' }
)
// Navigate home, assert no requests are made
await act(async () => {
await homeLink.click()
await browser.waitForElementByCss('#home-page')
}, 'no-requests')
// Toggle the link to the other page again, navigate, assert no requests (because it's cached)
const number = await act(async () => {
const link = await reveal(browser, '/0')
await link.click()
await browser.waitForElementByCss('#random-number')
return browser.elementByCss('#random-number').text()
}, 'no-requests')
expect(number).toBe(randomNumber)
// Navigate back home
await act(async () => {
const link = await reveal(browser, '/')
await link.click()
await browser.waitForElementByCss('#home-page')
}, 'no-requests')
// Fast forward 5 minutes
await page.clock.fastForward(5 * 60 * 1000)
// Toggle the link to the other page again, assert on prefetch content
const linkAfterExpiry = await act(
async () => {
return reveal(browser, '/0')
},
{ includes: 'random-number' }
)
// Navigate to the page and verify the content is fresh (different from cached)
const newNumber = await act(async () => {
await linkAfterExpiry.click()
await browser.waitForElementByCss('#random-number')
return browser.elementByCss('#random-number').text()
}, 'no-requests')
expect(newNumber).not.toBe(randomNumber)
})
})
})

View File

@@ -0,0 +1,8 @@
export default async function Page(props) {
const params = await props.params
return (
<div>
Catchall <pre>{JSON.stringify(params)}</pre>{' '}
</div>
)
}

View File

@@ -0,0 +1,5 @@
import { notFound } from 'next/navigation'
export default function Default() {
notFound()
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Root Breadcrumb</div>
}

View File

@@ -0,0 +1,16 @@
import { LinkAccordion } from '../components/link-accordion'
export default async function Page() {
const randomNumber = await new Promise((resolve) => {
setTimeout(() => {
resolve(Math.random())
}, 1000)
})
return (
<div id="dynamic-page">
<LinkAccordion href="/">Back to Home</LinkAccordion>
<div id="random-number">{randomNumber}</div>
</div>
)
}

View File

@@ -0,0 +1,25 @@
'use client'
import Link from 'next/link'
import { useState } from 'react'
export function LinkAccordion({ href, children, prefetch = undefined }) {
const [isVisible, setIsVisible] = useState(false)
return (
<>
<input
type="checkbox"
checked={isVisible}
onChange={() => setIsVisible(!isVisible)}
data-link-accordion={href}
/>
{isVisible ? (
<Link href={href} prefetch={prefetch}>
{children}
</Link>
) : (
<>{children} (link is hidden)</>
)}
</>
)
}

View File

@@ -0,0 +1,12 @@
export default function Root({ children, breadcrumbs }) {
return (
<html>
<head></head>
<body>
<div>{breadcrumbs}</div>
<div id="root-layout">Root Layout</div>
<div>{children}</div>
</body>
</html>
)
}

View File

@@ -0,0 +1,11 @@
import { LinkAccordion } from './components/link-accordion'
export default function Page() {
return (
<div id="home-page">
<LinkAccordion href="/0" prefetch={true}>
To Dynamic Page
</LinkAccordion>
</div>
)
}

View File

@@ -0,0 +1,9 @@
export default async function Page() {
const randomNumber = Math.random()
return (
<div>
<div>LOADING</div>
<div id="loading">{randomNumber}</div>
</div>
)
}

View File

@@ -0,0 +1,22 @@
import Link from 'next/link'
export default async function Page({ searchParams }) {
const timeout = (await searchParams).timeout
const randomNumber = await new Promise((resolve) => {
setTimeout(
() => {
resolve(Math.random())
},
timeout !== undefined ? Number.parseInt(timeout, 10) : 0
)
})
return (
<>
<div>
<Link href="/"> Back to Home </Link>
</div>
<div id="random-number">{randomNumber}</div>
</>
)
}

View File

@@ -0,0 +1,11 @@
export default function Root({ children }) {
return (
<html>
<head></head>
<body>
<div id="root-layout">Root Layout</div>
<div>{children}</div>
</body>
</html>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,15 @@
export const dynamic = 'force-dynamic'
export default async function Page() {
const randomNumber = await new Promise((resolve) => {
setTimeout(() => {
resolve(Math.random())
}, 1000)
})
return (
<div>
Page Data! <div id="random-number">{randomNumber}</div>
</div>
)
}

View File

@@ -0,0 +1,38 @@
import Link from 'next/link'
export default function HomePage() {
return (
<>
<div>
<Link href="/0?timeout=0" prefetch={true}>
To Random Number - prefetch: true
</Link>
</div>
<div>
<Link href="/0?timeout=1000" prefetch={true}>
To Random Number - prefetch: true, slow
</Link>
</div>
<div>
<Link href="/1">To Random Number - prefetch: auto</Link>
</div>
<div>
<Link href="/2" prefetch={false}>
To Random Number 2 - prefetch: false
</Link>
</div>
<div>
<Link href="/2?timeout=1000" prefetch={false}>
To Random Number 2 - prefetch: false, slow
</Link>
</div>
<div>
<Link href="/1?timeout=1000">
To Random Number - prefetch: auto, slow
</Link>
</div>
<div>
<Link href="/null-loading">To Null Loading - prefetch: auto</Link>
</div>
</>
)
}

View File

@@ -0,0 +1,22 @@
import Link from 'next/link'
export default async function Page({ searchParams }) {
const timeout = (await searchParams).timeout
const randomNumber = await new Promise((resolve) => {
setTimeout(
() => {
resolve(Math.random())
},
timeout !== undefined ? Number.parseInt(timeout, 10) : 0
)
})
return (
<>
<div>
<Link href="/without-loading">Back to Home</Link>
</div>
<div id="random-number">{randomNumber}</div>
</>
)
}

View File

@@ -0,0 +1,36 @@
import Link from 'next/link'
export default function Page() {
return (
<>
<div>
<Link href="/without-loading/0?timeout=0" prefetch={true}>
To Random Number - prefetch: true
</Link>
</div>
<div>
<Link href="/without-loading/0?timeout=1000" prefetch={true}>
To Random Number - prefetch: true, slow
</Link>
</div>
<div>
<Link href="/without-loading/1">To Random Number - prefetch: auto</Link>
</div>
<div>
<Link href="/without-loading/2" prefetch={false}>
To Random Number 2 - prefetch: false
</Link>
</div>
<div>
<Link href="/without-loading/2?timeout=1000" prefetch={false}>
To Random Number 2 - prefetch: false, slow
</Link>
</div>
<div>
<Link href="/without-loading/1?timeout=1000">
To Random Number - prefetch: auto, slow
</Link>
</div>
</>
)
}

View File

@@ -0,0 +1,71 @@
import { Playwright } from 'next-webdriver'
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: Playwright) => {
// wait for network idle
await browser.waitForIdleNetwork()
let requests = []
browser.on('request', (req) => {
requests.push([req.url(), !!req.headers()['next-router-prefetch']])
})
await browser.refresh()
return {
getRequests: () => requests,
clearRequests: () => {
requests = []
},
}
}