mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
https://github.com/vercel/next.js/pull/94197 had a bug in the Turbopack case, where it didn't properly trace all necessary files referenced from `cacheHandler` and `cacheHandlers`. If you imported `lodash` in the cache handler, then - `node_modules/.pnpm/[email protected].1/node_modules/lodash/lodash.js` was correctly traced but missing were - `node_modules/.pnpm/[email protected].1/node_modules/lodash/package.json` and - `node_modules/lodash` (the symlink) The problem was that it wasn't including affecting sources of these entrypoints, because it was treating those entrypoints as bundled: ``` SingleModuleGraph::new_inner( entries: Vec<ChunkGroupEntry>, visited_modules: &FxIndexMap<ResolvedVc<Box<dyn Module>>, GraphNodeIndex>, include_traced: bool, include_binding_usage: bool, entries_are_traced: bool, // <------ problematic ) -> Result<Vc<Self>> ``` should really have been ``` SingleModuleGraph::new_inner( entries: &GraphEntries, // contains Vec<ChunkGroupEntry> and traced_modules: Vec<Modules visited_modules: &FxIndexMap<ResolvedVc<Box<dyn Module>>, GraphNodeIndex>, include_traced: bool, include_binding_usage: bool, ) -> Result<Vc<Self>> { ``` because the single `entries_are_traced: bool` can be different per entry.
646 lines
20 KiB
TypeScript
646 lines
20 KiB
TypeScript
import fs from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import cheerio from 'cheerio'
|
|
import { FileRef, nextTestSetup } from 'e2e-utils'
|
|
import {
|
|
createNowRouteMatches,
|
|
fetchViaHTTP,
|
|
findPort,
|
|
initNextServerScript,
|
|
killApp,
|
|
retry,
|
|
withInvocationId,
|
|
} from 'next-test-utils'
|
|
import { ChildProcess } from 'node:child_process'
|
|
|
|
// TODO(NAR-423): Migrate to Cache Components.
|
|
describe.skip('required server files app router', () => {
|
|
let server: ChildProcess
|
|
let appPort: number | string
|
|
let delayedPostpone: string
|
|
let rewritePostpone: string
|
|
let secondCookiePostpone: string
|
|
let secondCookieHTML: string
|
|
let cliOutput = ''
|
|
|
|
beforeAll(() => {
|
|
process.env.NOW_BUILDER = '1'
|
|
process.env.NEXT_PRIVATE_TEST_HEADERS = '1'
|
|
process.env.NEXT_PRIVATE_DEBUG_CACHE_ENTRY_HANDLERS =
|
|
'./cache-entry-handlers.js'
|
|
})
|
|
|
|
const { next } = nextTestSetup({
|
|
files: {
|
|
app: new FileRef(join(__dirname, 'app')),
|
|
'pages/catch-all/[[...rest]].js': new FileRef(
|
|
join(__dirname, 'pages', 'catch-all', '[[...rest]].js')
|
|
),
|
|
lib: new FileRef(join(__dirname, 'lib')),
|
|
'cache-handler.js': new FileRef(join(__dirname, 'cache-handler.js')),
|
|
'middleware.js': new FileRef(join(__dirname, 'middleware.js')),
|
|
'data.txt': new FileRef(join(__dirname, 'data.txt')),
|
|
'.env': new FileRef(join(__dirname, '.env')),
|
|
'.env.local': new FileRef(join(__dirname, '.env.local')),
|
|
'.env.production': new FileRef(join(__dirname, '.env.production')),
|
|
'cache-entry-handlers.js': new FileRef(
|
|
join(__dirname, 'cache-entry-handlers.js')
|
|
),
|
|
},
|
|
overrideFiles: {
|
|
'app/not-found.js': new FileRef(
|
|
join(__dirname, 'ppr', 'app', 'not-found.js')
|
|
),
|
|
},
|
|
nextConfig: {
|
|
cacheHandler: './cache-handler.js',
|
|
cacheComponents: true,
|
|
output: 'standalone',
|
|
},
|
|
dependencies: {
|
|
'is-even': '1.0.0',
|
|
},
|
|
})
|
|
|
|
beforeAll(async () => {
|
|
// Stop the server, we're going to restart it using the standalone server
|
|
// below after some cleanup.
|
|
await next.stop()
|
|
|
|
// Read the postponed state and the HTML that was generated at build time
|
|
// from the output of the build.
|
|
delayedPostpone = (await next.readJSON('.next/server/app/delayed.meta'))
|
|
.postponed
|
|
rewritePostpone = (
|
|
await next.readJSON('.next/server/app/rewrite/first-cookie.meta')
|
|
).postponed
|
|
secondCookiePostpone = (
|
|
await next.readJSON('.next/server/app/rewrite/second-cookie.meta')
|
|
).postponed
|
|
secondCookieHTML = await next.readFile(
|
|
'.next/server/app/rewrite/second-cookie.html'
|
|
)
|
|
|
|
await fs.rename(
|
|
join(next.testDir, '.next/standalone'),
|
|
join(next.testDir, 'standalone')
|
|
)
|
|
|
|
const serverFilePath = join(next.testDir, 'standalone/server.js')
|
|
|
|
// We're going to use the minimal mode for the server.
|
|
await fs.writeFile(
|
|
serverFilePath,
|
|
(await fs.readFile(serverFilePath, 'utf8')).replace(
|
|
'port:',
|
|
`minimalMode: true, port:`
|
|
)
|
|
)
|
|
|
|
// Find a port to use for the server.
|
|
appPort = await findPort()
|
|
|
|
// Then we can start the server with the new environment variables.
|
|
server = await initNextServerScript(
|
|
serverFilePath,
|
|
/- Local:/,
|
|
{
|
|
...process.env,
|
|
...next.env,
|
|
__NEXT_TEST_MODE: 'e2e',
|
|
PORT: `${appPort}`,
|
|
NEXT_PRIVATE_DEBUG_CACHE: '1',
|
|
},
|
|
undefined,
|
|
{
|
|
cwd: next.testDir,
|
|
onStderr(data) {
|
|
cliOutput += data
|
|
},
|
|
onStdout(data) {
|
|
cliOutput += data
|
|
},
|
|
}
|
|
)
|
|
})
|
|
|
|
afterAll(async () => {
|
|
delete process.env.NOW_BUILDER
|
|
delete process.env.NEXT_PRIVATE_TEST_HEADERS
|
|
delete process.env.NEXT_PRIVATE_DEBUG_CACHE_ENTRY_HANDLERS
|
|
if (server) await killApp(server)
|
|
})
|
|
|
|
it('should not fail caching', async () => {
|
|
expect(next.cliOutput).not.toContain('ERR_INVALID_URL')
|
|
})
|
|
|
|
it('should de-dupe client segment tree revalidate requests', async () => {
|
|
const { segmentPaths } = await next.readJSON(
|
|
'standalone/.next/server/app/isr/first.meta'
|
|
)
|
|
const outputIdx = cliOutput.length
|
|
|
|
for (const segmentPath of segmentPaths) {
|
|
const outputSegmentPath =
|
|
join('/isr/[slug].segments', segmentPath) + '.segment.rsc'
|
|
|
|
require('console').error('requesting', outputSegmentPath)
|
|
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
outputSegmentPath,
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/isr/[slug].segments/_tree.segment.rsc',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'first',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toBe('text/x-component')
|
|
|
|
// We expect that because we're performing a segment prefetch, we
|
|
// shouldn't even get to the cache entry handler.
|
|
expect(res.headers.has('x-nextjs-cache-entry-handler')).toBe(false)
|
|
}
|
|
|
|
expect(
|
|
cliOutput.substring(outputIdx).match(/rendering \/isr\/\[slug\]/g).length
|
|
).toBe(1)
|
|
})
|
|
|
|
it('should properly stream resume with Next-Resume', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/delayed',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/delayed',
|
|
'next-resume': '1',
|
|
},
|
|
method: 'POST',
|
|
body: delayedPostpone,
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
|
|
// We expect that because we're performing a resume, we should be a miss.
|
|
expect(res.headers.get('x-nextjs-cache-entry-handler')).toBe('MISS_2')
|
|
|
|
let chunks = []
|
|
|
|
for await (const chunk of res.body) {
|
|
chunks.push({
|
|
time: Date.now(),
|
|
chunk: chunk.toString(),
|
|
})
|
|
}
|
|
|
|
const firstSuspense = chunks.find((item) => item.chunk.includes('time'))
|
|
const secondSuspense = chunks.find((item) => item.chunk.includes('random'))
|
|
|
|
expect(secondSuspense.time - firstSuspense.time).toBeGreaterThanOrEqual(
|
|
2 * 1000
|
|
)
|
|
})
|
|
|
|
it('should properly handle prerender for bot request', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/isr/first',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'user-agent':
|
|
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.179 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
|
|
'x-matched-path': '/isr/first',
|
|
},
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
|
|
expect($('#page').text()).toBe('/isr/[slug]')
|
|
|
|
const rscRes = await fetchViaHTTP(
|
|
appPort,
|
|
'/isr/first.rsc',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'user-agent':
|
|
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.179 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
|
|
'x-matched-path': '/isr/first',
|
|
},
|
|
})
|
|
)
|
|
|
|
expect(rscRes.status).toBe(200)
|
|
})
|
|
|
|
it('should properly handle fallback for bot request', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/isr/[slug]',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'user-agent':
|
|
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.179 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'new',
|
|
}).toString(),
|
|
'x-matched-path': '/isr/[slug]',
|
|
},
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
|
|
expect($('#page').text()).toBe('/isr/[slug]')
|
|
|
|
const rscRes = await fetchViaHTTP(
|
|
appPort,
|
|
'/isr/[slug].rsc',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'user-agent':
|
|
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.179 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'new',
|
|
}).toString(),
|
|
'x-matched-path': '/isr/[slug]',
|
|
},
|
|
})
|
|
)
|
|
|
|
expect(rscRes.status).toBe(200)
|
|
})
|
|
|
|
it('should properly handle resume request that looks like a data request', async () => {
|
|
const metadata = await next.readJSON('.next/server/app/[...catchAll].meta')
|
|
const postponed = metadata.postponed
|
|
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
// The pathname here represents a route that doesn't actually exist, but
|
|
// we want to simulate a pages route Link that performs a prefetch to a
|
|
// route backed by PPR.
|
|
`/_next/data/${next.buildId}/index.json`,
|
|
undefined,
|
|
withInvocationId({
|
|
method: 'POST',
|
|
headers: {
|
|
'x-matched-path': '/[...catchAll]',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
catchAll: `_next/data/${next.buildId}/index.json`,
|
|
}).toString(),
|
|
'next-resume': '1',
|
|
},
|
|
body: postponed,
|
|
})
|
|
)
|
|
|
|
// Expect that the status code is 422, we asked for a /_next/data route and
|
|
// also indicated that we wanted to resume a PPR render (which is
|
|
// impossible).
|
|
expect(res.status).toBe(422)
|
|
|
|
// We expect that because we have a short-circuit for these unprocessable
|
|
// requests, we should not have a cache entry handler header because it
|
|
// should never get reached.
|
|
expect(res.headers.has('x-nextjs-cache-entry-handler')).toBe(false)
|
|
|
|
// Expect that the response body is empty.
|
|
const html = await res.text()
|
|
expect(html).toBeEmpty()
|
|
})
|
|
|
|
describe('middleware rewrite', () => {
|
|
it('should work with a dynamic path with Next-Resume', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/rewrite-with-cookie',
|
|
undefined,
|
|
withInvocationId({
|
|
method: 'POST',
|
|
headers: {
|
|
'x-matched-path': '/rewrite/first-cookie',
|
|
'next-resume': '1',
|
|
},
|
|
body: rewritePostpone,
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
|
|
// We expect that because we're performing a resume, we should be a miss.
|
|
expect(res.headers.get('x-nextjs-cache-entry-handler')).toBe('MISS_2')
|
|
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
|
|
expect($('#page').text()).toBe('/rewrite/[slug]')
|
|
expect($('#params').text()).toBe(JSON.stringify({ slug: 'first-cookie' }))
|
|
})
|
|
})
|
|
|
|
it('should still render when postponed is corrupted with Next-Resume', async () => {
|
|
const random = Math.random().toString(36).substring(2)
|
|
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/dyn/' + random,
|
|
undefined,
|
|
withInvocationId({
|
|
method: 'POST',
|
|
headers: {
|
|
'x-matched-path': '/dyn/[slug]',
|
|
'next-resume': '1',
|
|
},
|
|
// This is a corrupted postponed JSON payload.
|
|
body: '{',
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
|
|
// We expect that because we're performing a resume, we should be a miss.
|
|
expect(res.headers.get('x-nextjs-cache-entry-handler')).toBe('MISS_2')
|
|
|
|
const html = await res.text()
|
|
|
|
// Expect that the closing HTML tag is still present, indicating a
|
|
// successful render.
|
|
expect(html).toContain('</html>')
|
|
})
|
|
|
|
it('should send cache tags in minimal mode for ISR', async () => {
|
|
for (const [path, tags] of [
|
|
[
|
|
'/isr/first',
|
|
'_N_T_/layout,_N_T_/isr/layout,_N_T_/isr/[slug]/layout,_N_T_/isr/[slug]/page,_N_T_/isr/first,isr-page',
|
|
],
|
|
[
|
|
'/isr/second',
|
|
'_N_T_/layout,_N_T_/isr/layout,_N_T_/isr/[slug]/layout,_N_T_/isr/[slug]/page,_N_T_/isr/second,isr-page',
|
|
],
|
|
[
|
|
'/api/isr/first',
|
|
'_N_T_/layout,_N_T_/api/layout,_N_T_/api/isr/layout,_N_T_/api/isr/[slug]/layout,_N_T_/api/isr/[slug]/route,_N_T_/api/isr/first,isr-page',
|
|
],
|
|
[
|
|
'/api/isr/second',
|
|
'_N_T_/layout,_N_T_/api/layout,_N_T_/api/isr/layout,_N_T_/api/isr/[slug]/layout,_N_T_/api/isr/[slug]/route,_N_T_/api/isr/second,isr-page',
|
|
],
|
|
]) {
|
|
require('console').error('checking', { path, tags })
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
path,
|
|
undefined,
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('x-next-cache-tags')).toBe(tags)
|
|
|
|
if (!path.startsWith('/api')) {
|
|
// We expect that because these aren't ISR'ed, they should all be a
|
|
// hit!
|
|
expect(res.headers.get('x-nextjs-cache-entry-handler')).toBe('HIT_2')
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should not send cache tags in minimal mode for SSR', async () => {
|
|
for (const path of [
|
|
'/ssr/first',
|
|
'/ssr/second',
|
|
'/api/ssr/first',
|
|
'/api/ssr/second',
|
|
]) {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
path,
|
|
undefined,
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('x-next-cache-tags')).toBeFalsy()
|
|
|
|
if (!path.startsWith('/api')) {
|
|
// We expect that because these aren't SSR'ed, they should all be a
|
|
// miss.
|
|
expect(res.headers.get('x-nextjs-cache-entry-handler')).toBe('MISS_1')
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should not send invalid soft tags to cache handler', async () => {
|
|
for (const path of [
|
|
'/ssr/first',
|
|
'/ssr/second',
|
|
'/api/ssr/first',
|
|
'/api/ssr/second',
|
|
]) {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
path,
|
|
{ hello: 'world' },
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('x-next-cache-tags')).toBeFalsy()
|
|
|
|
if (!path.startsWith('/api')) {
|
|
// We expect that because these aren't SSR'ed, they should all be a
|
|
// miss.
|
|
expect(res.headers.get('x-nextjs-cache-entry-handler')).toBe('MISS_1')
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should handle RSC requests', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/dyn/first.rsc',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/dyn/[slug]',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'first',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toEqual('text/x-component')
|
|
expect(res.headers.has('x-nextjs-postponed')).toBeFalse()
|
|
})
|
|
|
|
it('should use the postponed state for the RSC requests', async () => {
|
|
// Let's parse the random number out of the HTML that was generated at build
|
|
// time. We want to use that value as it's the one that's tied to the
|
|
// postponed state that we also have.
|
|
const $ = cheerio.load(secondCookieHTML)
|
|
|
|
const random = $('#random').text()
|
|
expect(random).toBeDefined()
|
|
expect(random.length).toBeGreaterThan(0)
|
|
|
|
// Record the start of the logs for this test.
|
|
let start = cliOutput.length
|
|
|
|
// Then let's do a Dynamic RSC request and verify that the random value is
|
|
// not present in the response without passing the postponed state.
|
|
let res = await fetchViaHTTP(
|
|
appPort,
|
|
'/rewrite/second-cookie.rsc',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/rewrite/[slug]',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'second-cookie',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toEqual('text/x-component')
|
|
expect(res.headers.has('x-nextjs-postponed')).toBeFalse()
|
|
|
|
// We expect that because we requested this dynamic RSC route without
|
|
// resuming it, it should be a hit because it should be producing a new
|
|
// static render.
|
|
expect(res.headers.get('x-nextjs-cache-entry-handler')).toBe('HIT_2')
|
|
|
|
// We expect that the random value is not present in the response because
|
|
// we're not providing a resume data cache via the postponed state.
|
|
// Instead it'll contain another random number that's been generated at
|
|
// runtime during this new static render.
|
|
let rsc = await res.text()
|
|
expect(rsc).not.toContain(random)
|
|
|
|
// Ensure that we hit the cache handler and not the resume data cache.
|
|
await retry(() => {
|
|
expect(cliOutput.substring(start)).toContain('cache-handler get')
|
|
expect(cliOutput.substring(start)).toContain('cache-handler set')
|
|
|
|
// We expect that there is both a miss and a hit because we're producing a
|
|
// new static render.
|
|
expect(cliOutput.substring(start)).toContain('rdc:miss')
|
|
expect(cliOutput.substring(start)).toContain('rdc:set')
|
|
})
|
|
|
|
// Reset the start of the logs for this test.
|
|
start = cliOutput.length
|
|
|
|
// Then let's get the Dynamic RSC request and verify that the random value
|
|
// is present in the response by passing the postponed state.
|
|
res = await fetchViaHTTP(
|
|
appPort,
|
|
'/rewrite/second-cookie.rsc',
|
|
undefined,
|
|
withInvocationId({
|
|
method: 'POST',
|
|
headers: {
|
|
'x-matched-path': '/rewrite/[slug]',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'second-cookie',
|
|
}).toString(),
|
|
'next-resume': '1',
|
|
},
|
|
body: secondCookiePostpone,
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toEqual('text/x-component')
|
|
expect(res.headers.has('x-nextjs-postponed')).toBeFalse()
|
|
|
|
// We expect that because we're resuming, it should be a miss, because it
|
|
// won't be producing a static response, and instead will be performing a
|
|
// dynamic render.
|
|
expect(res.headers.get('x-nextjs-cache-entry-handler')).toBe('MISS_2')
|
|
|
|
// We expect that the random value is present in the response because
|
|
// we're providing a resume data cache via the postponed state.
|
|
rsc = await res.text()
|
|
expect(rsc).toContain(random)
|
|
|
|
// Ensure that we hit the resume data cache and not the cache handler.
|
|
await retry(() => {
|
|
expect(cliOutput.substring(start)).not.toContain('cache-handler get')
|
|
expect(cliOutput.substring(start)).not.toContain('cache-handler set')
|
|
|
|
// We expect that there is a resume data cache hit because we're providing
|
|
// a resume data cache via the postponed state.
|
|
expect(cliOutput.substring(start)).toContain('rdc:hit')
|
|
|
|
// We expect that there is no resume data cache miss because we're
|
|
// providing a resume data cache via the postponed state.
|
|
expect(cliOutput.substring(start)).not.toContain('rdc:miss')
|
|
expect(cliOutput.substring(start)).not.toContain('rdc:no-resume-data')
|
|
})
|
|
})
|
|
|
|
it('should handle revalidating the fallback page', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/postpone/isr/[slug]',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/postpone/isr/[slug]',
|
|
// We don't include the `x-now-route-matches` header because we want to
|
|
// test that the fallback route params are correctly set.
|
|
'x-now-route-matches': '',
|
|
},
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(200)
|
|
|
|
// We expect that the cache entry handler was hit because we're performing a
|
|
// static render in minimal mode on a page that will suspend.
|
|
expect(res.headers.get('x-nextjs-cache-entry-handler')).toBe('HIT_2')
|
|
|
|
const html = await res.text()
|
|
|
|
// We only expect a partial shell because the page will suspend.
|
|
expect(html).not.toContain('</html>')
|
|
|
|
const $ = cheerio.load(html)
|
|
|
|
expect($('#page').text()).toBeEmpty()
|
|
expect($('#params').text()).toBeEmpty()
|
|
expect($('#now').text()).toBeEmpty()
|
|
expect($('#slot-loading').text()).toBe('/[...catchAll]')
|
|
expect($('#loading').text()).toBe('/postpone/isr/[slug]')
|
|
})
|
|
})
|