9697bcd3c8
This ensures we properly set/restore the status code with ISR paths in app router so that when we set the 404 status code with `notFound` it is persisted properly. Fixes: https://github.com/vercel/next.js/issues/43831 Closes: https://github.com/vercel/next.js/issues/48342 x-ref: https://github.com/vercel/next.js/issues/49387#issuecomment-1722575969
2900 lines
104 KiB
TypeScript
2900 lines
104 KiB
TypeScript
import globOrig from 'glob'
|
||
import cheerio from 'cheerio'
|
||
import { promisify } from 'util'
|
||
import { join } from 'path'
|
||
import { createNextDescribe } from 'e2e-utils'
|
||
import { check, fetchViaHTTP, normalizeRegEx, waitFor } from 'next-test-utils'
|
||
import stripAnsi from 'strip-ansi'
|
||
|
||
const glob = promisify(globOrig)
|
||
|
||
createNextDescribe(
|
||
'app-dir static/dynamic handling',
|
||
{
|
||
files: __dirname,
|
||
env: {
|
||
NEXT_DEBUG_BUILD: '1',
|
||
...(process.env.CUSTOM_CACHE_HANDLER
|
||
? {
|
||
CUSTOM_CACHE_HANDLER: process.env.CUSTOM_CACHE_HANDLER,
|
||
}
|
||
: {}),
|
||
},
|
||
},
|
||
({ next, isNextDev: isDev, isNextStart, isNextDeploy }) => {
|
||
let prerenderManifest
|
||
let buildCliOutputIndex = 0
|
||
|
||
beforeAll(async () => {
|
||
if (isNextStart) {
|
||
prerenderManifest = JSON.parse(
|
||
await next.readFile('.next/prerender-manifest.json')
|
||
)
|
||
buildCliOutputIndex = next.cliOutput.length
|
||
}
|
||
})
|
||
|
||
if (isNextStart) {
|
||
it('should propagate unstable_cache tags correctly', async () => {
|
||
const meta = JSON.parse(
|
||
await next.readFile(
|
||
'.next/server/app/variable-revalidate/revalidate-360-isr.meta'
|
||
)
|
||
)
|
||
expect(meta.headers['x-next-cache-tags']).toContain(
|
||
'unstable_cache_tag1'
|
||
)
|
||
})
|
||
}
|
||
|
||
it('should correctly include headers instance in cache key', async () => {
|
||
const res = await next.fetch('/variable-revalidate/headers-instance')
|
||
expect(res.status).toBe(200)
|
||
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const data1 = $('#page-data').text()
|
||
const data2 = $('#page-data2').text()
|
||
expect(data1).not.toBe(data2)
|
||
|
||
expect(data1).toBeTruthy()
|
||
expect(data2).toBeTruthy()
|
||
})
|
||
|
||
it.skip.each([
|
||
{
|
||
path: '/react-fetch-deduping-node',
|
||
},
|
||
{
|
||
path: '/react-fetch-deduping-edge',
|
||
},
|
||
])(
|
||
'should correctly de-dupe fetch without next cache $path',
|
||
async ({ path }) => {
|
||
for (let i = 0; i < 5; i++) {
|
||
const res = await next.fetch(path, {
|
||
redirect: 'manual',
|
||
})
|
||
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const data1 = $('#data-1').text()
|
||
const data2 = $('#data-2').text()
|
||
|
||
expect(data1).toBeTruthy()
|
||
expect(data1).toBe(data2)
|
||
|
||
await waitFor(250)
|
||
}
|
||
}
|
||
)
|
||
|
||
it.each([
|
||
{ pathname: '/unstable-cache-node' },
|
||
{ pathname: '/unstable-cache-edge' },
|
||
{ pathname: '/api/unstable-cache-node' },
|
||
{ pathname: '/api/unstable-cache-edge' },
|
||
])('unstable-cache should work in pages$pathname', async ({ pathname }) => {
|
||
let res = await next.fetch(pathname)
|
||
expect(res.status).toBe(200)
|
||
const isApi = pathname.startsWith('/api')
|
||
let prevData
|
||
|
||
if (isApi) {
|
||
prevData = await res.json()
|
||
} else {
|
||
const initialHtml = await res.text()
|
||
const initial$ = isApi ? undefined : cheerio.load(initialHtml)
|
||
prevData = JSON.parse(initial$('#props').text())
|
||
}
|
||
|
||
expect(prevData.data.random).toBeTruthy()
|
||
|
||
await check(async () => {
|
||
res = await next.fetch(pathname)
|
||
expect(res.status).toBe(200)
|
||
let curData
|
||
|
||
if (isApi) {
|
||
curData = await res.json()
|
||
} else {
|
||
const curHtml = await res.text()
|
||
const cur$ = cheerio.load(curHtml)
|
||
curData = JSON.parse(cur$('#props').text())
|
||
}
|
||
|
||
try {
|
||
expect(curData.data.random).toBeTruthy()
|
||
expect(curData.data.random).toBe(prevData.data.random)
|
||
} finally {
|
||
prevData = curData
|
||
}
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should not have cache tags header for non-minimal mode', async () => {
|
||
for (const path of [
|
||
'/ssr-forced',
|
||
'/ssr-forced',
|
||
'/variable-revalidate/revalidate-3',
|
||
'/variable-revalidate/revalidate-360',
|
||
'/variable-revalidate/revalidate-360-isr',
|
||
]) {
|
||
const res = await fetchViaHTTP(next.url, path, undefined, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(res.status).toBe(200)
|
||
expect(res.headers.get('x-next-cache-tags')).toBeFalsy()
|
||
}
|
||
})
|
||
|
||
if (isDev) {
|
||
it('should error correctly for invalid params from generateStaticParams', async () => {
|
||
await next.patchFile(
|
||
'app/invalid/[slug]/page.js',
|
||
`
|
||
export function generateStaticParams() {
|
||
return [{slug: { invalid: true }}]
|
||
}
|
||
`
|
||
)
|
||
|
||
// The page may take a moment to compile, so try it a few times.
|
||
await check(async () => {
|
||
return next.render('/invalid/first')
|
||
}, /A required parameter \(slug\) was not provided as a string received object/)
|
||
|
||
await next.deleteFile('app/invalid/[slug]/page.js')
|
||
})
|
||
|
||
it('should correctly handle multi-level generateStaticParams when some levels are missing', async () => {
|
||
const browser = await next.browser('/flight/foo/bar')
|
||
const v = ~~(Math.random() * 1000)
|
||
await browser.eval(`document.cookie = "test-cookie=${v}"`)
|
||
await browser.elementByCss('button').click()
|
||
await check(async () => {
|
||
return await browser.elementByCss('h1').text()
|
||
}, v.toString())
|
||
})
|
||
}
|
||
|
||
it('should correctly skip caching POST fetch for POST handler', async () => {
|
||
const res = await next.fetch('/route-handler/post', {
|
||
method: 'POST',
|
||
})
|
||
expect(res.status).toBe(200)
|
||
|
||
const data = await res.json()
|
||
expect(data).toBeTruthy()
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
const res2 = await next.fetch('/route-handler/post', {
|
||
method: 'POST',
|
||
})
|
||
expect(res2.status).toBe(200)
|
||
const newData = await res2.json()
|
||
expect(newData).toBeTruthy()
|
||
expect(newData).not.toEqual(data)
|
||
}
|
||
})
|
||
|
||
if (!process.env.CUSTOM_CACHE_HANDLER) {
|
||
it.each([
|
||
{
|
||
type: 'edge route handler',
|
||
revalidateApi: '/api/revalidate-tag-edge',
|
||
},
|
||
{
|
||
type: 'node route handler',
|
||
revalidateApi: '/api/revalidate-tag-node',
|
||
},
|
||
])(
|
||
'it should revalidate tag correctly with $type',
|
||
async ({ revalidateApi }) => {
|
||
const initRes = await next.fetch(
|
||
'/variable-revalidate/revalidate-360'
|
||
)
|
||
const html = await initRes.text()
|
||
const $ = cheerio.load(html)
|
||
const initLayoutData = $('#layout-data').text()
|
||
const initPageData = $('#page-data').text()
|
||
|
||
const routeHandlerRes = await next.fetch(
|
||
'/route-handler/revalidate-360'
|
||
)
|
||
const initRouteHandlerData = await routeHandlerRes.json()
|
||
|
||
const edgeRouteHandlerRes = await next.fetch(
|
||
'/route-handler-edge/revalidate-360'
|
||
)
|
||
const initEdgeRouteHandlerRes = await edgeRouteHandlerRes.json()
|
||
|
||
expect(initLayoutData).toBeTruthy()
|
||
expect(initPageData).toBeTruthy()
|
||
|
||
await check(async () => {
|
||
const revalidateRes = await next.fetch(
|
||
`${revalidateApi}?tag=thankyounext`
|
||
)
|
||
expect((await revalidateRes.json()).revalidated).toBe(true)
|
||
|
||
const newRes = await next.fetch(
|
||
'/variable-revalidate/revalidate-360'
|
||
)
|
||
const cacheHeader = newRes.headers.get('x-nextjs-cache')
|
||
|
||
if ((global as any).isNextStart && cacheHeader) {
|
||
expect(cacheHeader).toBe('MISS')
|
||
}
|
||
const newHtml = await newRes.text()
|
||
const new$ = cheerio.load(newHtml)
|
||
const newLayoutData = new$('#layout-data').text()
|
||
const newPageData = new$('#page-data').text()
|
||
|
||
const newRouteHandlerRes = await next.fetch(
|
||
'/route-handler/revalidate-360'
|
||
)
|
||
const newRouteHandlerData = await newRouteHandlerRes.json()
|
||
|
||
const newEdgeRouteHandlerRes = await next.fetch(
|
||
'/route-handler-edge/revalidate-360'
|
||
)
|
||
const newEdgeRouteHandlerData = await newEdgeRouteHandlerRes.json()
|
||
|
||
expect(newLayoutData).toBeTruthy()
|
||
expect(newPageData).toBeTruthy()
|
||
expect(newRouteHandlerData).toBeTruthy()
|
||
expect(newEdgeRouteHandlerData).toBeTruthy()
|
||
expect(newLayoutData).not.toBe(initLayoutData)
|
||
expect(newPageData).not.toBe(initPageData)
|
||
expect(newRouteHandlerData).not.toEqual(initRouteHandlerData)
|
||
expect(newEdgeRouteHandlerData).not.toEqual(initEdgeRouteHandlerRes)
|
||
return 'success'
|
||
}, 'success')
|
||
}
|
||
)
|
||
}
|
||
|
||
// On-Demand Revalidate has not effect in dev since app routes
|
||
// aren't considered static until prerendering
|
||
if (!(global as any).isNextDev && !process.env.CUSTOM_CACHE_HANDLER) {
|
||
it('should not revalidate / when revalidate is not used', async () => {
|
||
let prevData
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
const res = await next.fetch('/')
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
const data = $('#page-data').text()
|
||
|
||
expect(res.status).toBe(200)
|
||
|
||
if (prevData) {
|
||
expect(prevData).toBe(data)
|
||
prevData = data
|
||
}
|
||
await waitFor(500)
|
||
}
|
||
|
||
if (isNextStart) {
|
||
expect(next.cliOutput.substring(buildCliOutputIndex)).not.toContain(
|
||
'rendering index'
|
||
)
|
||
}
|
||
})
|
||
|
||
it.each([
|
||
{
|
||
type: 'edge route handler',
|
||
revalidateApi: '/api/revalidate-path-edge',
|
||
},
|
||
{
|
||
type: 'node route handler',
|
||
revalidateApi: '/api/revalidate-path-node',
|
||
},
|
||
])(
|
||
'it should revalidate correctly with $type',
|
||
async ({ revalidateApi }) => {
|
||
const initRes = await next.fetch(
|
||
'/variable-revalidate/revalidate-360-isr'
|
||
)
|
||
const html = await initRes.text()
|
||
const $ = cheerio.load(html)
|
||
const initLayoutData = $('#layout-data').text()
|
||
const initPageData = $('#page-data').text()
|
||
|
||
expect(initLayoutData).toBeTruthy()
|
||
expect(initPageData).toBeTruthy()
|
||
|
||
await check(async () => {
|
||
const revalidateRes = await next.fetch(
|
||
`${revalidateApi}?path=/variable-revalidate/revalidate-360-isr`
|
||
)
|
||
expect((await revalidateRes.json()).revalidated).toBe(true)
|
||
|
||
const newRes = await next.fetch(
|
||
'/variable-revalidate/revalidate-360-isr'
|
||
)
|
||
const newHtml = await newRes.text()
|
||
const new$ = cheerio.load(newHtml)
|
||
const newLayoutData = new$('#layout-data').text()
|
||
const newPageData = new$('#page-data').text()
|
||
|
||
expect(newLayoutData).toBeTruthy()
|
||
expect(newPageData).toBeTruthy()
|
||
expect(newLayoutData).not.toBe(initLayoutData)
|
||
expect(newPageData).not.toBe(initPageData)
|
||
return 'success'
|
||
}, 'success')
|
||
}
|
||
)
|
||
}
|
||
|
||
// On-Demand Revalidate has not effect in dev
|
||
if (!(global as any).isNextDev && !process.env.CUSTOM_CACHE_HANDLER) {
|
||
it('should revalidate all fetches during on-demand revalidate', async () => {
|
||
const initRes = await next.fetch(
|
||
'/variable-revalidate/revalidate-360-isr'
|
||
)
|
||
const html = await initRes.text()
|
||
const $ = cheerio.load(html)
|
||
const initLayoutData = $('#layout-data').text()
|
||
const initPageData = $('#page-data').text()
|
||
|
||
expect(initLayoutData).toBeTruthy()
|
||
expect(initPageData).toBeTruthy()
|
||
|
||
await check(async () => {
|
||
const revalidateRes = await next.fetch(
|
||
'/api/revalidate-path-node?path=/variable-revalidate/revalidate-360-isr'
|
||
)
|
||
expect((await revalidateRes.json()).revalidated).toBe(true)
|
||
|
||
const newRes = await next.fetch(
|
||
'/variable-revalidate/revalidate-360-isr'
|
||
)
|
||
const newHtml = await newRes.text()
|
||
const new$ = cheerio.load(newHtml)
|
||
const newLayoutData = new$('#layout-data').text()
|
||
const newPageData = new$('#page-data').text()
|
||
|
||
expect(newLayoutData).toBeTruthy()
|
||
expect(newPageData).toBeTruthy()
|
||
expect(newLayoutData).not.toBe(initLayoutData)
|
||
expect(newPageData).not.toBe(initPageData)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
}
|
||
|
||
it('should correctly handle fetchCache = "force-no-store"', async () => {
|
||
const initRes = await next.fetch('/force-no-store')
|
||
const html = await initRes.text()
|
||
const $ = cheerio.load(html)
|
||
const initPageData = $('#page-data').text()
|
||
expect(initPageData).toBeTruthy()
|
||
|
||
const newRes = await next.fetch('/force-no-store')
|
||
const newHtml = await newRes.text()
|
||
const new$ = cheerio.load(newHtml)
|
||
const newPageData = new$('#page-data').text()
|
||
|
||
expect(newPageData).toBeTruthy()
|
||
expect(newPageData).not.toBe(initPageData)
|
||
})
|
||
|
||
if (!process.env.CUSTOM_CACHE_HANDLER) {
|
||
it('should revalidate correctly with config and fetch revalidate', async () => {
|
||
const initial$ = await next.render$(
|
||
'/variable-config-revalidate/revalidate-3'
|
||
)
|
||
const initialDate = initial$('#date').text()
|
||
const initialRandomData = initial$('#random-data').text()
|
||
|
||
expect(initialDate).toBeTruthy()
|
||
expect(initialRandomData).toBeTruthy()
|
||
|
||
let prevInitialDate
|
||
let prevInitialRandomData
|
||
|
||
// wait for a fresh revalidation
|
||
await check(async () => {
|
||
const $ = await next.render$(
|
||
'/variable-config-revalidate/revalidate-3'
|
||
)
|
||
prevInitialDate = $('#date').text()
|
||
prevInitialRandomData = $('#random-data').text()
|
||
|
||
expect(prevInitialDate).not.toBe(initialDate)
|
||
expect(prevInitialRandomData).not.toBe(initialRandomData)
|
||
return 'success'
|
||
}, 'success')
|
||
|
||
// the date should revalidate first after 3 seconds
|
||
// while the fetch data stays in place for 9 seconds
|
||
await check(async () => {
|
||
const $ = await next.render$(
|
||
'/variable-config-revalidate/revalidate-3'
|
||
)
|
||
const curDate = $('#date').text()
|
||
const curRandomData = $('#random-data').text()
|
||
|
||
expect(curDate).not.toBe(prevInitialDate)
|
||
expect(curRandomData).not.toBe(prevInitialRandomData)
|
||
|
||
prevInitialDate = curDate
|
||
prevInitialRandomData = curRandomData
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
}
|
||
|
||
it('should not cache non-ok statusCode', async () => {
|
||
await check(async () => {
|
||
const $ = await next.render$('/variable-revalidate/status-code')
|
||
const origData = JSON.parse($('#page-data').text())
|
||
|
||
expect(origData.status).toBe(404)
|
||
|
||
const new$ = await next.render$('/variable-revalidate/status-code')
|
||
const newData = JSON.parse(new$('#page-data').text())
|
||
expect(newData.status).toBe(origData.status)
|
||
expect(newData.text).not.toBe(origData.text)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
if (isNextStart) {
|
||
it('should output HTML/RSC files for static paths', async () => {
|
||
const files = (
|
||
await glob('**/*', {
|
||
cwd: join(next.testDir, '.next/server/app'),
|
||
})
|
||
)
|
||
.filter((file) => file.match(/.*\.(js|html|rsc)$/))
|
||
.map((file) => {
|
||
return file.replace(
|
||
/partial-gen-params-no-additional-([\w]{1,})\/([\w]{1,})\/([\d]{1,})/,
|
||
'partial-gen-params-no-additional-$1/$2/RAND'
|
||
)
|
||
})
|
||
|
||
expect(files.sort()).toEqual(
|
||
[
|
||
'_not-found.html',
|
||
'_not-found.js',
|
||
'_not-found.rsc',
|
||
'_not-found_client-reference-manifest.js',
|
||
'page.js',
|
||
'index.rsc',
|
||
'index.html',
|
||
'blog/seb.rsc',
|
||
'blog/tim.rsc',
|
||
'blog/seb.html',
|
||
'blog/tim.html',
|
||
'blog/styfle.rsc',
|
||
'force-cache.rsc',
|
||
'blog/styfle.html',
|
||
'force-cache.html',
|
||
'ssg-draft-mode.rsc',
|
||
'ssr-forced/page.js',
|
||
'custom.prefetch.rsc',
|
||
'force-cache/page.js',
|
||
'ssg-draft-mode.html',
|
||
'(new)/custom/page.js',
|
||
'force-static/page.js',
|
||
'response-url/page.js',
|
||
'blog/[author]/page.js',
|
||
'default-cache/page.js',
|
||
'fetch-no-cache/page.js',
|
||
'force-no-store/page.js',
|
||
'force-static/first.rsc',
|
||
'api/draft-mode/route.js',
|
||
'blog/tim/first-post.rsc',
|
||
'force-static/first.html',
|
||
'force-static/second.rsc',
|
||
'ssg-draft-mode/test.rsc',
|
||
'ssr-forced.prefetch.rsc',
|
||
'blog/seb/second-post.rsc',
|
||
'blog/tim/first-post.html',
|
||
'force-static/second.html',
|
||
'ssg-draft-mode/test.html',
|
||
'blog/seb/second-post.html',
|
||
'force-static.prefetch.rsc',
|
||
'ssg-draft-mode/test-2.rsc',
|
||
'blog/styfle/first-post.rsc',
|
||
'default-cache.prefetch.rsc',
|
||
'dynamic-error/[id]/page.js',
|
||
'response-url.prefetch.rsc',
|
||
'ssg-draft-mode/test-2.html',
|
||
'blog/styfle/first-post.html',
|
||
'blog/styfle/second-post.rsc',
|
||
'fetch-no-cache.prefetch.rsc',
|
||
'force-no-store.prefetch.rsc',
|
||
'force-static/[slug]/page.js',
|
||
'hooks/use-pathname/slug.rsc',
|
||
'hooks/use-search-params.rsc',
|
||
'route-handler/post/route.js',
|
||
'blog/[author]/[slug]/page.js',
|
||
'blog/styfle/second-post.html',
|
||
'hooks/use-pathname/slug.html',
|
||
'hooks/use-search-params.html',
|
||
'flight/[slug]/[slug2]/page.js',
|
||
'variable-revalidate/cookie.rsc',
|
||
'hooks/use-search-params/page.js',
|
||
'ssr-auto/cache-no-store/page.js',
|
||
'variable-revalidate/cookie.html',
|
||
'api/revalidate-tag-edge/route.js',
|
||
'api/revalidate-tag-node/route.js',
|
||
'variable-revalidate/encoding.rsc',
|
||
'api/revalidate-path-edge/route.js',
|
||
'api/revalidate-path-node/route.js',
|
||
'gen-params-dynamic/[slug]/page.js',
|
||
'hooks/use-pathname/[slug]/page.js',
|
||
'page_client-reference-manifest.js',
|
||
'react-fetch-deduping-edge/page.js',
|
||
'react-fetch-deduping-node/page.js',
|
||
'variable-revalidate/encoding.html',
|
||
'variable-revalidate/cookie/page.js',
|
||
'gen-params-dynamic/one.prefetch.rsc',
|
||
'ssg-draft-mode/[[...route]]/page.js',
|
||
'variable-revalidate/post-method.rsc',
|
||
'dynamic-no-gen-params/[slug]/page.js',
|
||
'ssr-auto/cache-no-store.prefetch.rsc',
|
||
'static-to-dynamic-error/[id]/page.js',
|
||
'variable-revalidate/encoding/page.js',
|
||
'variable-revalidate/no-store/page.js',
|
||
'variable-revalidate/post-method.html',
|
||
'variable-revalidate/revalidate-3.rsc',
|
||
'gen-params-dynamic-revalidate/one.rsc',
|
||
'route-handler/revalidate-360/route.js',
|
||
'route-handler/static-cookies/route.js',
|
||
'variable-revalidate-edge/body/page.js',
|
||
'variable-revalidate/authorization.rsc',
|
||
'variable-revalidate/revalidate-3.html',
|
||
'force-dynamic-prerender/[slug]/page.js',
|
||
'gen-params-dynamic-revalidate/one.html',
|
||
'react-fetch-deduping-node.prefetch.rsc',
|
||
'ssr-auto/fetch-revalidate-zero/page.js',
|
||
'variable-revalidate/authorization.html',
|
||
'force-dynamic-no-prerender/[id]/page.js',
|
||
'variable-revalidate/post-method/page.js',
|
||
'variable-revalidate/status-code/page.js',
|
||
'dynamic-no-gen-params-ssr/[slug]/page.js',
|
||
'hooks/use-search-params/force-static.rsc',
|
||
'partial-gen-params/[lang]/[slug]/page.js',
|
||
'variable-revalidate/headers-instance.rsc',
|
||
'variable-revalidate/revalidate-3/page.js',
|
||
'force-dynamic-catch-all/slug.prefetch.rsc',
|
||
'hooks/use-search-params/force-static.html',
|
||
'hooks/use-search-params/with-suspense.rsc',
|
||
'route-handler/revalidate-360-isr/route.js',
|
||
'variable-revalidate-edge/encoding/page.js',
|
||
'variable-revalidate-edge/no-store/page.js',
|
||
'variable-revalidate/authorization/page.js',
|
||
'variable-revalidate/headers-instance.html',
|
||
'hooks/use-search-params/with-suspense.html',
|
||
'route-handler-edge/revalidate-360/route.js',
|
||
'variable-revalidate/no-store.prefetch.rsc',
|
||
'variable-revalidate/revalidate-360-isr.rsc',
|
||
'variable-revalidate/revalidate-360/page.js',
|
||
'ssr-auto/fetch-revalidate-zero.prefetch.rsc',
|
||
'static-to-dynamic-error-forced/[id]/page.js',
|
||
'variable-config-revalidate/revalidate-3.rsc',
|
||
'variable-revalidate/revalidate-360-isr.html',
|
||
'gen-params-dynamic-revalidate/[slug]/page.js',
|
||
'hooks/use-search-params/force-static/page.js',
|
||
'ssr-forced/page_client-reference-manifest.js',
|
||
'variable-config-revalidate/revalidate-3.html',
|
||
'variable-revalidate-edge/post-method/page.js',
|
||
'variable-revalidate/headers-instance/page.js',
|
||
'variable-revalidate/status-code.prefetch.rsc',
|
||
'force-cache/page_client-reference-manifest.js',
|
||
'hooks/use-search-params/with-suspense/page.js',
|
||
'variable-revalidate-edge/revalidate-3/page.js',
|
||
'(new)/custom/page_client-reference-manifest.js',
|
||
'force-static/page_client-reference-manifest.js',
|
||
'response-url/page_client-reference-manifest.js',
|
||
'variable-revalidate/revalidate-360-isr/page.js',
|
||
'blog/[author]/page_client-reference-manifest.js',
|
||
'default-cache/page_client-reference-manifest.js',
|
||
'force-dynamic-prerender/frameworks.prefetch.rsc',
|
||
'variable-config-revalidate/revalidate-3/page.js',
|
||
'variable-revalidate/post-method-request/page.js',
|
||
'variable-revalidate/revalidate-360.prefetch.rsc',
|
||
'fetch-no-cache/page_client-reference-manifest.js',
|
||
'force-dynamic-catch-all/[slug]/[[...id]]/page.js',
|
||
'force-no-store/page_client-reference-manifest.js',
|
||
'partial-gen-params-no-additional-lang/en/RAND.rsc',
|
||
'partial-gen-params-no-additional-lang/fr/RAND.rsc',
|
||
'partial-gen-params-no-additional-slug/en/RAND.rsc',
|
||
'partial-gen-params-no-additional-slug/fr/RAND.rsc',
|
||
'partial-gen-params-no-additional-lang/en/RAND.html',
|
||
'partial-gen-params-no-additional-lang/en/first.rsc',
|
||
'partial-gen-params-no-additional-lang/fr/RAND.html',
|
||
'partial-gen-params-no-additional-lang/fr/first.rsc',
|
||
'partial-gen-params-no-additional-slug/en/RAND.html',
|
||
'partial-gen-params-no-additional-slug/en/first.rsc',
|
||
'partial-gen-params-no-additional-slug/fr/RAND.html',
|
||
'partial-gen-params-no-additional-slug/fr/first.rsc',
|
||
'partial-gen-params-no-additional-lang/en/first.html',
|
||
'partial-gen-params-no-additional-lang/en/second.rsc',
|
||
'partial-gen-params-no-additional-lang/fr/first.html',
|
||
'partial-gen-params-no-additional-lang/fr/second.rsc',
|
||
'partial-gen-params-no-additional-slug/en/first.html',
|
||
'partial-gen-params-no-additional-slug/en/second.rsc',
|
||
'partial-gen-params-no-additional-slug/fr/first.html',
|
||
'partial-gen-params-no-additional-slug/fr/second.rsc',
|
||
'dynamic-error/[id]/page_client-reference-manifest.js',
|
||
'partial-gen-params-no-additional-lang/en/second.html',
|
||
'partial-gen-params-no-additional-lang/fr/second.html',
|
||
'partial-gen-params-no-additional-slug/en/second.html',
|
||
'partial-gen-params-no-additional-slug/fr/second.html',
|
||
'variable-revalidate/post-method-request.prefetch.rsc',
|
||
'variable-revalidate-edge/post-method-request/page.js',
|
||
'force-static/[slug]/page_client-reference-manifest.js',
|
||
'blog/[author]/[slug]/page_client-reference-manifest.js',
|
||
'flight/[slug]/[slug2]/page_client-reference-manifest.js',
|
||
'hooks/use-search-params/page_client-reference-manifest.js',
|
||
'ssr-auto/cache-no-store/page_client-reference-manifest.js',
|
||
'gen-params-dynamic/[slug]/page_client-reference-manifest.js',
|
||
'hooks/use-pathname/[slug]/page_client-reference-manifest.js',
|
||
'partial-gen-params-no-additional-lang/[lang]/[slug]/page.js',
|
||
'partial-gen-params-no-additional-slug/[lang]/[slug]/page.js',
|
||
'react-fetch-deduping-edge/page_client-reference-manifest.js',
|
||
'react-fetch-deduping-node/page_client-reference-manifest.js',
|
||
'variable-revalidate/cookie/page_client-reference-manifest.js',
|
||
'ssg-draft-mode/[[...route]]/page_client-reference-manifest.js',
|
||
'dynamic-no-gen-params/[slug]/page_client-reference-manifest.js',
|
||
'static-to-dynamic-error/[id]/page_client-reference-manifest.js',
|
||
'variable-revalidate/encoding/page_client-reference-manifest.js',
|
||
'variable-revalidate/no-store/page_client-reference-manifest.js',
|
||
'variable-revalidate-edge/body/page_client-reference-manifest.js',
|
||
'force-dynamic-prerender/[slug]/page_client-reference-manifest.js',
|
||
'ssr-auto/fetch-revalidate-zero/page_client-reference-manifest.js',
|
||
'force-dynamic-no-prerender/[id]/page_client-reference-manifest.js',
|
||
'variable-revalidate/post-method/page_client-reference-manifest.js',
|
||
'variable-revalidate/status-code/page_client-reference-manifest.js',
|
||
'dynamic-no-gen-params-ssr/[slug]/page_client-reference-manifest.js',
|
||
'partial-gen-params/[lang]/[slug]/page_client-reference-manifest.js',
|
||
'variable-revalidate/revalidate-3/page_client-reference-manifest.js',
|
||
'variable-revalidate-edge/encoding/page_client-reference-manifest.js',
|
||
'variable-revalidate-edge/no-store/page_client-reference-manifest.js',
|
||
'variable-revalidate/authorization/page_client-reference-manifest.js',
|
||
'variable-revalidate/revalidate-360/page_client-reference-manifest.js',
|
||
'static-to-dynamic-error-forced/[id]/page_client-reference-manifest.js',
|
||
'gen-params-dynamic-revalidate/[slug]/page_client-reference-manifest.js',
|
||
'hooks/use-search-params/force-static/page_client-reference-manifest.js',
|
||
'variable-revalidate-edge/post-method/page_client-reference-manifest.js',
|
||
'variable-revalidate/headers-instance/page_client-reference-manifest.js',
|
||
'hooks/use-search-params/with-suspense/page_client-reference-manifest.js',
|
||
'variable-revalidate-edge/revalidate-3/page_client-reference-manifest.js',
|
||
'variable-revalidate/revalidate-360-isr/page_client-reference-manifest.js',
|
||
'variable-config-revalidate/revalidate-3/page_client-reference-manifest.js',
|
||
'variable-revalidate/post-method-request/page_client-reference-manifest.js',
|
||
'force-dynamic-catch-all/[slug]/[[...id]]/page_client-reference-manifest.js',
|
||
'variable-revalidate-edge/post-method-request/page_client-reference-manifest.js',
|
||
'partial-gen-params-no-additional-lang/[lang]/[slug]/page_client-reference-manifest.js',
|
||
'partial-gen-params-no-additional-slug/[lang]/[slug]/page_client-reference-manifest.js',
|
||
'articles/[slug]/page.js',
|
||
'articles/[slug]/page_client-reference-manifest.js',
|
||
'articles/works.html',
|
||
'articles/works.rsc',
|
||
].sort()
|
||
)
|
||
})
|
||
|
||
it('should have correct prerender-manifest entries', async () => {
|
||
const curManifest = JSON.parse(JSON.stringify(prerenderManifest))
|
||
|
||
for (const key of Object.keys(curManifest.dynamicRoutes)) {
|
||
const item = curManifest.dynamicRoutes[key]
|
||
|
||
if (item.dataRouteRegex) {
|
||
item.dataRouteRegex = normalizeRegEx(item.dataRouteRegex)
|
||
}
|
||
if (item.routeRegex) {
|
||
item.routeRegex = normalizeRegEx(item.routeRegex)
|
||
}
|
||
}
|
||
|
||
for (const key of Object.keys(curManifest.routes)) {
|
||
const newKey = key.replace(
|
||
/partial-gen-params-no-additional-([\w]{1,})\/([\w]{1,})\/([\d]{1,})/,
|
||
'partial-gen-params-no-additional-$1/$2/RAND'
|
||
)
|
||
if (newKey !== key) {
|
||
const route = curManifest.routes[key]
|
||
delete curManifest.routes[key]
|
||
curManifest.routes[newKey] = {
|
||
...route,
|
||
dataRoute: `${newKey}.rsc`,
|
||
}
|
||
}
|
||
}
|
||
|
||
expect(curManifest.version).toBe(4)
|
||
expect(curManifest.routes).toMatchInlineSnapshot(`
|
||
Object {
|
||
"/": Object {
|
||
"dataRoute": "/index.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/",
|
||
},
|
||
"/articles/works": Object {
|
||
"dataRoute": "/articles/works.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 1,
|
||
"srcRoute": "/articles/[slug]",
|
||
},
|
||
"/blog/seb": Object {
|
||
"dataRoute": "/blog/seb.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 10,
|
||
"srcRoute": "/blog/[author]",
|
||
},
|
||
"/blog/seb/second-post": Object {
|
||
"dataRoute": "/blog/seb/second-post.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/blog/[author]/[slug]",
|
||
},
|
||
"/blog/styfle": Object {
|
||
"dataRoute": "/blog/styfle.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 10,
|
||
"srcRoute": "/blog/[author]",
|
||
},
|
||
"/blog/styfle/first-post": Object {
|
||
"dataRoute": "/blog/styfle/first-post.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/blog/[author]/[slug]",
|
||
},
|
||
"/blog/styfle/second-post": Object {
|
||
"dataRoute": "/blog/styfle/second-post.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/blog/[author]/[slug]",
|
||
},
|
||
"/blog/tim": Object {
|
||
"dataRoute": "/blog/tim.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 10,
|
||
"srcRoute": "/blog/[author]",
|
||
},
|
||
"/blog/tim/first-post": Object {
|
||
"dataRoute": "/blog/tim/first-post.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/blog/[author]/[slug]",
|
||
},
|
||
"/force-cache": Object {
|
||
"dataRoute": "/force-cache.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 3,
|
||
"srcRoute": "/force-cache",
|
||
},
|
||
"/force-static/first": Object {
|
||
"dataRoute": "/force-static/first.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/force-static/[slug]",
|
||
},
|
||
"/force-static/second": Object {
|
||
"dataRoute": "/force-static/second.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/force-static/[slug]",
|
||
},
|
||
"/gen-params-dynamic-revalidate/one": Object {
|
||
"dataRoute": "/gen-params-dynamic-revalidate/one.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 3,
|
||
"srcRoute": "/gen-params-dynamic-revalidate/[slug]",
|
||
},
|
||
"/hooks/use-pathname/slug": Object {
|
||
"dataRoute": "/hooks/use-pathname/slug.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/hooks/use-pathname/[slug]",
|
||
},
|
||
"/hooks/use-search-params": Object {
|
||
"dataRoute": "/hooks/use-search-params.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/hooks/use-search-params",
|
||
},
|
||
"/hooks/use-search-params/force-static": Object {
|
||
"dataRoute": "/hooks/use-search-params/force-static.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/hooks/use-search-params/force-static",
|
||
},
|
||
"/hooks/use-search-params/with-suspense": Object {
|
||
"dataRoute": "/hooks/use-search-params/with-suspense.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/hooks/use-search-params/with-suspense",
|
||
},
|
||
"/partial-gen-params-no-additional-lang/en/RAND": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-lang/en/RAND.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-lang/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-lang/en/first": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-lang/en/first.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-lang/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-lang/en/second": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-lang/en/second.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-lang/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-lang/fr/RAND": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-lang/fr/RAND.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-lang/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-lang/fr/first": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-lang/fr/first.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-lang/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-lang/fr/second": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-lang/fr/second.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-lang/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-slug/en/RAND": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-slug/en/RAND.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-slug/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-slug/en/first": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-slug/en/first.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-slug/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-slug/en/second": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-slug/en/second.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-slug/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-slug/fr/RAND": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-slug/fr/RAND.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-slug/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-slug/fr/first": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-slug/fr/first.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-slug/[lang]/[slug]",
|
||
},
|
||
"/partial-gen-params-no-additional-slug/fr/second": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-slug/fr/second.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/partial-gen-params-no-additional-slug/[lang]/[slug]",
|
||
},
|
||
"/route-handler/revalidate-360-isr": Object {
|
||
"dataRoute": null,
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialHeaders": Object {
|
||
"content-type": "application/json",
|
||
"x-next-cache-tags": "thankyounext,_N_T_/layout,_N_T_/route-handler/layout,_N_T_/route-handler/revalidate-360-isr/layout,_N_T_/route-handler/revalidate-360-isr/route,_N_T_/route-handler/revalidate-360-isr",
|
||
},
|
||
"initialRevalidateSeconds": 10,
|
||
"srcRoute": "/route-handler/revalidate-360-isr",
|
||
},
|
||
"/route-handler/static-cookies": Object {
|
||
"dataRoute": null,
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialHeaders": Object {
|
||
"set-cookie": "theme=light; Path=/,my_company=ACME; Path=/",
|
||
"x-next-cache-tags": "_N_T_/layout,_N_T_/route-handler/layout,_N_T_/route-handler/static-cookies/layout,_N_T_/route-handler/static-cookies/route,_N_T_/route-handler/static-cookies",
|
||
},
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/route-handler/static-cookies",
|
||
},
|
||
"/ssg-draft-mode": Object {
|
||
"dataRoute": "/ssg-draft-mode.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/ssg-draft-mode/[[...route]]",
|
||
},
|
||
"/ssg-draft-mode/test": Object {
|
||
"dataRoute": "/ssg-draft-mode/test.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/ssg-draft-mode/[[...route]]",
|
||
},
|
||
"/ssg-draft-mode/test-2": Object {
|
||
"dataRoute": "/ssg-draft-mode/test-2.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": false,
|
||
"srcRoute": "/ssg-draft-mode/[[...route]]",
|
||
},
|
||
"/variable-config-revalidate/revalidate-3": Object {
|
||
"dataRoute": "/variable-config-revalidate/revalidate-3.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 3,
|
||
"srcRoute": "/variable-config-revalidate/revalidate-3",
|
||
},
|
||
"/variable-revalidate/authorization": Object {
|
||
"dataRoute": "/variable-revalidate/authorization.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 10,
|
||
"srcRoute": "/variable-revalidate/authorization",
|
||
},
|
||
"/variable-revalidate/cookie": Object {
|
||
"dataRoute": "/variable-revalidate/cookie.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 3,
|
||
"srcRoute": "/variable-revalidate/cookie",
|
||
},
|
||
"/variable-revalidate/encoding": Object {
|
||
"dataRoute": "/variable-revalidate/encoding.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 3,
|
||
"srcRoute": "/variable-revalidate/encoding",
|
||
},
|
||
"/variable-revalidate/headers-instance": Object {
|
||
"dataRoute": "/variable-revalidate/headers-instance.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 10,
|
||
"srcRoute": "/variable-revalidate/headers-instance",
|
||
},
|
||
"/variable-revalidate/post-method": Object {
|
||
"dataRoute": "/variable-revalidate/post-method.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 10,
|
||
"srcRoute": "/variable-revalidate/post-method",
|
||
},
|
||
"/variable-revalidate/revalidate-3": Object {
|
||
"dataRoute": "/variable-revalidate/revalidate-3.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 3,
|
||
"srcRoute": "/variable-revalidate/revalidate-3",
|
||
},
|
||
"/variable-revalidate/revalidate-360-isr": Object {
|
||
"dataRoute": "/variable-revalidate/revalidate-360-isr.rsc",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"initialRevalidateSeconds": 10,
|
||
"srcRoute": "/variable-revalidate/revalidate-360-isr",
|
||
},
|
||
}
|
||
`)
|
||
expect(curManifest.dynamicRoutes).toMatchInlineSnapshot(`
|
||
Object {
|
||
"/articles/[slug]": Object {
|
||
"dataRoute": "/articles/[slug].rsc",
|
||
"dataRouteRegex": "^\\\\/articles\\\\/([^\\\\/]+?)\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": null,
|
||
"routeRegex": "^\\\\/articles\\\\/([^\\\\/]+?)(?:\\\\/)?$",
|
||
},
|
||
"/blog/[author]": Object {
|
||
"dataRoute": "/blog/[author].rsc",
|
||
"dataRouteRegex": "^\\\\/blog\\\\/([^\\\\/]+?)\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": false,
|
||
"routeRegex": "^\\\\/blog\\\\/([^\\\\/]+?)(?:\\\\/)?$",
|
||
},
|
||
"/blog/[author]/[slug]": Object {
|
||
"dataRoute": "/blog/[author]/[slug].rsc",
|
||
"dataRouteRegex": "^\\\\/blog\\\\/([^\\\\/]+?)\\\\/([^\\\\/]+?)\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": null,
|
||
"routeRegex": "^\\\\/blog\\\\/([^\\\\/]+?)\\\\/([^\\\\/]+?)(?:\\\\/)?$",
|
||
},
|
||
"/dynamic-error/[id]": Object {
|
||
"dataRoute": "/dynamic-error/[id].rsc",
|
||
"dataRouteRegex": "^\\\\/dynamic\\\\-error\\\\/([^\\\\/]+?)\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": null,
|
||
"routeRegex": "^\\\\/dynamic\\\\-error\\\\/([^\\\\/]+?)(?:\\\\/)?$",
|
||
},
|
||
"/force-static/[slug]": Object {
|
||
"dataRoute": "/force-static/[slug].rsc",
|
||
"dataRouteRegex": "^\\\\/force\\\\-static\\\\/([^\\\\/]+?)\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": null,
|
||
"routeRegex": "^\\\\/force\\\\-static\\\\/([^\\\\/]+?)(?:\\\\/)?$",
|
||
},
|
||
"/gen-params-dynamic-revalidate/[slug]": Object {
|
||
"dataRoute": "/gen-params-dynamic-revalidate/[slug].rsc",
|
||
"dataRouteRegex": "^\\\\/gen\\\\-params\\\\-dynamic\\\\-revalidate\\\\/([^\\\\/]+?)\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": null,
|
||
"routeRegex": "^\\\\/gen\\\\-params\\\\-dynamic\\\\-revalidate\\\\/([^\\\\/]+?)(?:\\\\/)?$",
|
||
},
|
||
"/hooks/use-pathname/[slug]": Object {
|
||
"dataRoute": "/hooks/use-pathname/[slug].rsc",
|
||
"dataRouteRegex": "^\\\\/hooks\\\\/use\\\\-pathname\\\\/([^\\\\/]+?)\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": null,
|
||
"routeRegex": "^\\\\/hooks\\\\/use\\\\-pathname\\\\/([^\\\\/]+?)(?:\\\\/)?$",
|
||
},
|
||
"/partial-gen-params-no-additional-lang/[lang]/[slug]": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-lang/[lang]/[slug].rsc",
|
||
"dataRouteRegex": "^\\\\/partial\\\\-gen\\\\-params\\\\-no\\\\-additional\\\\-lang\\\\/([^\\\\/]+?)\\\\/([^\\\\/]+?)\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": false,
|
||
"routeRegex": "^\\\\/partial\\\\-gen\\\\-params\\\\-no\\\\-additional\\\\-lang\\\\/([^\\\\/]+?)\\\\/([^\\\\/]+?)(?:\\\\/)?$",
|
||
},
|
||
"/partial-gen-params-no-additional-slug/[lang]/[slug]": Object {
|
||
"dataRoute": "/partial-gen-params-no-additional-slug/[lang]/[slug].rsc",
|
||
"dataRouteRegex": "^\\\\/partial\\\\-gen\\\\-params\\\\-no\\\\-additional\\\\-slug\\\\/([^\\\\/]+?)\\\\/([^\\\\/]+?)\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": false,
|
||
"routeRegex": "^\\\\/partial\\\\-gen\\\\-params\\\\-no\\\\-additional\\\\-slug\\\\/([^\\\\/]+?)\\\\/([^\\\\/]+?)(?:\\\\/)?$",
|
||
},
|
||
"/ssg-draft-mode/[[...route]]": Object {
|
||
"dataRoute": "/ssg-draft-mode/[[...route]].rsc",
|
||
"dataRouteRegex": "^\\\\/ssg\\\\-draft\\\\-mode(?:\\\\/(.+?))?\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": null,
|
||
"routeRegex": "^\\\\/ssg\\\\-draft\\\\-mode(?:\\\\/(.+?))?(?:\\\\/)?$",
|
||
},
|
||
"/static-to-dynamic-error-forced/[id]": Object {
|
||
"dataRoute": "/static-to-dynamic-error-forced/[id].rsc",
|
||
"dataRouteRegex": "^\\\\/static\\\\-to\\\\-dynamic\\\\-error\\\\-forced\\\\/([^\\\\/]+?)\\\\.rsc$",
|
||
"experimentalBypassFor": Array [
|
||
Object {
|
||
"key": "Next-Action",
|
||
"type": "header",
|
||
},
|
||
Object {
|
||
"key": "content-type",
|
||
"type": "header",
|
||
"value": "multipart/form-data",
|
||
},
|
||
],
|
||
"fallback": null,
|
||
"routeRegex": "^\\\\/static\\\\-to\\\\-dynamic\\\\-error\\\\-forced\\\\/([^\\\\/]+?)(?:\\\\/)?$",
|
||
},
|
||
}
|
||
`)
|
||
})
|
||
|
||
it('should output debug info for static bailouts', async () => {
|
||
const cleanedOutput = stripAnsi(next.cliOutput)
|
||
|
||
expect(cleanedOutput).toContain(
|
||
'Static generation failed due to dynamic usage on /force-static, reason: headers'
|
||
)
|
||
expect(cleanedOutput).toContain(
|
||
'Static generation failed due to dynamic usage on /ssr-auto/cache-no-store, reason: no-store fetch'
|
||
)
|
||
})
|
||
}
|
||
|
||
it('should correctly handle statusCode with notFound + ISR', async () => {
|
||
for (let i = 0; i < 5; i++) {
|
||
const res = await next.fetch('/articles/non-existent')
|
||
expect(res.status).toBe(404)
|
||
expect(await res.text()).toContain('This page could not be found')
|
||
await waitFor(500)
|
||
}
|
||
})
|
||
|
||
it('should cache correctly for fetchCache = default-cache', async () => {
|
||
const res = await next.fetch('/default-cache')
|
||
expect(res.status).toBe(200)
|
||
|
||
let prevHtml = await res.text()
|
||
let prev$ = cheerio.load(prevHtml)
|
||
|
||
await check(async () => {
|
||
const curRes = await next.fetch('/default-cache')
|
||
expect(curRes.status).toBe(200)
|
||
|
||
const curHtml = await curRes.text()
|
||
const cur$ = cheerio.load(curHtml)
|
||
|
||
try {
|
||
expect(cur$('#data-no-cache').text()).not.toBe(
|
||
prev$('#data-no-cache').text()
|
||
)
|
||
expect(cur$('#data-force-cache').text()).toBe(
|
||
prev$('#data-force-cache').text()
|
||
)
|
||
expect(cur$('#data-revalidate-cache').text()).toBe(
|
||
prev$('#data-revalidate-cache').text()
|
||
)
|
||
expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe(
|
||
prev$('#data-revalidate-and-fetch-cache').text()
|
||
)
|
||
expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe(
|
||
prev$('#data-revalidate-and-fetch-cache').text()
|
||
)
|
||
} finally {
|
||
prevHtml = curHtml
|
||
prev$ = cur$
|
||
}
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should cache correctly for fetchCache = force-cache', async () => {
|
||
const res = await next.fetch('/force-cache')
|
||
expect(res.status).toBe(200)
|
||
|
||
let prevHtml = await res.text()
|
||
let prev$ = cheerio.load(prevHtml)
|
||
|
||
await check(async () => {
|
||
const curRes = await next.fetch('/force-cache')
|
||
expect(curRes.status).toBe(200)
|
||
|
||
const curHtml = await curRes.text()
|
||
const cur$ = cheerio.load(curHtml)
|
||
|
||
expect(cur$('#data-no-cache').text()).toBe(
|
||
prev$('#data-no-cache').text()
|
||
)
|
||
expect(cur$('#data-force-cache').text()).toBe(
|
||
prev$('#data-force-cache').text()
|
||
)
|
||
expect(cur$('#data-revalidate-cache').text()).toBe(
|
||
prev$('#data-revalidate-cache').text()
|
||
)
|
||
expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe(
|
||
prev$('#data-revalidate-and-fetch-cache').text()
|
||
)
|
||
expect(cur$('#data-auto-cache').text()).toBe(
|
||
prev$('#data-auto-cache').text()
|
||
)
|
||
|
||
return 'success'
|
||
}, 'success')
|
||
|
||
if (!isNextDeploy) {
|
||
expect(next.cliOutput).toContain(
|
||
'fetch for https://next-data-api-endpoint.vercel.app/api/random?d4 on /force-cache specified "cache: force-cache" and "revalidate: 3", only one should be specified.'
|
||
)
|
||
}
|
||
})
|
||
|
||
it('should cache correctly for cache: no-store', async () => {
|
||
const res = await next.fetch('/fetch-no-cache')
|
||
expect(res.status).toBe(200)
|
||
|
||
let prevHtml = await res.text()
|
||
let prev$ = cheerio.load(prevHtml)
|
||
|
||
await check(async () => {
|
||
const curRes = await next.fetch('/fetch-no-cache')
|
||
expect(curRes.status).toBe(200)
|
||
|
||
const curHtml = await curRes.text()
|
||
const cur$ = cheerio.load(curHtml)
|
||
|
||
try {
|
||
expect(cur$('#data-no-cache').text()).not.toBe(
|
||
prev$('#data-no-cache').text()
|
||
)
|
||
expect(cur$('#data-force-cache').text()).toBe(
|
||
prev$('#data-force-cache').text()
|
||
)
|
||
expect(cur$('#data-revalidate-cache').text()).toBe(
|
||
prev$('#data-revalidate-cache').text()
|
||
)
|
||
expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe(
|
||
prev$('#data-revalidate-and-fetch-cache').text()
|
||
)
|
||
expect(cur$('#data-auto-cache').text()).not.toBe(
|
||
prev$('#data-auto-cache').text()
|
||
)
|
||
} finally {
|
||
prevHtml = curHtml
|
||
prev$ = cur$
|
||
}
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
if (isDev) {
|
||
it('should bypass fetch cache with cache-control: no-cache', async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/revalidate-3'
|
||
)
|
||
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/revalidate-3',
|
||
undefined,
|
||
{
|
||
headers: {
|
||
'cache-control': 'no-cache',
|
||
},
|
||
}
|
||
)
|
||
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
expect($2('#layout-data').text()).not.toBe(layoutData)
|
||
expect($2('#page-data').text()).not.toBe(pageData)
|
||
})
|
||
} else {
|
||
it('should not error with dynamic server usage with force-static', async () => {
|
||
const res = await next.fetch(
|
||
'/static-to-dynamic-error-forced/static-bailout-1'
|
||
)
|
||
const outputIndex = next.cliOutput.length
|
||
const html = await res.text()
|
||
|
||
expect(res.status).toBe(200)
|
||
expect(html).toContain('/static-to-dynamic-error-forced')
|
||
expect(html).toMatch(/id:.*?static-bailout-1/)
|
||
|
||
if (isNextStart) {
|
||
expect(stripAnsi(next.cliOutput).substring(outputIndex)).not.toMatch(
|
||
/Page changed from static to dynamic at runtime \/static-to-dynamic-error-forced\/static-bailout-1, reason: cookies/
|
||
)
|
||
}
|
||
})
|
||
|
||
it('should produce response with url from fetch', async () => {
|
||
const res = await next.fetch('/response-url')
|
||
expect(res.status).toBe(200)
|
||
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
expect($('#data-url-default-cache').text()).toBe(
|
||
'https://next-data-api-endpoint.vercel.app/api/random?a1'
|
||
)
|
||
expect($('#data-url-no-cache').text()).toBe(
|
||
'https://next-data-api-endpoint.vercel.app/api/random?b2'
|
||
)
|
||
expect($('#data-url-cached').text()).toBe(
|
||
'https://next-data-api-endpoint.vercel.app/api/random?a1'
|
||
)
|
||
expect($('#data-value-default-cache').text()).toBe(
|
||
$('#data-value-cached').text()
|
||
)
|
||
})
|
||
|
||
it('should properly error when dynamic = "error" page uses dynamic', async () => {
|
||
const res = await next.fetch('/dynamic-error/static-bailout-1')
|
||
const outputIndex = next.cliOutput.length
|
||
|
||
expect(res.status).toBe(500)
|
||
|
||
if (isNextStart) {
|
||
expect(stripAnsi(next.cliOutput).substring(outputIndex)).not.toMatch(
|
||
/Page with dynamic = "error" encountered dynamic data method on \/dynamic-error\/static-bailout-1/
|
||
)
|
||
}
|
||
})
|
||
}
|
||
|
||
it('should skip cache in draft mode', async () => {
|
||
const draftRes = await next.fetch('/api/draft-mode?status=enable')
|
||
const setCookie = draftRes.headers.get('set-cookie')
|
||
const cookieHeader = { Cookie: setCookie?.split(';')[0] }
|
||
|
||
expect(cookieHeader.Cookie).toBeTruthy()
|
||
|
||
const res = await next.fetch('/ssg-draft-mode/test-1', {
|
||
headers: cookieHeader,
|
||
})
|
||
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
const data1 = $('#data').text()
|
||
|
||
expect(data1).toBeTruthy()
|
||
expect(JSON.parse($('#draft-mode').text())).toEqual({ isEnabled: true })
|
||
|
||
const res2 = await next.fetch('/ssg-draft-mode/test-1', {
|
||
headers: cookieHeader,
|
||
})
|
||
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
const data2 = $2('#data').text()
|
||
|
||
expect(data2).toBeTruthy()
|
||
expect(data1).not.toBe(data2)
|
||
expect(JSON.parse($2('#draft-mode').text())).toEqual({ isEnabled: true })
|
||
})
|
||
|
||
it('should handle partial-gen-params with default dynamicParams correctly', async () => {
|
||
const res = await next.fetch('/partial-gen-params/en/first')
|
||
expect(res.status).toBe(200)
|
||
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
const params = JSON.parse($('#params').text())
|
||
|
||
expect(params).toEqual({ lang: 'en', slug: 'first' })
|
||
})
|
||
|
||
it('should handle partial-gen-params with layout dynamicParams = false correctly', async () => {
|
||
for (const { path, status, params } of [
|
||
// these checks don't work with custom memory only
|
||
// cache handler
|
||
...(process.env.CUSTOM_CACHE_HANDLER
|
||
? []
|
||
: [
|
||
{
|
||
path: '/partial-gen-params-no-additional-lang/en/first',
|
||
status: 200,
|
||
params: { lang: 'en', slug: 'first' },
|
||
},
|
||
]),
|
||
{
|
||
path: '/partial-gen-params-no-additional-lang/de/first',
|
||
status: 404,
|
||
params: {},
|
||
},
|
||
{
|
||
path: '/partial-gen-params-no-additional-lang/en/non-existent',
|
||
status: 404,
|
||
params: {},
|
||
},
|
||
]) {
|
||
const res = await next.fetch(path)
|
||
expect(res.status).toBe(status)
|
||
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
const curParams = JSON.parse($('#params').text() || '{}')
|
||
|
||
expect(curParams).toEqual(params)
|
||
}
|
||
})
|
||
|
||
it('should handle partial-gen-params with page dynamicParams = false correctly', async () => {
|
||
for (const { path, status, params } of [
|
||
// these checks don't work with custom memory only
|
||
// cache handler
|
||
...(process.env.CUSTOM_CACHE_HANDLER
|
||
? []
|
||
: [
|
||
{
|
||
path: '/partial-gen-params-no-additional-slug/en/first',
|
||
status: 200,
|
||
params: { lang: 'en', slug: 'first' },
|
||
},
|
||
]),
|
||
{
|
||
path: '/partial-gen-params-no-additional-slug/de/first',
|
||
status: 404,
|
||
params: {},
|
||
},
|
||
{
|
||
path: '/partial-gen-params-no-additional-slug/en/non-existent',
|
||
status: 404,
|
||
params: {},
|
||
},
|
||
]) {
|
||
const res = await next.fetch(path)
|
||
expect(res.status).toBe(status)
|
||
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
const curParams = JSON.parse($('#params').text() || '{}')
|
||
|
||
expect(curParams).toEqual(params)
|
||
}
|
||
})
|
||
|
||
// fetch cache in generateStaticParams needs fs for persistence
|
||
// so doesn't behave as expected with custom in memory only
|
||
// cache handler
|
||
if (!process.env.CUSTOM_CACHE_HANDLER) {
|
||
it('should honor fetch cache in generateStaticParams', async () => {
|
||
const initialRes = await next.fetch(
|
||
`/partial-gen-params-no-additional-lang/en/first`
|
||
)
|
||
|
||
expect(initialRes.status).toBe(200)
|
||
|
||
// we can't read prerender-manifest from deployment
|
||
if (isNextDeploy) return
|
||
|
||
let langFetchSlug
|
||
let slugFetchSlug
|
||
|
||
if (isDev) {
|
||
await check(() => {
|
||
const matches = stripAnsi(next.cliOutput).match(
|
||
/partial-gen-params fetch ([\d]{1,})/
|
||
)
|
||
|
||
if (matches[1]) {
|
||
langFetchSlug = matches[1]
|
||
slugFetchSlug = langFetchSlug
|
||
}
|
||
return langFetchSlug ? 'success' : next.cliOutput
|
||
}, 'success')
|
||
} else {
|
||
// the fetch cache can potentially be a miss since
|
||
// the generateStaticParams are executed parallel
|
||
// in separate workers so parse value from
|
||
// prerender-manifest
|
||
const routes = Object.keys(prerenderManifest.routes)
|
||
|
||
for (const route of routes) {
|
||
const langSlug = route.match(
|
||
/partial-gen-params-no-additional-lang\/en\/([\d]{1,})/
|
||
)?.[1]
|
||
|
||
if (langSlug) {
|
||
langFetchSlug = langSlug
|
||
}
|
||
|
||
const slugSlug = route.match(
|
||
/partial-gen-params-no-additional-slug\/en\/([\d]{1,})/
|
||
)?.[1]
|
||
|
||
if (slugSlug) {
|
||
slugFetchSlug = slugSlug
|
||
}
|
||
}
|
||
}
|
||
require('console').log({ langFetchSlug, slugFetchSlug })
|
||
|
||
for (const { pathname, slug } of [
|
||
{
|
||
pathname: '/partial-gen-params-no-additional-lang/en',
|
||
slug: langFetchSlug,
|
||
},
|
||
{
|
||
pathname: '/partial-gen-params-no-additional-slug/en',
|
||
slug: slugFetchSlug,
|
||
},
|
||
]) {
|
||
const res = await next.fetch(`${pathname}/${slug}`)
|
||
expect(res.status).toBe(200)
|
||
expect(
|
||
JSON.parse(
|
||
cheerio
|
||
.load(await res.text())('#params')
|
||
.text()
|
||
)
|
||
).toEqual({ lang: 'en', slug })
|
||
}
|
||
})
|
||
}
|
||
|
||
it('should honor fetch cache correctly', async () => {
|
||
await check(async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/revalidate-3'
|
||
)
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/revalidate-3'
|
||
)
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#layout-data').text()).toBe(layoutData)
|
||
expect($2('#page-data').text()).toBe(pageData)
|
||
return 'success'
|
||
}, 'success')
|
||
|
||
if (isNextStart) {
|
||
expect(next.cliOutput).toContain(
|
||
`Page "/variable-revalidate-edge/revalidate-3" is using runtime = 'edge' which is currently incompatible with dynamic = 'force-static'. Please remove either "runtime" or "force-static" for correct behavior`
|
||
)
|
||
}
|
||
})
|
||
|
||
it('should honor fetch cache correctly (edge)', async () => {
|
||
await check(async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate-edge/revalidate-3'
|
||
)
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
// the test cache handler is simple and doesn't share
|
||
// state across workers so not guaranteed to have cache hit
|
||
if (!(isNextDeploy && process.env.CUSTOM_CACHE_HANDLER)) {
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate-edge/revalidate-3'
|
||
)
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#layout-data').text()).toBe(layoutData)
|
||
expect($2('#page-data').text()).toBe(pageData)
|
||
}
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should cache correctly with authorization header and revalidate', async () => {
|
||
await check(async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/authorization'
|
||
)
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/authorization'
|
||
)
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#layout-data').text()).toBe(layoutData)
|
||
expect($2('#page-data').text()).toBe(pageData)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should not cache correctly with POST method request init', async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate-edge/post-method-request'
|
||
)
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const pageData2 = $('#page-data2').text()
|
||
|
||
for (let i = 0; i < 3; i++) {
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate-edge/post-method-request'
|
||
)
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#page-data2').text()).not.toBe(pageData2)
|
||
}
|
||
})
|
||
|
||
it('should cache correctly with post method and revalidate', async () => {
|
||
await check(async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/post-method'
|
||
)
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
const dataBody1 = $('#data-body1').text()
|
||
const dataBody2 = $('#data-body2').text()
|
||
const dataBody3 = $('#data-body3').text()
|
||
const dataBody4 = $('#data-body4').text()
|
||
|
||
expect(dataBody1).not.toBe(dataBody2)
|
||
expect(dataBody2).not.toBe(dataBody3)
|
||
expect(dataBody3).not.toBe(dataBody4)
|
||
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/post-method'
|
||
)
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#layout-data').text()).toBe(layoutData)
|
||
expect($2('#page-data').text()).toBe(pageData)
|
||
expect($2('#data-body1').text()).toBe(dataBody1)
|
||
expect($2('#data-body2').text()).toBe(dataBody2)
|
||
expect($2('#data-body3').text()).toBe(dataBody3)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should cache correctly with post method and revalidate edge', async () => {
|
||
await check(async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate-edge/post-method'
|
||
)
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
const dataBody1 = $('#data-body1').text()
|
||
const dataBody2 = $('#data-body2').text()
|
||
const dataBody3 = $('#data-body3').text()
|
||
const dataBody4 = $('#data-body4').text()
|
||
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate-edge/post-method'
|
||
)
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#layout-data').text()).toBe(layoutData)
|
||
expect($2('#page-data').text()).toBe(pageData)
|
||
expect($2('#data-body1').text()).toBe(dataBody1)
|
||
expect($2('#data-body2').text()).toBe(dataBody2)
|
||
expect($2('#data-body3').text()).toBe(dataBody3)
|
||
expect($2('#data-body4').text()).toBe(dataBody4)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should cache correctly with POST method and revalidate', async () => {
|
||
await check(async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/post-method'
|
||
)
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/post-method'
|
||
)
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#layout-data').text()).toBe(layoutData)
|
||
expect($2('#page-data').text()).toBe(pageData)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should cache correctly with cookie header and revalidate', async () => {
|
||
await check(async () => {
|
||
const res = await fetchViaHTTP(next.url, '/variable-revalidate/cookie')
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
|
||
const res2 = await fetchViaHTTP(next.url, '/variable-revalidate/cookie')
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#layout-data').text()).toBe(layoutData)
|
||
expect($2('#page-data').text()).toBe(pageData)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should cache correctly with utf8 encoding', async () => {
|
||
await check(async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/encoding'
|
||
)
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
|
||
expect(JSON.parse(pageData).jp).toBe(
|
||
'超鬼畜!激辛ボム兵スピンジャンプ Bomb Spin Jump'
|
||
)
|
||
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate/encoding'
|
||
)
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#layout-data').text()).toBe(layoutData)
|
||
expect($2('#page-data').text()).toBe(pageData)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should cache correctly with utf8 encoding edge', async () => {
|
||
await check(async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate-edge/encoding'
|
||
)
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
|
||
expect(JSON.parse(pageData).jp).toBe(
|
||
'超鬼畜!激辛ボム兵スピンジャンプ Bomb Spin Jump'
|
||
)
|
||
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate-edge/encoding'
|
||
)
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#layout-data').text()).toBe(layoutData)
|
||
expect($2('#page-data').text()).toBe(pageData)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should cache correctly handle JSON body', async () => {
|
||
await check(async () => {
|
||
const res = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate-edge/body'
|
||
)
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
const layoutData = $('#layout-data').text()
|
||
const pageData = $('#page-data').text()
|
||
|
||
expect(pageData).toBe('{"hello":"world"}')
|
||
|
||
const res2 = await fetchViaHTTP(
|
||
next.url,
|
||
'/variable-revalidate-edge/body'
|
||
)
|
||
expect(res2.status).toBe(200)
|
||
const html2 = await res2.text()
|
||
const $2 = cheerio.load(html2)
|
||
|
||
expect($2('#layout-data').text()).toBe(layoutData)
|
||
expect($2('#page-data').text()).toBe(pageData)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
it('should not throw Dynamic Server Usage error when using generateStaticParams with draftMode', async () => {
|
||
const browserOnIndexPage = await next.browser('/ssg-draft-mode')
|
||
|
||
const content = await browserOnIndexPage
|
||
.elementByCss('#draft-mode')
|
||
.text()
|
||
|
||
expect(content).toBe('{"isEnabled":false}')
|
||
})
|
||
|
||
it('should force SSR correctly for headers usage', async () => {
|
||
const res = await next.fetch('/force-static', {
|
||
headers: {
|
||
Cookie: 'myCookie=cookieValue',
|
||
another: 'header',
|
||
},
|
||
})
|
||
expect(res.status).toBe(200)
|
||
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
expect(JSON.parse($('#headers').text())).toIncludeAllMembers([
|
||
'cookie',
|
||
'another',
|
||
])
|
||
expect(JSON.parse($('#cookies').text())).toEqual([
|
||
{
|
||
name: 'myCookie',
|
||
value: 'cookieValue',
|
||
},
|
||
])
|
||
|
||
const firstTime = $('#now').text()
|
||
|
||
if (!(global as any).isNextDev) {
|
||
const res2 = await next.fetch('/force-static')
|
||
expect(res2.status).toBe(200)
|
||
|
||
const $2 = cheerio.load(await res2.text())
|
||
expect(firstTime).not.toBe($2('#now').text())
|
||
}
|
||
})
|
||
|
||
it('should allow dynamic routes to access cookies', async () => {
|
||
for (const slug of ['books', 'frameworks']) {
|
||
for (let i = 0; i < 2; i++) {
|
||
let $ = await next.render$(
|
||
`/force-dynamic-prerender/${slug}`,
|
||
{},
|
||
{ headers: { cookie: 'session=value' } }
|
||
)
|
||
|
||
expect($('#slug').text()).toBe(slug)
|
||
expect($('#cookie-result').text()).toBe('has cookie')
|
||
|
||
$ = await next.render$(`/force-dynamic-prerender/${slug}`)
|
||
|
||
expect($('#slug').text()).toBe(slug)
|
||
expect($('#cookie-result').text()).toBe('no cookie')
|
||
}
|
||
}
|
||
})
|
||
|
||
it('should not error with generateStaticParams and dynamic data', async () => {
|
||
const res = await next.fetch('/gen-params-dynamic/one')
|
||
const html = await res.text()
|
||
expect(res.status).toBe(200)
|
||
expect(html).toContain('gen-params-dynamic/[slug]')
|
||
expect(html).toContain('one')
|
||
|
||
const data = cheerio.load(html)('#data').text()
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
const res2 = await next.fetch('/gen-params-dynamic/one')
|
||
expect(res2.status).toBe(200)
|
||
expect(
|
||
cheerio
|
||
.load(await res2.text())('#data')
|
||
.text()
|
||
).not.toBe(data)
|
||
}
|
||
})
|
||
|
||
it('should not error with force-dynamic and catch-all routes', async () => {
|
||
// Regression test for https://github.com/vercel/next.js/issues/45603
|
||
const res = await next.fetch('/force-dynamic-catch-all/slug/a')
|
||
const html = await res.text()
|
||
expect(res.status).toBe(200)
|
||
expect(html).toContain('Dynamic catch-all route')
|
||
})
|
||
|
||
it('should not error with generateStaticParams and authed data on revalidate', async () => {
|
||
const res = await next.fetch('/gen-params-dynamic-revalidate/one')
|
||
const html = await res.text()
|
||
expect(res.status).toBe(200)
|
||
expect(html).toContain('gen-params-dynamic/[slug]')
|
||
expect(html).toContain('one')
|
||
const initData = cheerio.load(html)('#data').text()
|
||
|
||
await check(async () => {
|
||
const res2 = await next.fetch('/gen-params-dynamic-revalidate/one')
|
||
|
||
expect(res2.status).toBe(200)
|
||
|
||
const $ = cheerio.load(await res2.text())
|
||
expect($('#data').text()).toBeTruthy()
|
||
expect($('#data').text()).not.toBe(initData)
|
||
return 'success'
|
||
}, 'success')
|
||
})
|
||
|
||
if (!process.env.CUSTOM_CACHE_HANDLER) {
|
||
it('should honor dynamic = "force-static" correctly', async () => {
|
||
const res = await next.fetch('/force-static/first')
|
||
expect(res.status).toBe(200)
|
||
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
expect(JSON.parse($('#params').text())).toEqual({ slug: 'first' })
|
||
expect(JSON.parse($('#headers').text())).toEqual([])
|
||
expect(JSON.parse($('#cookies').text())).toEqual([])
|
||
|
||
const firstTime = $('#now').text()
|
||
|
||
if (!(global as any).isNextDev) {
|
||
const res2 = await next.fetch('/force-static/first')
|
||
expect(res2.status).toBe(200)
|
||
|
||
const $2 = cheerio.load(await res2.text())
|
||
expect(firstTime).toBe($2('#now').text())
|
||
}
|
||
})
|
||
|
||
it('should honor dynamic = "force-static" correctly (lazy)', async () => {
|
||
const res = await next.fetch('/force-static/random')
|
||
expect(res.status).toBe(200)
|
||
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
expect(JSON.parse($('#params').text())).toEqual({ slug: 'random' })
|
||
expect(JSON.parse($('#headers').text())).toEqual([])
|
||
expect(JSON.parse($('#cookies').text())).toEqual([])
|
||
|
||
const firstTime = $('#now').text()
|
||
|
||
if (!(global as any).isNextDev) {
|
||
const res2 = await next.fetch('/force-static/random')
|
||
expect(res2.status).toBe(200)
|
||
|
||
const $2 = cheerio.load(await res2.text())
|
||
expect(firstTime).toBe($2('#now').text())
|
||
}
|
||
})
|
||
}
|
||
|
||
// since we aren't leveraging fs cache with custom handler
|
||
// then these will 404 as they are cache misses
|
||
if (!(isNextStart && process.env.CUSTOM_CACHE_HANDLER)) {
|
||
it('should handle dynamicParams: false correctly', async () => {
|
||
const validParams = ['tim', 'seb', 'styfle']
|
||
|
||
for (const param of validParams) {
|
||
const res = await next.fetch(`/blog/${param}`, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
expect(JSON.parse($('#params').text())).toEqual({
|
||
author: param,
|
||
})
|
||
expect($('#page').text()).toBe('/blog/[author]')
|
||
}
|
||
const invalidParams = ['timm', 'non-existent']
|
||
|
||
for (const param of invalidParams) {
|
||
const invalidRes = await next.fetch(`/blog/${param}`, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(invalidRes.status).toBe(404)
|
||
expect(await invalidRes.text()).toContain('page could not be found')
|
||
}
|
||
})
|
||
}
|
||
|
||
it('should work with forced dynamic path', async () => {
|
||
for (const slug of ['first', 'second']) {
|
||
const res = await next.fetch(`/dynamic-no-gen-params-ssr/${slug}`, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(res.status).toBe(200)
|
||
expect(await res.text()).toContain(`${slug}`)
|
||
}
|
||
})
|
||
|
||
it('should work with dynamic path no generateStaticParams', async () => {
|
||
for (const slug of ['first', 'second']) {
|
||
const res = await next.fetch(`/dynamic-no-gen-params/${slug}`, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(res.status).toBe(200)
|
||
expect(await res.text()).toContain(`${slug}`)
|
||
}
|
||
})
|
||
|
||
it('should handle dynamicParams: true correctly', async () => {
|
||
const paramsToCheck = [
|
||
{
|
||
author: 'tim',
|
||
slug: 'first-post',
|
||
},
|
||
{
|
||
author: 'seb',
|
||
slug: 'second-post',
|
||
},
|
||
{
|
||
author: 'styfle',
|
||
slug: 'first-post',
|
||
},
|
||
{
|
||
author: 'new-author',
|
||
slug: 'first-post',
|
||
},
|
||
]
|
||
|
||
for (const params of paramsToCheck) {
|
||
const res = await next.fetch(`/blog/${params.author}/${params.slug}`, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(res.status).toBe(200)
|
||
const html = await res.text()
|
||
const $ = cheerio.load(html)
|
||
|
||
expect(JSON.parse($('#params').text())).toEqual(params)
|
||
expect($('#page').text()).toBe('/blog/[author]/[slug]')
|
||
}
|
||
})
|
||
|
||
// since we aren't leveraging fs cache with custom handler
|
||
// then these will 404 as they are cache misses
|
||
if (!(isNextStart && process.env.CUSTOM_CACHE_HANDLER)) {
|
||
it('should navigate to static path correctly', async () => {
|
||
const browser = await next.browser('/blog/tim')
|
||
await browser.eval('window.beforeNav = 1')
|
||
|
||
expect(
|
||
await browser.eval('document.documentElement.innerHTML')
|
||
).toContain('/blog/[author]')
|
||
await browser.elementByCss('#author-2').click()
|
||
|
||
await check(async () => {
|
||
const params = JSON.parse(
|
||
await browser.elementByCss('#params').text()
|
||
)
|
||
return params.author === 'seb' ? 'found' : params
|
||
}, 'found')
|
||
|
||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||
await browser.elementByCss('#author-1-post-1').click()
|
||
|
||
await check(async () => {
|
||
const params = JSON.parse(
|
||
await browser.elementByCss('#params').text()
|
||
)
|
||
return params.author === 'tim' && params.slug === 'first-post'
|
||
? 'found'
|
||
: params
|
||
}, 'found')
|
||
|
||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||
await browser.back()
|
||
|
||
await check(async () => {
|
||
const params = JSON.parse(
|
||
await browser.elementByCss('#params').text()
|
||
)
|
||
return params.author === 'seb' ? 'found' : params
|
||
}, 'found')
|
||
|
||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||
})
|
||
}
|
||
|
||
it('should ssr dynamically when detected automatically with fetch cache option', async () => {
|
||
const pathname = '/ssr-auto/cache-no-store'
|
||
const initialRes = await next.fetch(pathname, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(initialRes.status).toBe(200)
|
||
|
||
const initialHtml = await initialRes.text()
|
||
const initial$ = cheerio.load(initialHtml)
|
||
|
||
expect(initial$('#page').text()).toBe(pathname)
|
||
const initialDate = initial$('#date').text()
|
||
|
||
expect(initialHtml).toContain('Example Domain')
|
||
|
||
const secondRes = await next.fetch(pathname, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(secondRes.status).toBe(200)
|
||
|
||
const secondHtml = await secondRes.text()
|
||
const second$ = cheerio.load(secondHtml)
|
||
|
||
expect(second$('#page').text()).toBe(pathname)
|
||
const secondDate = second$('#date').text()
|
||
|
||
expect(secondHtml).toContain('Example Domain')
|
||
expect(secondDate).not.toBe(initialDate)
|
||
})
|
||
|
||
it('should render not found pages correctly and fallback to the default one', async () => {
|
||
const res = await next.fetch(`/blog/shu/hi`, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(res.status).toBe(404)
|
||
const html = await res.text()
|
||
expect(html).toInclude('"noindex"')
|
||
expect(html).toInclude('This page could not be found.')
|
||
})
|
||
|
||
// TODO-APP: support fetch revalidate case for dynamic rendering
|
||
it.skip('should ssr dynamically when detected automatically with fetch revalidate option', async () => {
|
||
const pathname = '/ssr-auto/fetch-revalidate-zero'
|
||
const initialRes = await next.fetch(pathname, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(initialRes.status).toBe(200)
|
||
|
||
const initialHtml = await initialRes.text()
|
||
const initial$ = cheerio.load(initialHtml)
|
||
|
||
expect(initial$('#page').text()).toBe(pathname)
|
||
const initialDate = initial$('#date').text()
|
||
|
||
expect(initialHtml).toContain('Example Domain')
|
||
|
||
const secondRes = await next.fetch(pathname, {
|
||
redirect: 'manual',
|
||
})
|
||
expect(secondRes.status).toBe(200)
|
||
|
||
const secondHtml = await secondRes.text()
|
||
const second$ = cheerio.load(secondHtml)
|
||
|
||
expect(second$('#page').text()).toBe(pathname)
|
||
const secondDate = second$('#date').text()
|
||
|
||
expect(secondHtml).toContain('Example Domain')
|
||
expect(secondDate).not.toBe(initialDate)
|
||
})
|
||
|
||
it('should ssr dynamically when forced via config', async () => {
|
||
const initialRes = await next.fetch('/ssr-forced', {
|
||
redirect: 'manual',
|
||
})
|
||
expect(initialRes.status).toBe(200)
|
||
|
||
const initialHtml = await initialRes.text()
|
||
const initial$ = cheerio.load(initialHtml)
|
||
|
||
expect(initial$('#page').text()).toBe('/ssr-forced')
|
||
const initialDate = initial$('#date').text()
|
||
|
||
const secondRes = await next.fetch('/ssr-forced', {
|
||
redirect: 'manual',
|
||
})
|
||
expect(secondRes.status).toBe(200)
|
||
|
||
const secondHtml = await secondRes.text()
|
||
const second$ = cheerio.load(secondHtml)
|
||
|
||
expect(second$('#page').text()).toBe('/ssr-forced')
|
||
const secondDate = second$('#date').text()
|
||
|
||
expect(secondDate).not.toBe(initialDate)
|
||
})
|
||
|
||
describe('useSearchParams', () => {
|
||
describe('client', () => {
|
||
it('should bailout to client rendering - without suspense boundary', async () => {
|
||
const browser = await next.browser(
|
||
'/hooks/use-search-params?first=value&second=other&third'
|
||
)
|
||
|
||
expect(await browser.elementByCss('#params-first').text()).toBe(
|
||
'value'
|
||
)
|
||
expect(await browser.elementByCss('#params-second').text()).toBe(
|
||
'other'
|
||
)
|
||
expect(await browser.elementByCss('#params-third').text()).toBe('')
|
||
expect(await browser.elementByCss('#params-not-real').text()).toBe(
|
||
'N/A'
|
||
)
|
||
})
|
||
|
||
it('should bailout to client rendering - with suspense boundary', async () => {
|
||
const browser = await next.browser(
|
||
'/hooks/use-search-params/with-suspense?first=value&second=other&third'
|
||
)
|
||
|
||
expect(await browser.elementByCss('#params-first').text()).toBe(
|
||
'value'
|
||
)
|
||
expect(await browser.elementByCss('#params-second').text()).toBe(
|
||
'other'
|
||
)
|
||
expect(await browser.elementByCss('#params-third').text()).toBe('')
|
||
expect(await browser.elementByCss('#params-not-real').text()).toBe(
|
||
'N/A'
|
||
)
|
||
})
|
||
|
||
it.skip('should have empty search params on force-static', async () => {
|
||
const browser = await next.browser(
|
||
'/hooks/use-search-params/force-static?first=value&second=other&third'
|
||
)
|
||
|
||
expect(await browser.elementByCss('#params-first').text()).toBe('N/A')
|
||
expect(await browser.elementByCss('#params-second').text()).toBe(
|
||
'N/A'
|
||
)
|
||
expect(await browser.elementByCss('#params-third').text()).toBe('N/A')
|
||
expect(await browser.elementByCss('#params-not-real').text()).toBe(
|
||
'N/A'
|
||
)
|
||
|
||
await browser.elementById('to-use-search-params').click()
|
||
await browser.waitForElementByCss('#hooks-use-search-params')
|
||
|
||
// Should not be empty after navigating to another page with useSearchParams
|
||
expect(await browser.elementByCss('#params-first').text()).toBe('1')
|
||
expect(await browser.elementByCss('#params-second').text()).toBe('2')
|
||
expect(await browser.elementByCss('#params-third').text()).toBe('3')
|
||
expect(await browser.elementByCss('#params-not-real').text()).toBe(
|
||
'N/A'
|
||
)
|
||
})
|
||
|
||
// TODO-APP: re-enable after investigating rewrite params
|
||
if (!(global as any).isNextDeploy) {
|
||
it('should have values from canonical url on rewrite', async () => {
|
||
const browser = await next.browser(
|
||
'/rewritten-use-search-params?first=a&second=b&third=c'
|
||
)
|
||
|
||
expect(await browser.elementByCss('#params-first').text()).toBe('a')
|
||
expect(await browser.elementByCss('#params-second').text()).toBe(
|
||
'b'
|
||
)
|
||
expect(await browser.elementByCss('#params-third').text()).toBe('c')
|
||
expect(await browser.elementByCss('#params-not-real').text()).toBe(
|
||
'N/A'
|
||
)
|
||
})
|
||
}
|
||
})
|
||
// Don't run these tests in dev mode since they won't be statically generated
|
||
if (!isDev) {
|
||
describe('server response', () => {
|
||
it('should bailout to client rendering - without suspense boundary', async () => {
|
||
const res = await next.fetch('/hooks/use-search-params')
|
||
const html = await res.text()
|
||
expect(html).toInclude('<html id="__next_error__">')
|
||
})
|
||
|
||
it('should bailout to client rendering - with suspense boundary', async () => {
|
||
const res = await next.fetch(
|
||
'/hooks/use-search-params/with-suspense'
|
||
)
|
||
const html = await res.text()
|
||
expect(html).toInclude('<p>search params suspense</p>')
|
||
})
|
||
|
||
it.skip('should have empty search params on force-static', async () => {
|
||
const res = await next.fetch(
|
||
'/hooks/use-search-params/force-static?first=value&second=other&third'
|
||
)
|
||
const html = await res.text()
|
||
|
||
// Should not bail out to client rendering
|
||
expect(html).not.toInclude('<p>search params suspense</p>')
|
||
|
||
// Use empty search params instead
|
||
const $ = cheerio.load(html)
|
||
expect($('#params-first').text()).toBe('N/A')
|
||
expect($('#params-second').text()).toBe('N/A')
|
||
expect($('#params-third').text()).toBe('N/A')
|
||
expect($('#params-not-real').text()).toBe('N/A')
|
||
})
|
||
})
|
||
}
|
||
})
|
||
|
||
describe('usePathname', () => {
|
||
it('should have the correct values', async () => {
|
||
const $ = await next.render$('/hooks/use-pathname/slug')
|
||
expect($('#pathname').text()).toContain('/hooks/use-pathname/slug')
|
||
|
||
const browser = await next.browser('/hooks/use-pathname/slug')
|
||
|
||
expect(await browser.elementByCss('#pathname').text()).toBe(
|
||
'/hooks/use-pathname/slug'
|
||
)
|
||
})
|
||
|
||
it('should have values from canonical url on rewrite', async () => {
|
||
const browser = await next.browser('/rewritten-use-pathname')
|
||
|
||
expect(await browser.elementByCss('#pathname').text()).toBe(
|
||
'/rewritten-use-pathname'
|
||
)
|
||
})
|
||
})
|
||
|
||
it('should keep querystring on static page', async () => {
|
||
const browser = await next.browser('/blog/tim?message=hello-world')
|
||
const checkUrl = async () =>
|
||
expect(await browser.url()).toBe(
|
||
next.url + '/blog/tim?message=hello-world'
|
||
)
|
||
|
||
checkUrl()
|
||
await waitFor(1000)
|
||
checkUrl()
|
||
})
|
||
|
||
if (process.env.CUSTOM_CACHE_HANDLER && !isNextDeploy) {
|
||
it('should have logs from cache-handler', () => {
|
||
expect(next.cliOutput).toContain('initialized custom cache-handler')
|
||
expect(next.cliOutput).toContain('cache-handler get')
|
||
expect(next.cliOutput).toContain('cache-handler set')
|
||
})
|
||
}
|
||
}
|
||
)
|