next.js / test /e2e /filesystem-cache /filesystem-cache.test.ts
AbdulElahGwaith's picture
Upload folder using huggingface_hub
b91e262 verified
/* eslint-disable jest/no-standalone-expect */
import { nextTestSetup, isNextDev } from 'e2e-utils'
import { waitFor } from 'next-test-utils'
for (const cacheEnabled of [false, true]) {
describe(`filesystem-caching with cache ${cacheEnabled ? 'enabled' : 'disabled'}`, () => {
beforeAll(() => {
process.env.NEXT_PUBLIC_ENV_VAR = 'hello world'
})
afterAll(() => {
delete process.env.NEXT_PUBLIC_ENV_VAR
})
let envVars = [
`ENABLE_CACHING=${cacheEnabled ? '1' : ''}`,
// Make it easier to run in development, test directories are cleared between runs already so this is safe.
`TURBO_ENGINE_IGNORE_DIRTY=1`,
// decrease the idle timeout to make the test more reliable
`TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS=1000`,
].join(' ')
const { skipped, next, isTurbopack } = nextTestSetup({
files: __dirname,
skipDeployment: true,
packageJson: {
scripts: {
build: `${envVars} next build`,
dev: `${envVars} next dev`,
start: 'next start',
},
},
// We need to use npm here as pnpms symlinks trigger a weird bug (kernel bug?)
installCommand: 'npm i',
// Next is always started with caching, but this can disable it for the followup restarts
buildCommand: `npm run build`,
startCommand: isNextDev ? 'npm run dev' : 'npm run start',
})
if (skipped) {
return
}
beforeAll(() => {
// We can skip the dev watch delay since this is not an HMR test
;(next as any).handleDevWatchDelayBeforeChange = () => {}
;(next as any).handleDevWatchDelayAfterChange = () => {}
})
async function restartCycle() {
await stop()
await start()
}
async function stop() {
if (isNextDev) {
// Give FileSystem Cache time to write to disk
// Turbopack is configured to wait 1s above.
// Webpack has an idle timeout (after large changes) of 1s
// and we give time a bit more to allow writing to disk
await waitFor(3000)
}
await next.stop()
}
async function start() {
await next.start()
}
// Very flakey with Webpack enabled
;(process.env.IS_TURBOPACK_TEST ? it : it.skip)(
'should cache or not cache loaders',
async () => {
let appTimestamp, unchangedTimestamp, appClientTimestamp, pagesTimestamp
{
const browser = await next.browser('/')
appTimestamp = await browser.elementByCss('main').text()
expect(appTimestamp).toMatch(/Timestamp = \d+$/)
await browser.close()
}
{
const browser = await next.browser('/unchanged')
unchangedTimestamp = await browser.elementByCss('main').text()
expect(unchangedTimestamp).toMatch(/Timestamp = \d+$/)
await browser.close()
}
{
const browser = await next.browser('/client')
appClientTimestamp = await browser.elementByCss('main').text()
expect(appClientTimestamp).toMatch(/Timestamp = \d+$/)
await browser.close()
}
{
const browser = await next.browser('/pages')
pagesTimestamp = await browser.elementByCss('main').text()
expect(pagesTimestamp).toMatch(/Timestamp = \d+$/)
await browser.close()
}
await restartCycle()
{
const browser = await next.browser('/')
const newTimestamp = await browser.elementByCss('main').text()
expect(newTimestamp).toMatch(/Timestamp = \d+$/)
if (cacheEnabled) {
expect(newTimestamp).toBe(appTimestamp)
} else {
expect(newTimestamp).not.toBe(appTimestamp)
}
await browser.close()
}
{
const browser = await next.browser('/unchanged')
const newTimestamp = await browser.elementByCss('main').text()
expect(newTimestamp).toMatch(/Timestamp = \d+$/)
if (cacheEnabled) {
expect(newTimestamp).toBe(unchangedTimestamp)
} else {
expect(newTimestamp).not.toBe(unchangedTimestamp)
}
await browser.close()
}
{
const browser = await next.browser('/client')
const newTimestamp = await browser.elementByCss('main').text()
expect(newTimestamp).toMatch(/Timestamp = \d+$/)
if (cacheEnabled) {
expect(newTimestamp).toBe(appClientTimestamp)
} else {
expect(newTimestamp).not.toBe(appClientTimestamp)
}
await browser.close()
}
{
const browser = await next.browser('/pages')
const newTimestamp = await browser.elementByCss('main').text()
expect(newTimestamp).toMatch(/Timestamp = \d+$/)
if (cacheEnabled) {
expect(newTimestamp).toBe(pagesTimestamp)
} else {
expect(newTimestamp).not.toBe(pagesTimestamp)
}
await browser.close()
}
}
)
function makeTextCheck(url: string, text: string) {
return textCheck.bind(null, url, text)
}
async function textCheck(url: string, text: string) {
const browser = await next.browser(url)
expect(await browser.elementByCss('p').text()).toBe(text)
await browser.close()
}
function makeFileEdit(file: string) {
return async (inner: () => Promise<void>) => {
await next.patchFile(
file,
(content) => {
return content.replace('hello world', 'hello filesystem cache')
},
inner
)
}
}
interface Change {
checkInitial(): Promise<void>
withChange(previous: () => Promise<void>): Promise<void>
checkChanged(): Promise<void>
fullInvalidation?: boolean
}
const POTENTIAL_CHANGES: Record<string, Change> = {
'RSC change': {
checkInitial: makeTextCheck('/', 'hello world'),
withChange: makeFileEdit('app/page.tsx'),
checkChanged: makeTextCheck('/', 'hello filesystem cache'),
},
'RCC change': {
checkInitial: makeTextCheck('/client', 'hello world'),
withChange: makeFileEdit('app/client/page.tsx'),
checkChanged: makeTextCheck('/client', 'hello filesystem cache'),
},
'Pages change': {
checkInitial: makeTextCheck('/pages', 'hello world'),
withChange: makeFileEdit('pages/pages.tsx'),
checkChanged: makeTextCheck('/pages', 'hello filesystem cache'),
},
'rename app page': {
checkInitial: makeTextCheck('/remove-me', 'hello world'),
async withChange(inner) {
await next.renameFolder('app/remove-me', 'app/add-me')
try {
await inner()
} finally {
await next.renameFolder('app/add-me', 'app/remove-me')
}
},
checkChanged: makeTextCheck('/add-me', 'hello world'),
},
// TODO fix this case with Turbopack
...(isTurbopack
? {}
: {
'loader change': {
async checkInitial() {
await textCheck('/loader', 'hello world')
await textCheck('/loader/client', 'hello world')
},
withChange: makeFileEdit('my-loader.js'),
async checkChanged() {
await textCheck('/loader', 'hello filesystem cache')
await textCheck('/loader/client', 'hello filesystem cache')
},
fullInvalidation: !isTurbopack,
},
}),
'next config change': {
async checkInitial() {
await textCheck('/next-config', 'hello world')
await textCheck('/next-config/client', 'hello world')
},
withChange: makeFileEdit('next.config.js'),
async checkChanged() {
await textCheck('/next-config', 'hello filesystem cache')
await textCheck('/next-config/client', 'hello filesystem cache')
},
fullInvalidation: !isTurbopack,
},
'env var change': {
async checkInitial() {
await textCheck('/env', 'hello world')
await textCheck('/env/client', 'hello world')
},
async withChange(inner) {
process.env.NEXT_PUBLIC_ENV_VAR = 'hello filesystem cache'
try {
await inner()
} finally {
process.env.NEXT_PUBLIC_ENV_VAR = 'hello world'
}
},
async checkChanged() {
await textCheck('/env', 'hello filesystem cache')
await textCheck('/env/client', 'hello filesystem cache')
},
},
} as const
// Checking only single change and all combined for performance reasons.
const combinations = Object.entries(POTENTIAL_CHANGES).map(([k, v]) => [
k,
[v],
]) as Array<[string, Array<Change>]>
combinations.push([
Object.keys(POTENTIAL_CHANGES).join(', '),
Object.values(POTENTIAL_CHANGES),
])
for (const [name, changes] of combinations) {
// Very flakey with Webpack enabled
;(process.env.IS_TURBOPACK_TEST ? it : it.skip)(
`should allow to change files while stopped (${name})`,
async () => {
let fullInvalidation = !cacheEnabled
for (const change of changes) {
await change.checkInitial()
if (change.fullInvalidation) {
fullInvalidation = true
}
}
let unchangedTimestamp: string
if (!fullInvalidation) {
const browser = await next.browser('/unchanged')
unchangedTimestamp = await browser.elementByCss('main').text()
expect(unchangedTimestamp).toMatch(/Timestamp = \d+$/)
await browser.close()
}
async function checkChanged() {
for (const change of changes) {
await change.checkChanged()
}
if (!fullInvalidation) {
const browser = await next.browser('/unchanged')
const timestamp = await browser.elementByCss('main').text()
expect(unchangedTimestamp).toEqual(timestamp)
await browser.close()
}
}
await stop()
async function inner() {
await start()
await checkChanged()
// Some no-op change builds
for (let i = 0; i < 2; i++) {
await restartCycle()
await checkChanged()
}
await stop()
}
let current = inner
for (const change of changes) {
const prev = current
current = () => change.withChange(prev)
}
await current()
await start()
for (const change of changes) {
await change.checkInitial()
}
if (!fullInvalidation) {
const browser = await next.browser('/unchanged')
const timestamp = await browser.elementByCss('main').text()
expect(unchangedTimestamp).toEqual(timestamp)
await browser.close()
}
},
200000
)
}
})
}