f354f46b3f
This PR deprecates declaring a middleware under `pages` in favour of the project root naming it after `middleware` instead of `_middleware`. This is in the context of having a simpler execution model for middleware and also ships some refactor work. There is a ton of a code to be simplified after this deprecation but I think it is best to do it progressively. With this PR, when in development, we will **fail** whenever we find a nested middleware but we do **not** include it in the compiler so if the project is using it, it will no longer work. For production we will **fail** too so it will not be possible to build and deploy a deprecated middleware. The error points to a page that should also be reviewed as part of **documentation**. Aside from the deprecation, this migrates all middleware tests to work with a single middleware. It also splits tests into multiple folders to make them easier to isolate and work with. Finally it ships some small code refactor and simplifications.
1022 lines
30 KiB
TypeScript
1022 lines
30 KiB
TypeScript
import glob from 'glob'
|
|
import fs from 'fs-extra'
|
|
import cheerio from 'cheerio'
|
|
import { join } from 'path'
|
|
import { createNext, FileRef } from 'e2e-utils'
|
|
import { NextInstance } from 'test/lib/next-modes/base'
|
|
import {
|
|
check,
|
|
fetchViaHTTP,
|
|
findPort,
|
|
initNextServerScript,
|
|
killApp,
|
|
renderViaHTTP,
|
|
waitFor,
|
|
} from 'next-test-utils'
|
|
|
|
describe('should set-up next', () => {
|
|
let next: NextInstance
|
|
let server
|
|
let appPort
|
|
let errors = []
|
|
let stderr = ''
|
|
let requiredFilesManifest
|
|
|
|
beforeAll(async () => {
|
|
// test build against environment with next support
|
|
process.env.NOW_BUILDER = '1'
|
|
|
|
next = await createNext({
|
|
files: {
|
|
pages: new FileRef(join(__dirname, 'required-server-files/pages')),
|
|
lib: new FileRef(join(__dirname, 'required-server-files/lib')),
|
|
'middleware.js': new FileRef(
|
|
join(__dirname, 'required-server-files/middleware.js')
|
|
),
|
|
'data.txt': new FileRef(
|
|
join(__dirname, 'required-server-files/data.txt')
|
|
),
|
|
'.env': new FileRef(join(__dirname, 'required-server-files/.env')),
|
|
'.env.local': new FileRef(
|
|
join(__dirname, 'required-server-files/.env.local')
|
|
),
|
|
'.env.production': new FileRef(
|
|
join(__dirname, 'required-server-files/.env.production')
|
|
),
|
|
},
|
|
nextConfig: {
|
|
eslint: {
|
|
ignoreDuringBuilds: true,
|
|
},
|
|
experimental: {
|
|
outputStandalone: true,
|
|
},
|
|
async rewrites() {
|
|
return {
|
|
beforeFiles: [],
|
|
fallback: [
|
|
{
|
|
source: '/an-ssg-path',
|
|
destination: '/hello.txt',
|
|
},
|
|
{
|
|
source: '/fallback-false/:path',
|
|
destination: '/hello.txt',
|
|
},
|
|
],
|
|
afterFiles: [
|
|
{
|
|
source: '/some-catch-all/:path*',
|
|
destination: '/',
|
|
},
|
|
{
|
|
source: '/to-dynamic/post-2',
|
|
destination: '/dynamic/post-2?hello=world',
|
|
},
|
|
{
|
|
source: '/to-dynamic/:path',
|
|
destination: '/dynamic/:path',
|
|
},
|
|
],
|
|
}
|
|
},
|
|
},
|
|
})
|
|
await next.stop()
|
|
|
|
requiredFilesManifest = JSON.parse(
|
|
await next.readFile('.next/required-server-files.json')
|
|
)
|
|
await fs.move(
|
|
join(next.testDir, '.next/standalone'),
|
|
join(next.testDir, 'standalone')
|
|
)
|
|
for (const file of await fs.readdir(next.testDir)) {
|
|
if (file !== 'standalone') {
|
|
await fs.remove(join(next.testDir, file))
|
|
console.log('removed', file)
|
|
}
|
|
}
|
|
const files = glob.sync('**/*', {
|
|
cwd: join(next.testDir, 'standalone/.next/server/pages'),
|
|
dot: true,
|
|
})
|
|
|
|
for (const file of files) {
|
|
if (file.endsWith('.json') || file.endsWith('.html')) {
|
|
await fs.remove(join(next.testDir, '.next/server', file))
|
|
}
|
|
}
|
|
|
|
const testServer = join(next.testDir, 'standalone/server.js')
|
|
await fs.writeFile(
|
|
testServer,
|
|
(await fs.readFile(testServer, 'utf8'))
|
|
.replace('console.error(err)', `console.error('top-level', err)`)
|
|
.replace('conf:', 'minimalMode: true,conf:')
|
|
)
|
|
appPort = await findPort()
|
|
server = await initNextServerScript(
|
|
testServer,
|
|
/Listening on/,
|
|
{
|
|
...process.env,
|
|
PORT: appPort,
|
|
},
|
|
undefined,
|
|
{
|
|
cwd: next.testDir,
|
|
onStderr(msg) {
|
|
if (msg.includes('top-level')) {
|
|
errors.push(msg)
|
|
}
|
|
stderr += msg
|
|
},
|
|
}
|
|
)
|
|
})
|
|
afterAll(async () => {
|
|
await next.destroy()
|
|
if (server) await killApp(server)
|
|
})
|
|
|
|
it('should warn when "next" is imported directly', async () => {
|
|
await renderViaHTTP(appPort, '/gssp')
|
|
await check(
|
|
() => stderr,
|
|
/"next" should not be imported directly, imported in/
|
|
)
|
|
})
|
|
|
|
it('`compress` should be `true` by default', async () => {
|
|
expect(
|
|
await fs.readFileSync(join(next.testDir, 'standalone/server.js'), 'utf8')
|
|
).toContain('"compress":true')
|
|
})
|
|
|
|
it('should output middleware correctly', async () => {
|
|
expect(
|
|
await fs.pathExists(
|
|
join(next.testDir, 'standalone/.next/server/edge-runtime-webpack.js')
|
|
)
|
|
).toBe(true)
|
|
expect(
|
|
await fs.pathExists(
|
|
join(next.testDir, 'standalone/.next/server/middleware.js')
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
it('should output required-server-files manifest correctly', async () => {
|
|
expect(requiredFilesManifest.version).toBe(1)
|
|
expect(Array.isArray(requiredFilesManifest.files)).toBe(true)
|
|
expect(Array.isArray(requiredFilesManifest.ignore)).toBe(true)
|
|
expect(requiredFilesManifest.files.length).toBeGreaterThan(0)
|
|
expect(requiredFilesManifest.ignore.length).toBeGreaterThan(0)
|
|
expect(typeof requiredFilesManifest.config.configFile).toBe('undefined')
|
|
expect(typeof requiredFilesManifest.config.trailingSlash).toBe('boolean')
|
|
expect(typeof requiredFilesManifest.appDir).toBe('string')
|
|
})
|
|
|
|
it('should de-dupe HTML/data requests', async () => {
|
|
const res = await fetchViaHTTP(appPort, '/gsp', undefined, {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('x-nextjs-cache')).toBeFalsy()
|
|
const $ = cheerio.load(await res.text())
|
|
const props = JSON.parse($('#props').text())
|
|
expect(props.gspCalls).toBeDefined()
|
|
|
|
const res2 = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/gsp.json`,
|
|
undefined,
|
|
{
|
|
redirect: 'manual',
|
|
}
|
|
)
|
|
expect(res2.status).toBe(200)
|
|
expect(res2.headers.get('x-nextjs-cache')).toBeFalsy()
|
|
const { pageProps: props2 } = await res2.json()
|
|
expect(props2.gspCalls).toBe(props.gspCalls)
|
|
|
|
const res3 = await fetchViaHTTP(appPort, '/index', undefined, {
|
|
redirect: 'manual',
|
|
headers: {
|
|
'x-matched-path': '/index',
|
|
},
|
|
})
|
|
expect(res3.status).toBe(200)
|
|
const $2 = cheerio.load(await res3.text())
|
|
const props3 = JSON.parse($2('#props').text())
|
|
expect(props3.gspCalls).toBeDefined()
|
|
|
|
const res4 = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/index.json`,
|
|
undefined,
|
|
{
|
|
redirect: 'manual',
|
|
}
|
|
)
|
|
expect(res4.status).toBe(200)
|
|
const { pageProps: props4 } = await res4.json()
|
|
expect(props4.gspCalls).toBe(props3.gspCalls)
|
|
})
|
|
|
|
it('should cap de-dupe previousCacheItem expires time', async () => {
|
|
const res = await fetchViaHTTP(appPort, '/gsp-long-revalidate', undefined, {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res.status).toBe(200)
|
|
const $ = cheerio.load(await res.text())
|
|
const props = JSON.parse($('#props').text())
|
|
expect(props.gspCalls).toBeDefined()
|
|
|
|
await waitFor(1000)
|
|
|
|
const res2 = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/gsp-long-revalidate.json`,
|
|
undefined,
|
|
{
|
|
redirect: 'manual',
|
|
}
|
|
)
|
|
expect(res2.status).toBe(200)
|
|
const { pageProps: props2 } = await res2.json()
|
|
expect(props2.gspCalls).not.toBe(props.gspCalls)
|
|
})
|
|
|
|
it('should not 404 for onlyGenerated manual revalidate in minimal mode', async () => {
|
|
const previewProps = JSON.parse(
|
|
await next.readFile('standalone/.next/prerender-manifest.json')
|
|
).preview
|
|
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/optional-ssg/only-generated-1',
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-prerender-revalidate': previewProps.previewModeId,
|
|
'x-prerender-revalidate-if-generated': '1',
|
|
},
|
|
}
|
|
)
|
|
expect(res.status).toBe(200)
|
|
})
|
|
|
|
it('should set correct SWR headers with notFound gsp', async () => {
|
|
await waitFor(2000)
|
|
await next.patchFile('standalone/data.txt', 'show')
|
|
|
|
const res = await fetchViaHTTP(appPort, '/gsp', undefined, {
|
|
redirect: 'manual ',
|
|
})
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('cache-control')).toBe(
|
|
's-maxage=1, stale-while-revalidate'
|
|
)
|
|
|
|
await waitFor(2000)
|
|
await next.patchFile('standalone/data.txt', 'hide')
|
|
|
|
const res2 = await fetchViaHTTP(appPort, '/gsp', undefined, {
|
|
redirect: 'manual ',
|
|
})
|
|
expect(res2.status).toBe(404)
|
|
expect(res2.headers.get('cache-control')).toBe(
|
|
's-maxage=1, stale-while-revalidate'
|
|
)
|
|
})
|
|
|
|
it('should set correct SWR headers with notFound gssp', async () => {
|
|
await next.patchFile('standalone/data.txt', 'show')
|
|
|
|
const res = await fetchViaHTTP(appPort, '/gssp', undefined, {
|
|
redirect: 'manual ',
|
|
})
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('cache-control')).toBe(
|
|
's-maxage=1, stale-while-revalidate'
|
|
)
|
|
|
|
await next.patchFile('standalone/data.txt', 'hide')
|
|
|
|
const res2 = await fetchViaHTTP(appPort, '/gssp', undefined, {
|
|
redirect: 'manual ',
|
|
})
|
|
await next.patchFile('standalone/data.txt', 'show')
|
|
|
|
expect(res2.status).toBe(404)
|
|
expect(res2.headers.get('cache-control')).toBe(
|
|
's-maxage=1, stale-while-revalidate'
|
|
)
|
|
})
|
|
|
|
it('should render SSR page correctly', async () => {
|
|
const html = await renderViaHTTP(appPort, '/gssp')
|
|
const $ = cheerio.load(html)
|
|
const data = JSON.parse($('#props').text())
|
|
|
|
expect($('#gssp').text()).toBe('getServerSideProps page')
|
|
expect(data.hello).toBe('world')
|
|
|
|
const html2 = await renderViaHTTP(appPort, '/gssp')
|
|
const $2 = cheerio.load(html2)
|
|
const data2 = JSON.parse($2('#props').text())
|
|
|
|
expect($2('#gssp').text()).toBe('getServerSideProps page')
|
|
expect(isNaN(data2.random)).toBe(false)
|
|
expect(data2.random).not.toBe(data.random)
|
|
})
|
|
|
|
it('should render dynamic SSR page correctly', async () => {
|
|
const html = await renderViaHTTP(appPort, '/dynamic/first')
|
|
const $ = cheerio.load(html)
|
|
const data = JSON.parse($('#props').text())
|
|
|
|
expect($('#dynamic').text()).toBe('dynamic page')
|
|
expect($('#slug').text()).toBe('first')
|
|
expect(data.hello).toBe('world')
|
|
|
|
const html2 = await renderViaHTTP(appPort, '/dynamic/second')
|
|
const $2 = cheerio.load(html2)
|
|
const data2 = JSON.parse($2('#props').text())
|
|
|
|
expect($2('#dynamic').text()).toBe('dynamic page')
|
|
expect($2('#slug').text()).toBe('second')
|
|
expect(isNaN(data2.random)).toBe(false)
|
|
expect(data2.random).not.toBe(data.random)
|
|
})
|
|
|
|
it('should render fallback page correctly', async () => {
|
|
const html = await renderViaHTTP(appPort, '/fallback/first')
|
|
const $ = cheerio.load(html)
|
|
const data = JSON.parse($('#props').text())
|
|
|
|
expect($('#fallback').text()).toBe('fallback page')
|
|
expect($('#slug').text()).toBe('first')
|
|
expect(data.hello).toBe('world')
|
|
|
|
await waitFor(2000)
|
|
const html2 = await renderViaHTTP(appPort, '/fallback/first')
|
|
const $2 = cheerio.load(html2)
|
|
const data2 = JSON.parse($2('#props').text())
|
|
|
|
expect($2('#fallback').text()).toBe('fallback page')
|
|
expect($2('#slug').text()).toBe('first')
|
|
expect(isNaN(data2.random)).toBe(false)
|
|
expect(data2.random).not.toBe(data.random)
|
|
|
|
const html3 = await renderViaHTTP(appPort, '/fallback/second')
|
|
const $3 = cheerio.load(html3)
|
|
const data3 = JSON.parse($3('#props').text())
|
|
|
|
expect($3('#fallback').text()).toBe('fallback page')
|
|
expect($3('#slug').text()).toBe('second')
|
|
expect(isNaN(data3.random)).toBe(false)
|
|
|
|
const { pageProps: data4 } = JSON.parse(
|
|
await renderViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/fallback/third.json`
|
|
)
|
|
)
|
|
expect(data4.hello).toBe('world')
|
|
expect(data4.slug).toBe('third')
|
|
})
|
|
|
|
it('should render SSR page correctly with x-matched-path', async () => {
|
|
const html = await renderViaHTTP(appPort, '/some-other-path', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/gssp',
|
|
},
|
|
})
|
|
const $ = cheerio.load(html)
|
|
const data = JSON.parse($('#props').text())
|
|
|
|
expect($('#gssp').text()).toBe('getServerSideProps page')
|
|
expect(data.hello).toBe('world')
|
|
|
|
const html2 = await renderViaHTTP(appPort, '/some-other-path', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/gssp',
|
|
},
|
|
})
|
|
const $2 = cheerio.load(html2)
|
|
const data2 = JSON.parse($2('#props').text())
|
|
|
|
expect($2('#gssp').text()).toBe('getServerSideProps page')
|
|
expect(isNaN(data2.random)).toBe(false)
|
|
expect(data2.random).not.toBe(data.random)
|
|
})
|
|
|
|
it('should render dynamic SSR page correctly with x-matched-path', async () => {
|
|
const html = await renderViaHTTP(appPort, '/some-other-path', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]?slug=first',
|
|
},
|
|
})
|
|
const $ = cheerio.load(html)
|
|
const data = JSON.parse($('#props').text())
|
|
|
|
expect($('#dynamic').text()).toBe('dynamic page')
|
|
expect($('#slug').text()).toBe('first')
|
|
expect(data.hello).toBe('world')
|
|
|
|
const html2 = await renderViaHTTP(appPort, '/some-other-path', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]?slug=second',
|
|
},
|
|
})
|
|
const $2 = cheerio.load(html2)
|
|
const data2 = JSON.parse($2('#props').text())
|
|
|
|
expect($2('#dynamic').text()).toBe('dynamic page')
|
|
expect($2('#slug').text()).toBe('second')
|
|
expect(isNaN(data2.random)).toBe(false)
|
|
expect(data2.random).not.toBe(data.random)
|
|
|
|
const html3 = await renderViaHTTP(appPort, '/some-other-path', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]?slug=%5Bslug%5D.json',
|
|
'x-now-route-matches': '1=second&slug=second',
|
|
},
|
|
})
|
|
const $3 = cheerio.load(html3)
|
|
const data3 = JSON.parse($3('#props').text())
|
|
|
|
expect($3('#dynamic').text()).toBe('dynamic page')
|
|
expect($3('#slug').text()).toBe('second')
|
|
expect(isNaN(data3.random)).toBe(false)
|
|
expect(data3.random).not.toBe(data.random)
|
|
})
|
|
|
|
it('should render fallback page correctly with x-matched-path and routes-matches', async () => {
|
|
const html = await renderViaHTTP(appPort, '/fallback/first', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/fallback/first',
|
|
'x-now-route-matches': '1=first',
|
|
},
|
|
})
|
|
const $ = cheerio.load(html)
|
|
const data = JSON.parse($('#props').text())
|
|
|
|
expect($('#fallback').text()).toBe('fallback page')
|
|
expect($('#slug').text()).toBe('first')
|
|
expect(data.hello).toBe('world')
|
|
|
|
const html2 = await renderViaHTTP(appPort, `/fallback/[slug]`, undefined, {
|
|
headers: {
|
|
'x-matched-path': '/fallback/[slug]',
|
|
'x-now-route-matches': '1=second',
|
|
},
|
|
})
|
|
const $2 = cheerio.load(html2)
|
|
const data2 = JSON.parse($2('#props').text())
|
|
|
|
expect($2('#fallback').text()).toBe('fallback page')
|
|
expect($2('#slug').text()).toBe('second')
|
|
expect(isNaN(data2.random)).toBe(false)
|
|
expect(data2.random).not.toBe(data.random)
|
|
})
|
|
|
|
it('should return data correctly with x-matched-path', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/dynamic/first.json`,
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]?slug=first',
|
|
},
|
|
}
|
|
)
|
|
|
|
const { pageProps: data } = await res.json()
|
|
|
|
expect(data.slug).toBe('first')
|
|
expect(data.hello).toBe('world')
|
|
|
|
const res2 = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/fallback/[slug].json`,
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/fallback/[slug].json`,
|
|
'x-now-route-matches': '1=second',
|
|
},
|
|
}
|
|
)
|
|
|
|
const { pageProps: data2 } = await res2.json()
|
|
|
|
expect(data2.slug).toBe('second')
|
|
expect(data2.hello).toBe('world')
|
|
})
|
|
|
|
it('should render fallback optional catch-all route correctly with x-matched-path and routes-matches', async () => {
|
|
const html = await renderViaHTTP(
|
|
appPort,
|
|
'/catch-all/[[...rest]]',
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/catch-all/[[...rest]]',
|
|
'x-now-route-matches': '',
|
|
},
|
|
}
|
|
)
|
|
const $ = cheerio.load(html)
|
|
const data = JSON.parse($('#props').text())
|
|
|
|
expect($('#catch-all').text()).toBe('optional catch-all page')
|
|
expect(data.params).toEqual({})
|
|
expect(data.hello).toBe('world')
|
|
|
|
const html2 = await renderViaHTTP(
|
|
appPort,
|
|
'/catch-all/[[...rest]]',
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/catch-all/[[...rest]]',
|
|
'x-now-route-matches': '1=hello&catchAll=hello',
|
|
},
|
|
}
|
|
)
|
|
const $2 = cheerio.load(html2)
|
|
const data2 = JSON.parse($2('#props').text())
|
|
|
|
expect($2('#catch-all').text()).toBe('optional catch-all page')
|
|
expect(data2.params).toEqual({ rest: ['hello'] })
|
|
expect(isNaN(data2.random)).toBe(false)
|
|
expect(data2.random).not.toBe(data.random)
|
|
|
|
const html3 = await renderViaHTTP(
|
|
appPort,
|
|
'/catch-all/[[..rest]]',
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/catch-all/[[...rest]]',
|
|
'x-now-route-matches': '1=hello/world&catchAll=hello/world',
|
|
},
|
|
}
|
|
)
|
|
const $3 = cheerio.load(html3)
|
|
const data3 = JSON.parse($3('#props').text())
|
|
|
|
expect($3('#catch-all').text()).toBe('optional catch-all page')
|
|
expect(data3.params).toEqual({ rest: ['hello', 'world'] })
|
|
expect(isNaN(data3.random)).toBe(false)
|
|
expect(data3.random).not.toBe(data.random)
|
|
})
|
|
|
|
it('should return data correctly with x-matched-path for optional catch-all route', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/catch-all.json`,
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/catch-all/[[...rest]]',
|
|
},
|
|
}
|
|
)
|
|
|
|
const { pageProps: data } = await res.json()
|
|
|
|
expect(data.params).toEqual({})
|
|
expect(data.hello).toBe('world')
|
|
|
|
const res2 = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/catch-all/[[...rest]].json`,
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/catch-all/[[...rest]].json`,
|
|
'x-now-route-matches': '1=hello&rest=hello',
|
|
},
|
|
}
|
|
)
|
|
|
|
const { pageProps: data2 } = await res2.json()
|
|
|
|
expect(data2.params).toEqual({ rest: ['hello'] })
|
|
expect(data2.hello).toBe('world')
|
|
|
|
const res3 = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/catch-all/[[...rest]].json`,
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/catch-all/[[...rest]].json`,
|
|
'x-now-route-matches': '1=hello/world&rest=hello/world',
|
|
},
|
|
}
|
|
)
|
|
|
|
const { pageProps: data3 } = await res3.json()
|
|
|
|
expect(data3.params).toEqual({ rest: ['hello', 'world'] })
|
|
expect(data3.hello).toBe('world')
|
|
})
|
|
|
|
it('should not apply trailingSlash redirect', async () => {
|
|
for (const path of [
|
|
'/',
|
|
'/dynamic/another/',
|
|
'/dynamic/another',
|
|
'/fallback/first/',
|
|
'/fallback/first',
|
|
'/fallback/another/',
|
|
'/fallback/another',
|
|
]) {
|
|
const res = await fetchViaHTTP(appPort, path, undefined, {
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(200)
|
|
}
|
|
})
|
|
|
|
it('should normalize catch-all rewrite query values correctly', async () => {
|
|
const html = await renderViaHTTP(
|
|
appPort,
|
|
'/some-catch-all/hello/world',
|
|
{
|
|
path: 'hello/world',
|
|
},
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/gssp',
|
|
},
|
|
}
|
|
)
|
|
const $ = cheerio.load(html)
|
|
expect(JSON.parse($('#router').text()).query).toEqual({
|
|
path: ['hello', 'world'],
|
|
})
|
|
})
|
|
|
|
it('should handle bad request correctly with rewrite', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/to-dynamic/%c0.%c0.',
|
|
'?path=%c0.%c0.',
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]',
|
|
},
|
|
}
|
|
)
|
|
expect(res.status).toBe(400)
|
|
expect(await res.text()).toContain('Bad Request')
|
|
})
|
|
|
|
it('should have correct resolvedUrl from rewrite', async () => {
|
|
const res = await fetchViaHTTP(appPort, '/to-dynamic/post-1', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]',
|
|
},
|
|
})
|
|
expect(res.status).toBe(200)
|
|
const $ = cheerio.load(await res.text())
|
|
expect($('#resolved-url').text()).toBe('/dynamic/post-1')
|
|
})
|
|
|
|
it('should have correct resolvedUrl from rewrite with added query', async () => {
|
|
const res = await fetchViaHTTP(appPort, '/to-dynamic/post-2', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]',
|
|
},
|
|
})
|
|
expect(res.status).toBe(200)
|
|
const $ = cheerio.load(await res.text())
|
|
expect($('#resolved-url').text()).toBe('/dynamic/post-2')
|
|
expect(JSON.parse($('#router').text()).asPath).toBe('/to-dynamic/post-2')
|
|
})
|
|
|
|
it('should have correct resolvedUrl from dynamic route', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/dynamic/post-2.json`,
|
|
{ slug: 'post-2' },
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]',
|
|
},
|
|
}
|
|
)
|
|
expect(res.status).toBe(200)
|
|
const json = await res.json()
|
|
expect(json.pageProps.resolvedUrl).toBe('/dynamic/post-2')
|
|
})
|
|
|
|
it('should bubble error correctly for gip page', async () => {
|
|
errors = []
|
|
const res = await fetchViaHTTP(appPort, '/errors/gip', { crash: '1' })
|
|
expect(res.status).toBe(500)
|
|
expect(await res.text()).toBe('internal server error')
|
|
|
|
await check(
|
|
() => (errors[0].includes('gip hit an oops') ? 'success' : errors[0]),
|
|
'success'
|
|
)
|
|
})
|
|
|
|
it('should bubble error correctly for gssp page', async () => {
|
|
errors = []
|
|
const res = await fetchViaHTTP(appPort, '/errors/gssp', { crash: '1' })
|
|
expect(res.status).toBe(500)
|
|
expect(await res.text()).toBe('internal server error')
|
|
await check(
|
|
() => (errors[0].includes('gssp hit an oops') ? 'success' : errors[0]),
|
|
'success'
|
|
)
|
|
})
|
|
|
|
it('should bubble error correctly for gsp page', async () => {
|
|
errors = []
|
|
const res = await fetchViaHTTP(appPort, '/errors/gsp/crash')
|
|
expect(res.status).toBe(500)
|
|
expect(await res.text()).toBe('internal server error')
|
|
await check(
|
|
() => (errors[0].includes('gsp hit an oops') ? 'success' : errors[0]),
|
|
'success'
|
|
)
|
|
})
|
|
|
|
it('should bubble error correctly for API page', async () => {
|
|
errors = []
|
|
const res = await fetchViaHTTP(appPort, '/api/error')
|
|
expect(res.status).toBe(500)
|
|
expect(await res.text()).toBe('internal server error')
|
|
await check(
|
|
() =>
|
|
errors[0].includes('some error from /api/error')
|
|
? 'success'
|
|
: errors[0],
|
|
'success'
|
|
)
|
|
})
|
|
|
|
it('should normalize optional values correctly for SSP page', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/optional-ssp',
|
|
{ rest: '', another: 'value' },
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/optional-ssp/[[...rest]]',
|
|
},
|
|
}
|
|
)
|
|
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
const props = JSON.parse($('#props').text())
|
|
expect(props.params).toEqual({})
|
|
expect(props.query).toEqual({ another: 'value' })
|
|
})
|
|
|
|
it('should normalize optional values correctly for SSG page', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/optional-ssg',
|
|
{ rest: '', another: 'value' },
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/optional-ssg/[[...rest]]',
|
|
},
|
|
}
|
|
)
|
|
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
const props = JSON.parse($('#props').text())
|
|
expect(props.params).toEqual({})
|
|
})
|
|
|
|
it('should normalize optional revalidations correctly for SSG page', async () => {
|
|
const reqs = [
|
|
{
|
|
path: `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
},
|
|
},
|
|
{
|
|
path: `/_next/data/${next.buildId}/optional-ssg.json`,
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
},
|
|
},
|
|
{
|
|
path: `/_next/data/${next.buildId}/optional-ssg.json`,
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/optional-ssg.json`,
|
|
},
|
|
},
|
|
{
|
|
path: `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
},
|
|
query: { rest: '' },
|
|
},
|
|
{
|
|
path: `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
'x-now-route-matches': '1=',
|
|
},
|
|
},
|
|
{
|
|
path: `/_next/data/${next.buildId}/optional-ssg/.json`,
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
'x-now-route-matches': '',
|
|
'x-vercel-id': 'cle1::',
|
|
},
|
|
},
|
|
{
|
|
path: `/optional-ssg/[[...rest]]`,
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
'x-now-route-matches': '',
|
|
'x-vercel-id': 'cle1::',
|
|
},
|
|
},
|
|
{
|
|
path: `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
headers: {
|
|
'x-matched-path': `/optional-ssg/[[...rest]]`,
|
|
'x-now-route-matches': '',
|
|
'x-vercel-id': 'cle1::',
|
|
},
|
|
},
|
|
]
|
|
|
|
for (const req of reqs) {
|
|
console.error('checking', req)
|
|
const res = await fetchViaHTTP(appPort, req.path, req.query, {
|
|
headers: req.headers,
|
|
})
|
|
|
|
const content = await res.text()
|
|
let props
|
|
|
|
try {
|
|
const data = JSON.parse(content)
|
|
props = data.pageProps
|
|
} catch (_) {
|
|
props = JSON.parse(cheerio.load(content)('#__NEXT_DATA__').text()).props
|
|
.pageProps
|
|
}
|
|
expect(props.params).toEqual({})
|
|
}
|
|
})
|
|
|
|
it('should normalize optional values correctly for SSG page with encoded slash', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/optional-ssg/[[...rest]]',
|
|
undefined,
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/optional-ssg/[[...rest]]',
|
|
'x-now-route-matches':
|
|
'1=en%2Fes%2Fhello%252Fworld&rest=en%2Fes%2Fhello%252Fworld',
|
|
},
|
|
}
|
|
)
|
|
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
const props = JSON.parse($('#props').text())
|
|
expect(props.params).toEqual({
|
|
rest: ['en', 'es', 'hello/world'],
|
|
})
|
|
})
|
|
|
|
it('should normalize optional values correctly for API page', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/api/optional',
|
|
{ rest: '', another: 'value' },
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/api/optional/[[...rest]]',
|
|
},
|
|
}
|
|
)
|
|
|
|
const json = await res.json()
|
|
expect(json.query).toEqual({ another: 'value' })
|
|
expect(json.url).toBe('/api/optional?another=value')
|
|
})
|
|
|
|
it('should normalize index optional values correctly for API page', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/api/optional/index',
|
|
{ rest: 'index', another: 'value' },
|
|
{
|
|
headers: {
|
|
'x-matched-path': '/api/optional/[[...rest]]',
|
|
},
|
|
}
|
|
)
|
|
|
|
const json = await res.json()
|
|
expect(json.query).toEqual({ another: 'value', rest: ['index'] })
|
|
expect(json.url).toBe('/api/optional/index?another=value')
|
|
})
|
|
|
|
it('should match the index page correctly', async () => {
|
|
const res = await fetchViaHTTP(appPort, '/', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/index',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
expect($('#index').text()).toBe('index page')
|
|
})
|
|
|
|
it('should match the root dynamic page correctly', async () => {
|
|
const res = await fetchViaHTTP(appPort, '/index', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/[slug]',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
expect($('#slug-page').text()).toBe('[slug] page')
|
|
})
|
|
|
|
it('should have correct asPath on dynamic SSG page correctly', async () => {
|
|
const res = await fetchViaHTTP(appPort, '/an-ssg-path', undefined, {
|
|
headers: {
|
|
'x-matched-path': '/[slug]',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
expect($('#slug-page').text()).toBe('[slug] page')
|
|
expect(JSON.parse($('#router').text()).asPath).toBe('/an-ssg-path')
|
|
})
|
|
|
|
it('should have correct asPath on dynamic SSG page fallback correctly', async () => {
|
|
const toCheck = [
|
|
{
|
|
pathname: '/fallback-false/first',
|
|
matchedPath: '/fallback-false/first',
|
|
},
|
|
{
|
|
pathname: '/fallback-false/first',
|
|
matchedPath: `/_next/data/${next.buildId}/fallback-false/first.json`,
|
|
},
|
|
]
|
|
for (const check of toCheck) {
|
|
console.warn('checking', check)
|
|
const res = await fetchViaHTTP(appPort, check.pathname, undefined, {
|
|
headers: {
|
|
'x-matched-path': check.matchedPath,
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
expect($('#page').text()).toBe('blog slug')
|
|
expect($('#asPath').text()).toBe('/fallback-false/first')
|
|
expect($('#pathname').text()).toBe('/fallback-false/[slug]')
|
|
expect(JSON.parse($('#query').text())).toEqual({ slug: 'first' })
|
|
}
|
|
})
|
|
|
|
it('should copy and read .env file', async () => {
|
|
const res = await fetchViaHTTP(appPort, '/api/env')
|
|
|
|
const envVariables = await res.json()
|
|
|
|
expect(envVariables.env).not.toBeUndefined()
|
|
expect(envVariables.envProd).not.toBeUndefined()
|
|
expect(envVariables.envLocal).toBeUndefined()
|
|
})
|
|
})
|