mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
e0966df114
## Summary - migrate Cache Components, PPR, prefetching, prerendering, revalidation, and SSG deployment exclusions to `@force-gate` - remove the corresponding `skipDeployment` options and `skipped` control flow while preserving each suite's rationale This keeps caching-related exclusions together so their deployed semantics can be reviewed by the same owners. ## Verification - verified on the combined top-of-stack tree with `pnpm typescript` - compared ordinary-mode collection before and after: all 4,670 existing test names matched - collected all 510 affected files in deploy mode: 4,578 skipped tests, zero failures <!-- NEXT_JS_LLM -->
320 lines
9.9 KiB
TypeScript
320 lines
9.9 KiB
TypeScript
import { isNextDev, nextTestSetup } from 'e2e-utils'
|
|
import { retry } from 'next-test-utils'
|
|
import { getPrerenderOutput } from './utils'
|
|
|
|
// TODO(deploy-test-completion): Re-enable this suite in deploy mode.
|
|
// It likely asserts local CLI or runtime output that deploy tests do not expose.
|
|
// @force-gate !deploy
|
|
describe('Cache Components HTTP Access Fallback Prerender', () => {
|
|
const { next, isNextStart } = nextTestSetup({
|
|
files: __dirname + '/fixtures/http-access-fallback-prerender',
|
|
skipStart: !isNextDev,
|
|
})
|
|
|
|
let cliOutputLength: number
|
|
|
|
beforeEach(() => {
|
|
cliOutputLength = next.cliOutput.length
|
|
})
|
|
|
|
afterEach(async () => {
|
|
if (isNextStart) {
|
|
await next.stop()
|
|
}
|
|
})
|
|
|
|
const testCases: { isDebugPrerender: boolean; name: string }[] = []
|
|
|
|
if (isNextDev) {
|
|
testCases.push({ isDebugPrerender: false, name: 'Dev' })
|
|
} else {
|
|
const prerenderMode = process.env.NEXT_TEST_DEBUG_PRERENDER
|
|
if (!prerenderMode || prerenderMode === 'true') {
|
|
testCases.push({
|
|
isDebugPrerender: true,
|
|
name: 'Build With --debug-prerender',
|
|
})
|
|
}
|
|
if (!prerenderMode || prerenderMode === 'false') {
|
|
testCases.push({
|
|
isDebugPrerender: false,
|
|
name: 'Build Without --debug-prerender',
|
|
})
|
|
}
|
|
}
|
|
|
|
describe.each(testCases)('$name', ({ isDebugPrerender }) => {
|
|
beforeAll(async () => {
|
|
if (isNextStart) {
|
|
const args = ['--experimental-build-mode', 'compile']
|
|
|
|
if (isDebugPrerender) {
|
|
args.push('--debug-prerender')
|
|
}
|
|
|
|
await next.build({ args })
|
|
}
|
|
})
|
|
|
|
const prerender = async (pathname: string) => {
|
|
const args = [
|
|
'--experimental-build-mode',
|
|
'generate',
|
|
'--debug-build-paths',
|
|
`app${pathname}/page.tsx`,
|
|
]
|
|
|
|
if (isDebugPrerender) {
|
|
args.push('--debug-prerender')
|
|
}
|
|
|
|
await next.build({ args })
|
|
}
|
|
|
|
const buildPath = async (pathname: string) => {
|
|
const args = ['--debug-build-paths', `app${pathname}/page.tsx`]
|
|
|
|
if (isDebugPrerender) {
|
|
args.push('--debug-prerender')
|
|
}
|
|
|
|
await next.build({ args })
|
|
}
|
|
|
|
const expectStaticRouteArtifacts = async (route: string, status = 404) => {
|
|
const meta = JSON.parse(
|
|
await next.readFile(`.next/server/app/${route}.meta`)
|
|
)
|
|
|
|
expect(await next.readFile(`.next/server/app/${route}.html`)).toEqual(
|
|
expect.any(String)
|
|
)
|
|
expect(meta.status).toBe(status)
|
|
expect(meta.postponed).toBeUndefined()
|
|
expect(meta.segmentPaths).toContain('/_tree')
|
|
expect(
|
|
await next.readFile(
|
|
`.next/server/app/${route}.segments/_tree.segment.rsc`
|
|
)
|
|
).toEqual(expect.any(String))
|
|
}
|
|
|
|
const expectPartiallyStaticErrorArtifacts = async (
|
|
route: string,
|
|
status = 404
|
|
) => {
|
|
const meta = JSON.parse(
|
|
await next.readFile(`.next/server/app/${route}.meta`)
|
|
)
|
|
|
|
expect(await next.readFile(`.next/server/app/${route}.html`)).toEqual(
|
|
expect.any(String)
|
|
)
|
|
expect(meta.status).toBe(status)
|
|
expect(meta.postponed).toEqual(expect.any(String))
|
|
expect(meta.segmentPaths).toContain('/_tree')
|
|
expect(
|
|
await next.readFile(
|
|
`.next/server/app/${route}.segments/_tree.segment.rsc`
|
|
)
|
|
).toEqual(expect.any(String))
|
|
}
|
|
|
|
describe('notFound()', () => {
|
|
const pagePath = '/not-found/[slug]'
|
|
const visitUrl = '/not-found/not-found'
|
|
|
|
if (isNextDev) {
|
|
it('should show a collapsed redbox when not-found.tsx uses useSearchParams without Suspense', async () => {
|
|
const browser = await next.browser(visitUrl)
|
|
|
|
await expect(browser).toDisplayCollapsedRedbox(
|
|
`"Redbox did not open."`
|
|
)
|
|
})
|
|
} else {
|
|
it('should skip static shell validation during fallback recovery', async () => {
|
|
await prerender(pagePath)
|
|
|
|
const output = getPrerenderOutput(
|
|
next.cliOutput.slice(cliOutputLength),
|
|
{ isMinified: !isDebugPrerender }
|
|
)
|
|
|
|
expect(output).toMatchInlineSnapshot(`""`)
|
|
await expectStaticRouteArtifacts('not-found/not-found')
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('notFound() with root client params during recovery', () => {
|
|
const pagePath = '/not-found-use-params/[slug]'
|
|
|
|
if (!isNextDev) {
|
|
it('should fall through from the resume-abort recovery and emit static artifacts', async () => {
|
|
await prerender(pagePath)
|
|
|
|
const output = getPrerenderOutput(
|
|
next.cliOutput.slice(cliOutputLength),
|
|
{ isMinified: !isDebugPrerender }
|
|
)
|
|
|
|
expect(output).toMatchInlineSnapshot(`""`)
|
|
await expectStaticRouteArtifacts('not-found-use-params/not-found')
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('notFound() with dynamic metadata and viewport', () => {
|
|
const pagePath = '/not-found-dynamic-head/[slug]'
|
|
|
|
if (!isNextDev) {
|
|
it('should allow fallback recovery when the route opts out of static shell validation', async () => {
|
|
await prerender(pagePath)
|
|
|
|
const output = getPrerenderOutput(
|
|
next.cliOutput.slice(cliOutputLength),
|
|
{ isMinified: !isDebugPrerender }
|
|
)
|
|
|
|
expect(output).toMatchInlineSnapshot(`""`)
|
|
await expectPartiallyStaticErrorArtifacts(
|
|
'not-found-dynamic-head/not-found'
|
|
)
|
|
const prerenderedHtml = await next.readFile(
|
|
'.next/server/app/not-found-dynamic-head/not-found.html'
|
|
)
|
|
expect(prerenderedHtml).not.toContain('not-found metadata marker')
|
|
expect(prerenderedHtml).not.toContain('metadata from not-found.tsx')
|
|
expect(prerenderedHtml).not.toContain('#123456')
|
|
|
|
await next.start({ skipBuild: true })
|
|
const browser = await next.browser(
|
|
'/not-found-dynamic-head/not-found'
|
|
)
|
|
|
|
await retry(async () => {
|
|
const head = await browser.eval(() => {
|
|
return {
|
|
title: document.title,
|
|
description: document
|
|
.querySelector('meta[name="description"]')
|
|
?.getAttribute('content'),
|
|
themeColors: Array.from(
|
|
document.querySelectorAll('meta[name="theme-color"]')
|
|
).map((meta) => meta.getAttribute('content')),
|
|
}
|
|
})
|
|
|
|
expect(head.title).toBe('not-found metadata marker')
|
|
expect(head.description).toBe('metadata from not-found.tsx')
|
|
expect(head.themeColors).toContain('#123456')
|
|
expect(head.themeColors).not.toContain('black')
|
|
})
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('notFound() with static RSC data', () => {
|
|
const pagePath = '/not-found-static-flight/[slug]'
|
|
|
|
if (!isNextDev) {
|
|
it('should preserve the original static Flight data during recovery', async () => {
|
|
await prerender(pagePath)
|
|
|
|
const output = getPrerenderOutput(
|
|
next.cliOutput.slice(cliOutputLength),
|
|
{ isMinified: !isDebugPrerender }
|
|
)
|
|
|
|
expect(output).toMatchInlineSnapshot(`""`)
|
|
await expectStaticRouteArtifacts('not-found-static-flight/not-found')
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('notFound() with dynamic RSC data', () => {
|
|
const pagePath = '/not-found-dynamic-flight/[slug]'
|
|
|
|
if (!isNextDev) {
|
|
it('should preserve the original dynamic Flight data during recovery', async () => {
|
|
await prerender(pagePath)
|
|
|
|
const output = getPrerenderOutput(
|
|
next.cliOutput.slice(cliOutputLength),
|
|
{ isMinified: !isDebugPrerender }
|
|
)
|
|
|
|
expect(output).toMatchInlineSnapshot(`""`)
|
|
await expectPartiallyStaticErrorArtifacts(
|
|
'not-found-dynamic-flight/not-found'
|
|
)
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('forbidden()', () => {
|
|
const pagePath = '/forbidden/[slug]'
|
|
const visitUrl = '/forbidden/forbidden'
|
|
|
|
if (isNextDev) {
|
|
it('should show a collapsed redbox when forbidden.tsx uses useSearchParams without Suspense', async () => {
|
|
const browser = await next.browser(visitUrl)
|
|
|
|
await expect(browser).toDisplayCollapsedRedbox(
|
|
`"Redbox did not open."`
|
|
)
|
|
})
|
|
} else {
|
|
it('should skip static shell validation during fallback recovery', async () => {
|
|
await prerender(pagePath)
|
|
|
|
const output = getPrerenderOutput(
|
|
next.cliOutput.slice(cliOutputLength),
|
|
{ isMinified: !isDebugPrerender }
|
|
)
|
|
|
|
expect(output).toMatchInlineSnapshot(`""`)
|
|
await expectStaticRouteArtifacts('forbidden/forbidden', 403)
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('unauthorized()', () => {
|
|
const pagePath = '/unauthorized/[slug]'
|
|
const visitUrl = '/unauthorized/unauthorized'
|
|
|
|
if (isNextDev) {
|
|
it('should show a collapsed redbox when unauthorized.tsx uses useSearchParams without Suspense', async () => {
|
|
const browser = await next.browser(visitUrl)
|
|
|
|
await expect(browser).toDisplayCollapsedRedbox(
|
|
`"Redbox did not open."`
|
|
)
|
|
})
|
|
} else {
|
|
it('should skip static shell validation during fallback recovery', async () => {
|
|
await prerender(pagePath)
|
|
|
|
const output = getPrerenderOutput(
|
|
next.cliOutput.slice(cliOutputLength),
|
|
{ isMinified: !isDebugPrerender }
|
|
)
|
|
|
|
expect(output).toMatchInlineSnapshot(`""`)
|
|
await expectStaticRouteArtifacts('unauthorized/unauthorized', 401)
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('notFound() above the matching not-found boundary', () => {
|
|
if (!isNextDev) {
|
|
it('should emit static artifacts', async () => {
|
|
await buildPath('/not-found-above-boundary/child')
|
|
await expectStaticRouteArtifacts('not-found-above-boundary/child')
|
|
})
|
|
}
|
|
})
|
|
})
|
|
})
|