Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
1613 lines
44 KiB
TypeScript
1613 lines
44 KiB
TypeScript
import glob from 'glob'
|
|
import fs from 'fs-extra'
|
|
import cheerio from 'cheerio'
|
|
import { join } from 'path'
|
|
import { nanoid } from 'nanoid'
|
|
import { createNext, FileRef } from 'e2e-utils'
|
|
import { NextInstance } from 'e2e-utils'
|
|
import {
|
|
check,
|
|
createNowRouteMatches,
|
|
fetchViaHTTP,
|
|
findPort,
|
|
initNextServerScript,
|
|
killApp,
|
|
renderViaHTTP,
|
|
retry,
|
|
waitFor,
|
|
withInvocationId,
|
|
} from 'next-test-utils'
|
|
import { ChildProcess } from 'child_process'
|
|
|
|
describe('required server files', () => {
|
|
let next: NextInstance
|
|
let server: ChildProcess
|
|
let appPort: number | string
|
|
let errors = []
|
|
let stderr = ''
|
|
let requiredFilesManifest
|
|
let minimalMode = true
|
|
|
|
const setupNext = async ({ nextEnv }: { nextEnv?: boolean }) => {
|
|
// test build against environment with next support
|
|
process.env.NOW_BUILDER = nextEnv ? '1' : ''
|
|
process.env.NEXT_PRIVATE_TEST_HEADERS = '1'
|
|
|
|
next = await createNext({
|
|
files: {
|
|
pages: new FileRef(join(__dirname, 'pages')),
|
|
lib: new FileRef(join(__dirname, 'lib')),
|
|
'middleware.js': new FileRef(
|
|
join(
|
|
__dirname,
|
|
process.env.TEST_NODE_MIDDLEWARE
|
|
? 'middleware-node.js'
|
|
: 'middleware.js'
|
|
)
|
|
),
|
|
'instrumentation.js': new FileRef(
|
|
join(__dirname, 'instrumentation.js')
|
|
),
|
|
'cache-handler.js': new FileRef(join(__dirname, 'cache-handler.js')),
|
|
'data.txt': new FileRef(join(__dirname, 'data.txt')),
|
|
'.env': new FileRef(join(__dirname, '.env')),
|
|
'.env.local': new FileRef(join(__dirname, '.env.local')),
|
|
'.env.production': new FileRef(join(__dirname, '.env.production')),
|
|
},
|
|
nextConfig: {
|
|
cacheHandler: './cache-handler.js',
|
|
cacheMaxMemorySize: 0,
|
|
output: 'standalone',
|
|
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',
|
|
},
|
|
],
|
|
}
|
|
},
|
|
},
|
|
skipStart: true,
|
|
})
|
|
|
|
let { exitCode } = await next.build()
|
|
expect(exitCode).toBe(0)
|
|
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
await setupNext({ nextEnv: true })
|
|
})
|
|
|
|
beforeEach(async () => {
|
|
errors = []
|
|
stderr = ''
|
|
|
|
const testServerFilename = join(next.testDir, 'standalone/server.js')
|
|
const testServerContent = await fs.readFile(testServerFilename, 'utf8')
|
|
|
|
await fs.writeFile(
|
|
testServerFilename,
|
|
testServerContent.replace(
|
|
/(startServer\({\s*)(minimalMode: (true|false),\n {2})?/,
|
|
`$1minimalMode: ${minimalMode},\n `
|
|
)
|
|
)
|
|
|
|
appPort = await findPort()
|
|
|
|
server = await initNextServerScript(
|
|
testServerFilename,
|
|
/- Local:/,
|
|
{
|
|
...process.env,
|
|
...next.env,
|
|
ENV_FROM_HOST: 'FOOBAR',
|
|
PORT: `${appPort}`,
|
|
},
|
|
undefined,
|
|
{
|
|
cwd: next.testDir,
|
|
onStderr(msg) {
|
|
errors.push(msg)
|
|
stderr += msg
|
|
},
|
|
shouldRejectOnError: true,
|
|
}
|
|
)
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await killApp(server)
|
|
})
|
|
|
|
afterAll(async () => {
|
|
delete process.env.NOW_BUILDER
|
|
delete process.env.NEXT_PRIVATE_TEST_HEADERS
|
|
await next.destroy()
|
|
})
|
|
|
|
it('should resolve correctly when a redirect is returned', async () => {
|
|
const toRename = `standalone/.next/server/pages/route-resolving/[slug]/[project].html`
|
|
await next.renameFile(toRename, `${toRename}.bak`)
|
|
try {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/route-resolving/import/first',
|
|
undefined,
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
headers: {
|
|
'x-matched-path': '/route-resolving/import/[slug]',
|
|
},
|
|
})
|
|
)
|
|
expect(res.status).toBe(307)
|
|
expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe(
|
|
'/somewhere'
|
|
)
|
|
|
|
await waitFor(3000)
|
|
expect(stderr).not.toContain('ENOENT')
|
|
} finally {
|
|
await next.renameFile(`${toRename}.bak`, toRename)
|
|
}
|
|
})
|
|
|
|
it('should show invariant when an automatic static page is requested', async () => {
|
|
const toRename = `standalone/.next/server/pages/auto-static.html`
|
|
await next.renameFile(toRename, `${toRename}.bak`)
|
|
|
|
try {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/auto-static',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/auto-static',
|
|
},
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(500)
|
|
await check(() => stderr, /Invariant: failed to load static page/)
|
|
} finally {
|
|
await next.renameFile(`${toRename}.bak`, toRename)
|
|
}
|
|
})
|
|
|
|
it.each([
|
|
{
|
|
case: 'redirect no revalidate',
|
|
path: '/optional-ssg/redirect-1',
|
|
dest: '/somewhere',
|
|
cacheControl: 's-maxage=31536000',
|
|
},
|
|
{
|
|
case: 'redirect with revalidate',
|
|
path: '/optional-ssg/redirect-2',
|
|
dest: '/somewhere-else',
|
|
cacheControl: 's-maxage=5, stale-while-revalidate=31535995',
|
|
},
|
|
])(
|
|
`should have correct cache-control for $case`,
|
|
async ({ path, dest, cacheControl }) => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
path,
|
|
undefined,
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
expect(res.status).toBe(307)
|
|
expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe(
|
|
dest
|
|
)
|
|
expect(res.headers.get('cache-control')).toBe(cacheControl)
|
|
|
|
const dataRes = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}${path}.json`,
|
|
undefined,
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
expect((await dataRes.json()).pageProps).toEqual({
|
|
__N_REDIRECT: dest,
|
|
__N_REDIRECT_STATUS: 307,
|
|
})
|
|
expect(dataRes.headers.get('cache-control')).toBe(cacheControl)
|
|
}
|
|
)
|
|
|
|
it('should handle data routes with optional catch-all params', async () => {
|
|
let res = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/catch-all.json`,
|
|
{},
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/catch-all.json`,
|
|
},
|
|
})
|
|
)
|
|
expect(res.status).toBe(200)
|
|
|
|
let json = await res.json()
|
|
expect(json.pageProps.params).toEqual({
|
|
rest: undefined,
|
|
})
|
|
|
|
res = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/catch-all/next.js.json`,
|
|
{},
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/catch-all/next.js.json`,
|
|
},
|
|
})
|
|
)
|
|
expect(res.status).toBe(200)
|
|
|
|
json = await res.json()
|
|
expect(json.pageProps.params).toEqual({
|
|
rest: ['next.js'],
|
|
})
|
|
})
|
|
|
|
it.each([
|
|
{
|
|
case: 'notFound no revalidate',
|
|
path: '/optional-ssg/not-found-1',
|
|
dest: '/somewhere',
|
|
cacheControl: 's-maxage=31536000',
|
|
},
|
|
{
|
|
case: 'notFound with revalidate',
|
|
path: '/optional-ssg/not-found-2',
|
|
dest: '/somewhere-else',
|
|
cacheControl: 's-maxage=5, stale-while-revalidate=31535995',
|
|
},
|
|
])(
|
|
`should have correct cache-control for $case`,
|
|
async ({ path, dest, cacheControl }) => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
path,
|
|
undefined,
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
expect(res.status).toBe(404)
|
|
expect(res.headers.get('cache-control')).toBe(cacheControl)
|
|
|
|
const dataRes = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}${path}.json`,
|
|
undefined,
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
expect(dataRes.headers.get('cache-control')).toBe(cacheControl)
|
|
}
|
|
)
|
|
|
|
it('should have the correct cache-control for props with no revalidate', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/optional-ssg/props-no-revalidate',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('cache-control')).toBe('s-maxage=31536000')
|
|
const $ = cheerio.load(await res.text())
|
|
expect(JSON.parse($('#props').text()).params).toEqual({
|
|
rest: ['props-no-revalidate'],
|
|
})
|
|
|
|
const dataRes = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/optional-ssg/props-no-revalidate.json`,
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
expect(dataRes.status).toBe(200)
|
|
expect(res.headers.get('cache-control')).toBe('s-maxage=31536000')
|
|
expect((await dataRes.json()).pageProps.params).toEqual({
|
|
rest: ['props-no-revalidate'],
|
|
})
|
|
})
|
|
|
|
// TODO(mischnic) do we still want to do this?
|
|
;(process.env.IS_TURBOPACK_TEST ? it.skip : it)(
|
|
'should warn when "next" is imported directly',
|
|
async () => {
|
|
await renderViaHTTP(appPort, '/gssp', undefined, withInvocationId())
|
|
await check(
|
|
() => stderr,
|
|
/"next" should not be imported directly, imported in/
|
|
)
|
|
}
|
|
)
|
|
|
|
it('`compress` should be `false` in nextEnv', async () => {
|
|
expect(
|
|
await fs.readFileSync(join(next.testDir, 'standalone/server.js'), 'utf8')
|
|
).toContain('"compress":false')
|
|
})
|
|
|
|
it('`cacheHandler` should have correct path', async () => {
|
|
expect(
|
|
await fs.pathExists(join(next.testDir, 'standalone/cache-handler.js'))
|
|
).toBe(true)
|
|
|
|
expect(
|
|
await fs.readFileSync(join(next.testDir, 'standalone/server.js'), 'utf8')
|
|
).toContain('"cacheHandler":"../cache-handler.js"')
|
|
})
|
|
|
|
it('`cacheMaxMemorySize` should be disabled by setting to 0', async () => {
|
|
expect(
|
|
await fs.readFileSync(join(next.testDir, 'standalone/server.js'), 'utf8')
|
|
).toContain('"cacheMaxMemorySize":0')
|
|
})
|
|
|
|
it('should output middleware correctly', async () => {
|
|
if (process.env.TEST_NODE_MIDDLEWARE) {
|
|
expect(
|
|
await fs.pathExists(
|
|
join(next.testDir, 'standalone/.next/server/middleware.js')
|
|
)
|
|
).toBe(true)
|
|
} else {
|
|
let manifest = await fs.readJSON(
|
|
join(next.testDir, 'standalone/.next/server/middleware-manifest.json')
|
|
)
|
|
let middleware = manifest.middleware['/']
|
|
let files = [
|
|
...middleware.files,
|
|
...middleware.wasm.map((f) => f.filePath),
|
|
...middleware.assets.map((f) => f.filePath),
|
|
]
|
|
console.log(files)
|
|
for (const file of files) {
|
|
try {
|
|
expect(
|
|
await fs.pathExists(join(next.testDir, 'standalone/.next', file))
|
|
).toBe(true)
|
|
} catch (err) {
|
|
throw new Error('Missing file ' + file)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
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).toBe(0)
|
|
expect(typeof requiredFilesManifest.config.configFile).toBe('undefined')
|
|
expect(typeof requiredFilesManifest.config.trailingSlash).toBe('boolean')
|
|
expect(typeof requiredFilesManifest.appDir).toBe('string')
|
|
// not in a monorepo so relative app dir is empty string
|
|
expect(requiredFilesManifest.relativeAppDir).toBe('')
|
|
|
|
expect(
|
|
requiredFilesManifest.files.filter(
|
|
(f) => !fs.pathExistsSync(join(next.testDir, 'standalone', f))
|
|
)
|
|
).toBeEmpty()
|
|
})
|
|
|
|
it('should de-dupe HTML/data requests', async () => {
|
|
// Create a shared invocation ID for /gsp - both HTML and JSON requests share same x-invocation-id
|
|
const gspOpts = withInvocationId()
|
|
|
|
const res = await fetchViaHTTP(appPort, '/gsp', undefined, {
|
|
...gspOpts,
|
|
redirect: 'manual',
|
|
headers: {
|
|
...gspOpts.headers,
|
|
// ensure the nextjs-data header being present
|
|
// doesn't incorrectly return JSON for HTML path
|
|
// during prerendering
|
|
'x-nextjs-data': '1',
|
|
},
|
|
})
|
|
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,
|
|
{
|
|
...gspOpts,
|
|
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)
|
|
|
|
// Create a separate shared invocation ID for /index - different x-invocation-id
|
|
const indexOpts = withInvocationId()
|
|
|
|
const res3 = await fetchViaHTTP(appPort, '/index', undefined, {
|
|
...indexOpts,
|
|
redirect: 'manual',
|
|
headers: {
|
|
...indexOpts.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,
|
|
{
|
|
...indexOpts,
|
|
redirect: 'manual',
|
|
}
|
|
)
|
|
expect(res4.status).toBe(200)
|
|
const { pageProps: props4 } = await res4.json()
|
|
expect(props4.gspCalls).toBe(props3.gspCalls)
|
|
})
|
|
|
|
it('should not 404 for onlyGenerated on-demand 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,
|
|
withInvocationId({
|
|
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,
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('cache-control')).toBe(
|
|
's-maxage=1, stale-while-revalidate=31535999'
|
|
)
|
|
|
|
await waitFor(2000)
|
|
await next.patchFile('standalone/data.txt', 'hide')
|
|
|
|
const res2 = await fetchViaHTTP(
|
|
appPort,
|
|
'/gsp',
|
|
undefined,
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
expect(res2.status).toBe(404)
|
|
expect(res2.headers.get('cache-control')).toBe(
|
|
's-maxage=1, stale-while-revalidate=31535999'
|
|
)
|
|
})
|
|
|
|
it('should set correct SWR headers with notFound gssp', async () => {
|
|
await next.patchFile('standalone/data.txt', 'show')
|
|
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/gssp',
|
|
undefined,
|
|
withInvocationId({
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('cache-control')).toBe(
|
|
's-maxage=1, stale-while-revalidate=31535999'
|
|
)
|
|
|
|
await next.patchFile('standalone/data.txt', 'hide')
|
|
|
|
const res2 = await fetchViaHTTP(
|
|
appPort,
|
|
'/gssp',
|
|
undefined,
|
|
withInvocationId({
|
|
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=31535999'
|
|
)
|
|
})
|
|
|
|
it('should render SSR page correctly', async () => {
|
|
const html = await renderViaHTTP(
|
|
appPort,
|
|
'/gssp',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
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',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
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',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
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',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
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',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
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',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
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',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
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`,
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
)
|
|
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,
|
|
withInvocationId({
|
|
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,
|
|
withInvocationId({
|
|
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?nxtPslug=first',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]',
|
|
},
|
|
})
|
|
)
|
|
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?nxtPslug=second',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]',
|
|
},
|
|
})
|
|
)
|
|
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?nxtPslug=second',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/dynamic/[slug]',
|
|
},
|
|
})
|
|
)
|
|
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,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/fallback/first',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'first',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
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,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/fallback/[slug]',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'second',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
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 favor valid route params over routes-matches', async () => {
|
|
const html = await renderViaHTTP(
|
|
appPort,
|
|
'/fallback/first',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/fallback/first',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'fallback/first',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
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/second`,
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/fallback/[slug]',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'fallback/second',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
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 favor valid route params over routes-matches optional', async () => {
|
|
const html = await renderViaHTTP(
|
|
appPort,
|
|
'/optional-ssg',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/optional-ssg',
|
|
'x-now-route-matches': '',
|
|
},
|
|
})
|
|
)
|
|
const $ = cheerio.load(html)
|
|
const data = JSON.parse($('#props').text())
|
|
expect(data.params).toEqual({})
|
|
|
|
const html2 = await renderViaHTTP(
|
|
appPort,
|
|
`/optional-ssg`,
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/optional-ssg',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'another',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
const $2 = cheerio.load(html2)
|
|
const data2 = JSON.parse($2('#props').text())
|
|
|
|
expect(isNaN(data2.random)).toBe(false)
|
|
expect(data2.params).toEqual({})
|
|
})
|
|
|
|
it('should return data correctly with x-matched-path', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${next.buildId}/dynamic/first.json?${createNowRouteMatches({
|
|
slug: 'first',
|
|
}).toString()}`,
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': `/dynamic/[slug]`,
|
|
},
|
|
})
|
|
)
|
|
|
|
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,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/fallback/[slug].json`,
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
slug: 'second',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
|
|
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,
|
|
withInvocationId({
|
|
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,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/catch-all/[[...rest]]',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
rest: 'hello',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
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,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/catch-all/[[...rest]]',
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
rest: 'hello/world',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
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,
|
|
withInvocationId({
|
|
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,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/catch-all/[[...rest]].json`,
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
rest: 'hello',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
|
|
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,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/catch-all/[[...rest]].json`,
|
|
'x-now-route-matches': createNowRouteMatches({
|
|
rest: 'hello/world',
|
|
}).toString(),
|
|
},
|
|
})
|
|
)
|
|
|
|
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,
|
|
withInvocationId({
|
|
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',
|
|
},
|
|
withInvocationId({
|
|
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.',
|
|
},
|
|
withInvocationId({
|
|
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,
|
|
withInvocationId({
|
|
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,
|
|
withInvocationId({
|
|
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' },
|
|
withInvocationId({
|
|
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 () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/errors/gip',
|
|
{ crash: '1' },
|
|
withInvocationId()
|
|
)
|
|
expect(res.status).toBe(500)
|
|
expect(await res.text()).toBe('Internal Server Error')
|
|
|
|
await retry(() => {
|
|
expect(errors.join('\n')).toInclude('gip hit an oops')
|
|
})
|
|
})
|
|
|
|
it('should bubble error correctly for gssp page', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/errors/gssp',
|
|
{ crash: '1' },
|
|
withInvocationId()
|
|
)
|
|
expect(res.status).toBe(500)
|
|
expect(await res.text()).toBe('Internal Server Error')
|
|
|
|
await retry(() => {
|
|
expect(errors.join('\n')).toInclude('gssp hit an oops')
|
|
})
|
|
})
|
|
|
|
it('should bubble error correctly for gsp page', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/errors/gsp/crash',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
expect(res.status).toBe(500)
|
|
expect(await res.text()).toBe('Internal Server Error')
|
|
|
|
await retry(() => {
|
|
expect(errors.join('\n')).toInclude('gsp hit an oops')
|
|
})
|
|
})
|
|
|
|
it('should bubble error correctly for API page', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/api/error',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
expect(res.status).toBe(500)
|
|
expect(await res.text()).toBe('Internal Server Error')
|
|
|
|
await retry(() => {
|
|
expect(errors.join('\n')).toInclude('some error from /api/error')
|
|
})
|
|
})
|
|
|
|
it('should normalize optional values correctly for SSP page', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/optional-ssp',
|
|
{ nxtPrest: '', another: 'value' },
|
|
withInvocationId({
|
|
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',
|
|
{ nxtPrest: '', another: 'value' },
|
|
withInvocationId({
|
|
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': createNowRouteMatches({
|
|
rest: '',
|
|
}).toString(),
|
|
},
|
|
},
|
|
{
|
|
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-invocation-id': 'cle1::',
|
|
},
|
|
},
|
|
{
|
|
path: `/optional-ssg/[[...rest]]`,
|
|
headers: {
|
|
'x-matched-path': `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
'x-now-route-matches': '',
|
|
'x-invocation-id': 'cle1::',
|
|
},
|
|
},
|
|
{
|
|
path: `/_next/data/${next.buildId}/optional-ssg/[[...rest]].json`,
|
|
headers: {
|
|
'x-matched-path': `/optional-ssg/[[...rest]]`,
|
|
'x-now-route-matches': '',
|
|
'x-invocation-id': 'cle1::',
|
|
},
|
|
},
|
|
]
|
|
|
|
for (const req of reqs) {
|
|
console.error('checking', req)
|
|
const res = await fetchViaHTTP(appPort, req.path, req.query, {
|
|
...withInvocationId(),
|
|
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,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/optional-ssg/[[...rest]]',
|
|
'x-now-route-matches': 'nxtPrest=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',
|
|
{ nxtPrest: '', another: 'value' },
|
|
withInvocationId({
|
|
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',
|
|
{ nxtPrest: 'index', another: 'value' },
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/api/optional/[[...rest]]',
|
|
},
|
|
})
|
|
)
|
|
|
|
const json = await res.json()
|
|
expect(json.query).toEqual({ another: 'value' })
|
|
expect(json.url).toBe('/api/optional/index?another=value')
|
|
})
|
|
|
|
it('should match the index page correctly', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/',
|
|
undefined,
|
|
withInvocationId({
|
|
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,
|
|
'/slug-1',
|
|
undefined,
|
|
withInvocationId({
|
|
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()).query).toEqual({
|
|
slug: 'slug-1',
|
|
})
|
|
|
|
const res2 = await fetchViaHTTP(
|
|
appPort,
|
|
'/[slug]',
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/[slug]',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
)
|
|
|
|
const html2 = await res2.text()
|
|
const $2 = cheerio.load(html2)
|
|
expect($2('#slug-page').text()).toBe('[slug] page')
|
|
expect(JSON.parse($2('#router').text()).query).toEqual({
|
|
slug: '[slug]',
|
|
})
|
|
})
|
|
|
|
it('should have correct asPath on dynamic SSG page correctly', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/an-ssg-path',
|
|
undefined,
|
|
withInvocationId({
|
|
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,
|
|
withInvocationId({
|
|
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 read .env files and process.env', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/api/env',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
|
|
const envVariables = await res.json()
|
|
|
|
expect(envVariables.env).not.toBeUndefined()
|
|
expect(envVariables.envProd).not.toBeUndefined()
|
|
expect(envVariables.envLocal).toBeUndefined()
|
|
expect(envVariables.envFromHost).toBe('FOOBAR')
|
|
})
|
|
|
|
describe('without minimalMode, with wasm', () => {
|
|
beforeAll(() => {
|
|
minimalMode = false
|
|
})
|
|
|
|
it('should run middleware correctly', async () => {
|
|
const standaloneDir = join(next.testDir, 'standalone')
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
'/',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
expect(res.status).toBe(200)
|
|
expect(await res.text()).toContain('index page')
|
|
|
|
if (!process.env.TEST_NODE_MIDDLEWARE) {
|
|
if (process.env.IS_TURBOPACK_TEST) {
|
|
expect(
|
|
fs.existsSync(join(standaloneDir, '.next/server/edge/chunks'))
|
|
).toBe(true)
|
|
} else {
|
|
expect(
|
|
fs.existsSync(join(standaloneDir, '.next/server/edge-chunks'))
|
|
).toBe(true)
|
|
}
|
|
}
|
|
|
|
const resImageResponse = await fetchViaHTTP(
|
|
appPort,
|
|
'/a-non-existent-page/to-test-with-middleware',
|
|
undefined,
|
|
withInvocationId()
|
|
)
|
|
|
|
expect(resImageResponse.status).toBe(200)
|
|
expect(resImageResponse.headers.get('content-type')).toBe('image/png')
|
|
})
|
|
})
|
|
|
|
it('should correctly handle a mismatch in buildIds when normalizing next data', async () => {
|
|
const res = await fetchViaHTTP(
|
|
appPort,
|
|
`/_next/data/${nanoid()}/index.json`,
|
|
undefined,
|
|
withInvocationId({
|
|
headers: {
|
|
'x-matched-path': '/[teamSlug]/[project]/[id]/[suffix]',
|
|
},
|
|
})
|
|
)
|
|
|
|
expect(res.status).toBe(404)
|
|
})
|
|
})
|