mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
This moves the dev-mode Cache Components validation renders off the dev server's main thread onto a worker thread, so rapid navigation no longer starves the event loop. The worker crosses into the app-page bundle exactly once, calling a new `ComponentMod.routeModule.runValidationInDev` entry that rebuilds the render context, work store, and request store from a serializable snapshot and runs the whole validation there, so the client prerender and the user's client components resolve the single app-page React instance rather than a second copy. A thin worker shell (`dev-validation-worker.ts`) plus a single-worker pool (`dev-validation-worker-pool.ts`) load the user bundle via `loadComponents`, install code-frame support for CLI output, and forward the validation errors back as Flight bytes; the main thread only delivers them to the overlay through `sendErrorsToBrowser`. The snapshot, the globalThis-symbol handoff, and the shared error-delivery helpers live in their own modules. The dev server installs the worker when `experimental.devValidationWorker` is not `false`, and `runDevValidationInBackground` uses it when present, falling back to the in-process path otherwise. A one-slot `SharedArrayBuffer` propagates a supersede abort into the worker so a newer navigation cancels an in-flight validation. The runtime bundle gains an `app-worker` entry for the worker with the `build/swc` boundary externalized for the code-frame native binding, and `patch-error-inspect.ts` now backs its code-frame renderer with a globalThis symbol so all copies of the module share it across the thread. The synthetic `bench/dev-validation` benchmark navigates back-to-back, so every click lands inside the validation window — the worst case for main-thread contention. Browser-observed navigation TTFB in that window, worker vs in-process: | Route | Worker (p50/p95/max) | In-process (p50/p95/max) | | ------ | -------------------- | ------------------------ | | client | 19 / 24 / 27 ms | 40 / 66 / 7762 ms | | server | 42 / 45 / 46 ms | 122 / 158 / 252 ms | | sprite | 109 / 117 / 169 ms | 196 / 208 / 299 ms | The steady-state difference is only tens of milliseconds; the effect that matters is the tail. In-process, a navigation that collides with an in-flight validation render can stall for seconds (this run peaked at ~7.8s on the client route, and the peak varies run to run) because a staged render does not yield until it finishes; off-thread the main thread stays free and that stall disappears. Read these as an upper bound, not a speedup that generalizes. The work moved off-thread is the validation render's CPU — bounded, route-dependent, and free of IO — so it does not grow with the main render's cost. In an app whose main render is dominated by IO the same absolute saving is a small fraction of the request, and it only appears when a navigation lands in the brief validation window; at ordinary click speed it is largely invisible. This is a dev-only responsiveness improvement whose benefit varies widely with the app and the navigation pattern. As a follow-up, the validation work that still runs on the main thread could move to the worker as well. When the main render can't be reused for validation (for example after a cache miss), the validation re-renders on the main thread to produce its inputs, resuming from the Resume Data Cache (RDC) the main render already filled, and only then hands the resulting Flight chunks to the worker. A later iteration could run those renders inside the worker too, transporting the (serializable) RDC so the worker can resume from the filled caches rather than reading them back on the main thread. closes NAR-895
624 lines
29 KiB
TypeScript
624 lines
29 KiB
TypeScript
import {
|
|
expectNoBuildValidationErrors,
|
|
extractBuildValidationError,
|
|
waitForValidation,
|
|
} from 'e2e-utils/instant-validation'
|
|
import {
|
|
openRedbox,
|
|
waitForNoErrorToast,
|
|
waitForRedbox,
|
|
} from '../../../lib/next-test-utils'
|
|
import {
|
|
createRedboxSnapshot,
|
|
type ErrorSnapshot,
|
|
type RedboxSnapshot,
|
|
} from '../../../lib/add-redbox-matchers'
|
|
import {
|
|
NO_VALIDATION_ERRORS_WAIT,
|
|
type InstantValidationCaseContext,
|
|
} from './harness.util'
|
|
|
|
export function registerClientTests(ctx: InstantValidationCaseContext) {
|
|
const {
|
|
next,
|
|
isNextDev,
|
|
isClientNav,
|
|
navigateTo,
|
|
expectNoDevValidationErrors,
|
|
getCliOutputSinceMark,
|
|
prerender,
|
|
} = ctx
|
|
|
|
describe('client components', () => {
|
|
it('unable to validate - parent suspends on client data and blocks children', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/invalid-client-data-blocks-validation'
|
|
)
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
{
|
|
"cause": [
|
|
{
|
|
"label": "Caused by: Instant Validation",
|
|
"source": "app/suspense-in-root/static/invalid-client-data-blocks-validation/page.tsx (1:24) @ instant
|
|
> 1 | export const instant = { level: 'experimental-error' }
|
|
| ^",
|
|
"stack": [
|
|
"instant app/suspense-in-root/static/invalid-client-data-blocks-validation/page.tsx (1:24)",
|
|
"Set.forEach <anonymous>",
|
|
],
|
|
},
|
|
],
|
|
"code": "E1331",
|
|
"description": "Route "/suspense-in-root/static/invalid-client-data-blocks-validation": Could not validate \`instant\` because a Client Component in a parent segment prevented the page from rendering.",
|
|
"environmentLabel": "Server",
|
|
"label": "Console Error",
|
|
"source": "app/suspense-in-root/static/invalid-client-data-blocks-validation/client.tsx (12:19) @ FetchesClientData
|
|
> 12 | const data = use(promise)
|
|
| ^",
|
|
"stack": [
|
|
"FetchesClientData app/suspense-in-root/static/invalid-client-data-blocks-validation/client.tsx (12:19)",
|
|
"Layout app/suspense-in-root/static/invalid-client-data-blocks-validation/layout.tsx (21:11)",
|
|
],
|
|
}
|
|
`)
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/invalid-client-data-blocks-validation'
|
|
)
|
|
expect(extractBuildValidationError(result.cliOutput))
|
|
.toMatchInlineSnapshot(`
|
|
"client-data-fetching-lib :: MISS my-key
|
|
client-data-fetching-lib :: MISS my-key
|
|
client-data-fetching-lib :: MISS my-key
|
|
Error: Route "/suspense-in-root/static/invalid-client-data-blocks-validation": Could not validate \`instant\` because a Client Component in a parent segment prevented the page from rendering.
|
|
at <unknown> (app/suspense-in-root/static/invalid-client-data-blocks-validation/client.tsx:6:37)
|
|
at div (<anonymous>)
|
|
at body (<anonymous>)
|
|
at html (<anonymous>)
|
|
at a (<anonymous>)
|
|
4 | import { useDataCache } from '../../../../client-data-fetching-lib/client'
|
|
5 |
|
|
> 6 | export function FetchesClientData({ children }) {
|
|
| ^
|
|
7 | const dataCache = useDataCache()
|
|
8 | const promise = dataCache.getOrLoad('my-key', async () => {
|
|
9 | await new Promise<void>((resolve) => setTimeout(resolve, 10))
|
|
Build-time instant validation failed for route "/suspense-in-root/static/invalid-client-data-blocks-validation".
|
|
To get a more detailed stack trace and pinpoint the issue, try one of the following:
|
|
- Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/static/invalid-client-data-blocks-validation" in your browser to investigate the error.
|
|
- Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.
|
|
Stopping prerender due to instant validation errors."
|
|
`)
|
|
expect(result.exitCode).toBe(1)
|
|
}
|
|
})
|
|
|
|
it('valid - parent suspends on client data but does not block children', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/valid-client-data-does-not-block-validation'
|
|
)
|
|
await expectNoDevValidationErrors(browser, await browser.url())
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/valid-client-data-does-not-block-validation'
|
|
)
|
|
expectNoBuildValidationErrors(result)
|
|
}
|
|
})
|
|
|
|
it('valid - parent uses sync IO in a client component', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/valid-client-api-in-parent/sync-io'
|
|
)
|
|
await expectNoDevValidationErrors(browser, await browser.url())
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/valid-client-api-in-parent/sync-io'
|
|
)
|
|
expectNoBuildValidationErrors(result)
|
|
}
|
|
})
|
|
|
|
it('valid - parent uses dynamic usePathname() in a client component', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/valid-client-api-in-parent/dynamic-params/123'
|
|
)
|
|
await expectNoDevValidationErrors(browser, await browser.url())
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/valid-client-api-in-parent/dynamic-params/[id]'
|
|
)
|
|
expectNoBuildValidationErrors(result)
|
|
}
|
|
})
|
|
|
|
it('valid - parent uses useSearchParams() in a client component', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/valid-client-api-in-parent/search-params'
|
|
)
|
|
await expectNoDevValidationErrors(browser, await browser.url())
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/valid-client-api-in-parent/search-params'
|
|
)
|
|
expectNoBuildValidationErrors(result)
|
|
}
|
|
})
|
|
|
|
it('valid - unguarded params in client page', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/valid-client-params/123'
|
|
)
|
|
await expectNoDevValidationErrors(browser, await browser.url())
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/valid-client-params/[slug]'
|
|
)
|
|
expectNoBuildValidationErrors(result)
|
|
}
|
|
})
|
|
|
|
it('valid - unguarded searchParams in client page', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/valid-client-search-params?query=foo'
|
|
)
|
|
await expectNoDevValidationErrors(browser, await browser.url())
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/valid-client-search-params'
|
|
)
|
|
expectNoBuildValidationErrors(result)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('client errors', () => {
|
|
function removeExpectedError(
|
|
errors: RedboxSnapshot,
|
|
shouldRemove: (error: ErrorSnapshot) => boolean
|
|
): ErrorSnapshot[] {
|
|
if (!Array.isArray(errors)) {
|
|
throw new Error('Expected to receive multiple errors to filter')
|
|
}
|
|
let found = false
|
|
const result = errors.filter((err) => {
|
|
if (shouldRemove(err)) {
|
|
found = true
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
})
|
|
if (!found) {
|
|
throw new Error(
|
|
`Did not find expected error in errors array: ${JSON.stringify(errors, null, 2)}`
|
|
)
|
|
}
|
|
return result
|
|
}
|
|
|
|
it('unable to validate - client error in parent blocks children', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/invalid-client-error-in-parent-blocks-children'
|
|
)
|
|
// We expect a collapsed redbox. We need to open it to assert on the messages.
|
|
await openRedbox(browser)
|
|
|
|
let errors = await createRedboxSnapshot(browser, next)
|
|
|
|
if (!isClientNav) {
|
|
// In SSR, we expect a "Switched to client rendering ..." error because we deliberately throw in a client component.
|
|
// However, the timing of when it appears is inconsistent -- sometimes it's before validation errors,
|
|
// and sometimes it's after.
|
|
// To avoid flakiness, we filter it out (but assert that it appears in the redbox)
|
|
errors = removeExpectedError(errors, (err) => {
|
|
return (
|
|
err.label === 'Recoverable Error' &&
|
|
err.description.startsWith(
|
|
'Switched to client rendering because the server rendering errored:\n\nNo SSR please'
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
expect(errors).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"description": "Route "/suspense-in-root/static/invalid-client-error-in-parent-blocks-children": Could not validate \`instant\` because the target segment was prevented from rendering, likely due to the following error.",
|
|
"environmentLabel": "Server",
|
|
"label": "Console Error",
|
|
"source": "app/suspense-in-root/static/invalid-client-error-in-parent-blocks-children/page.tsx (1:24) @ instant
|
|
> 1 | export const instant = { level: 'experimental-error' }
|
|
| ^",
|
|
"stack": [
|
|
"instant app/suspense-in-root/static/invalid-client-error-in-parent-blocks-children/page.tsx (1:24)",
|
|
],
|
|
},
|
|
{
|
|
"cause": [
|
|
{
|
|
"label": "Caused by: Error",
|
|
"message": "No SSR please",
|
|
"source": "app/suspense-in-root/static/invalid-client-error-in-parent-blocks-children/client.tsx (5:11) @ ErrorInSSR
|
|
> 5 | throw new Error('No SSR please')
|
|
| ^",
|
|
"stack": [
|
|
"ErrorInSSR app/suspense-in-root/static/invalid-client-error-in-parent-blocks-children/client.tsx (5:11)",
|
|
],
|
|
},
|
|
],
|
|
"code": "E1118",
|
|
"description": "An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.",
|
|
"environmentLabel": "Server",
|
|
"label": "Console Error",
|
|
"source": "app/suspense-in-root/static/invalid-client-error-in-parent-blocks-children/layout.tsx (19:11) @ Layout
|
|
> 19 | <ErrorInSSR>{children}</ErrorInSSR>
|
|
| ^",
|
|
"stack": [
|
|
"Layout app/suspense-in-root/static/invalid-client-error-in-parent-blocks-children/layout.tsx (19:11)",
|
|
],
|
|
},
|
|
]
|
|
`)
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/invalid-client-error-in-parent-blocks-children'
|
|
)
|
|
expect(extractBuildValidationError(result.cliOutput))
|
|
.toMatchInlineSnapshot(`
|
|
"Error: Route "/suspense-in-root/static/invalid-client-error-in-parent-blocks-children": Could not validate \`instant\` because the target segment was prevented from rendering, likely due to the following error.
|
|
at ignore-listed frames
|
|
Error: An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.
|
|
at <unknown> (app/suspense-in-root/static/invalid-client-error-in-parent-blocks-children/client.tsx:3:30)
|
|
at a (<anonymous>)
|
|
at div (<anonymous>)
|
|
at body (<anonymous>)
|
|
at html (<anonymous>)
|
|
at b (<anonymous>)
|
|
1 | 'use client'
|
|
2 |
|
|
> 3 | export function ErrorInSSR({ children }) {
|
|
| ^
|
|
4 | if (typeof window === 'undefined') {
|
|
5 | throw new Error('No SSR please')
|
|
6 | } {
|
|
[cause]: Error: No SSR please
|
|
at <unknown> (app/suspense-in-root/static/invalid-client-error-in-parent-blocks-children/client.tsx:5:11)
|
|
3 | export function ErrorInSSR({ children }) {
|
|
4 | if (typeof window === 'undefined') {
|
|
> 5 | throw new Error('No SSR please')
|
|
| ^
|
|
6 | }
|
|
7 | return <>{children}</>
|
|
8 | }
|
|
}
|
|
Build-time instant validation failed for route "/suspense-in-root/static/invalid-client-error-in-parent-blocks-children".
|
|
To get a more detailed stack trace and pinpoint the issue, try one of the following:
|
|
- Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/static/invalid-client-error-in-parent-blocks-children" in your browser to investigate the error.
|
|
- Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.
|
|
Stopping prerender due to instant validation errors."
|
|
`)
|
|
expect(result.exitCode).toBe(1)
|
|
}
|
|
})
|
|
|
|
it('unable to validate - client error in component from node_modules blocks children', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/invalid-error-in-node-modules-blocks-children'
|
|
)
|
|
// We expect a collapsed redbox. We need to open it to assert on the messages.
|
|
await openRedbox(browser)
|
|
|
|
let errors = await createRedboxSnapshot(browser, next)
|
|
|
|
if (!isClientNav) {
|
|
// In SSR, we expect a "Switched to client rendering ..." error because we deliberately throw in a client component.
|
|
// However, the timing of when it appears is inconsistent -- sometimes it's before validation errors,
|
|
// and sometimes it's after.
|
|
// To avoid flakiness, we filter it out (but assert that it appears in the redbox)
|
|
errors = removeExpectedError(errors, (err) => {
|
|
return (
|
|
err.label === 'Recoverable Error' &&
|
|
err.description.startsWith(
|
|
'Switched to client rendering because the server rendering errored:\n\nError from node_modules'
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
expect(errors).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"description": "Route "/suspense-in-root/static/invalid-error-in-node-modules-blocks-children": Could not validate \`instant\` because the target segment was prevented from rendering, likely due to the following error.",
|
|
"environmentLabel": "Server",
|
|
"label": "Console Error",
|
|
"source": "app/suspense-in-root/static/invalid-error-in-node-modules-blocks-children/page.tsx (1:24) @ instant
|
|
> 1 | export const instant = { level: 'experimental-error' }
|
|
| ^",
|
|
"stack": [
|
|
"instant app/suspense-in-root/static/invalid-error-in-node-modules-blocks-children/page.tsx (1:24)",
|
|
],
|
|
},
|
|
{
|
|
"cause": [
|
|
{
|
|
"label": "Caused by: Error",
|
|
"message": "Error from node_modules",
|
|
"source": null,
|
|
"stack": [],
|
|
},
|
|
],
|
|
"code": "E1118",
|
|
"description": "An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.",
|
|
"environmentLabel": "Server",
|
|
"label": "Console Error",
|
|
"source": "app/suspense-in-root/static/invalid-error-in-node-modules-blocks-children/layout.tsx (21:11) @ Layout
|
|
> 21 | <ErrorInSSRFromPackage>{children}</ErrorInSSRFromPackage>
|
|
| ^",
|
|
"stack": [
|
|
"Layout app/suspense-in-root/static/invalid-error-in-node-modules-blocks-children/layout.tsx (21:11)",
|
|
],
|
|
},
|
|
]
|
|
`)
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/invalid-error-in-node-modules-blocks-children'
|
|
)
|
|
expect(extractBuildValidationError(result.cliOutput))
|
|
.toMatchInlineSnapshot(`
|
|
"Error: Route "/suspense-in-root/static/invalid-error-in-node-modules-blocks-children": Could not validate \`instant\` because the target segment was prevented from rendering, likely due to the following error.
|
|
at ignore-listed frames
|
|
Error: An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.
|
|
at a (<anonymous>)
|
|
at div (<anonymous>)
|
|
at body (<anonymous>)
|
|
at html (<anonymous>)
|
|
at b (<anonymous>) {
|
|
[cause]: Error: Error from node_modules
|
|
at ignore-listed frames
|
|
}
|
|
Build-time instant validation failed for route "/suspense-in-root/static/invalid-error-in-node-modules-blocks-children".
|
|
To get a more detailed stack trace and pinpoint the issue, try one of the following:
|
|
- Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/static/invalid-error-in-node-modules-blocks-children" in your browser to investigate the error.
|
|
- Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.
|
|
Stopping prerender due to instant validation errors."
|
|
`)
|
|
expect(result.exitCode).toBe(1)
|
|
}
|
|
})
|
|
|
|
it('unable to validate - CSR bailout from next/dynamic blocks children', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/invalid-csr-bailout-blocks-children'
|
|
)
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
[
|
|
{
|
|
"description": "Route "/suspense-in-root/static/invalid-csr-bailout-blocks-children": Could not validate \`instant\` because the target segment was prevented from rendering, likely due to the following error.",
|
|
"environmentLabel": "Server",
|
|
"label": "Console Error",
|
|
"source": "app/suspense-in-root/static/invalid-csr-bailout-blocks-children/page.tsx (1:24) @ instant
|
|
> 1 | export const instant = { level: 'experimental-error' }
|
|
| ^",
|
|
"stack": [
|
|
"instant app/suspense-in-root/static/invalid-csr-bailout-blocks-children/page.tsx (1:24)",
|
|
],
|
|
},
|
|
{
|
|
"cause": [
|
|
{
|
|
"label": "Caused by: Error",
|
|
"message": "Bail out to client-side rendering: next/dynamic",
|
|
"source": null,
|
|
"stack": [],
|
|
},
|
|
],
|
|
"code": "E1118",
|
|
"description": "An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.",
|
|
"environmentLabel": "Server",
|
|
"label": "Console Error",
|
|
"source": "app/suspense-in-root/static/invalid-csr-bailout-blocks-children/layout.tsx (19:9) @ Layout
|
|
> 19 | <LazyClientWrapperWithNoSSR>{children}</LazyClientWrapperWithNoSSR>
|
|
| ^",
|
|
"stack": [
|
|
"Layout app/suspense-in-root/static/invalid-csr-bailout-blocks-children/layout.tsx (19:9)",
|
|
],
|
|
},
|
|
]
|
|
`)
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/invalid-csr-bailout-blocks-children'
|
|
)
|
|
expect(extractBuildValidationError(result.cliOutput))
|
|
.toMatchInlineSnapshot(`
|
|
"Error: Route "/suspense-in-root/static/invalid-csr-bailout-blocks-children": Could not validate \`instant\` because the target segment was prevented from rendering, likely due to the following error.
|
|
at ignore-listed frames
|
|
Error: An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.
|
|
at a (<anonymous>)
|
|
at b (<anonymous>)
|
|
at div (<anonymous>)
|
|
at body (<anonymous>)
|
|
at html (<anonymous>)
|
|
at c (<anonymous>) {
|
|
[cause]: Error: Bail out to client-side rendering: next/dynamic
|
|
at ignore-listed frames {
|
|
reason: 'next/dynamic',
|
|
digest: 'BAILOUT_TO_CLIENT_SIDE_RENDERING'
|
|
}
|
|
}
|
|
Build-time instant validation failed for route "/suspense-in-root/static/invalid-csr-bailout-blocks-children".
|
|
To get a more detailed stack trace and pinpoint the issue, try one of the following:
|
|
- Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/static/invalid-csr-bailout-blocks-children" in your browser to investigate the error.
|
|
- Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.
|
|
Stopping prerender due to instant validation errors."
|
|
`)
|
|
expect(result.exitCode).toBe(1)
|
|
}
|
|
})
|
|
|
|
it('unable to validate - client error from sibling of children slot without suspense', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/invalid-client-error-in-parent-sibling'
|
|
)
|
|
|
|
// Wait for validation to finish before snapshotting. Unlike the other
|
|
// client-error cases, this page also throws a client error during the
|
|
// render itself, which opens the redbox before validation reports its
|
|
// own errors. Without waiting, the snapshot can race the
|
|
// (asynchronously delivered) validation errors and capture only the
|
|
// render error.
|
|
await waitForValidation(await browser.url(), getCliOutputSinceMark)
|
|
|
|
if (isClientNav) {
|
|
// In a client navigation, the redbox will be collapsed.
|
|
await openRedbox(browser)
|
|
} else {
|
|
// In SSR, the redbox will be open due to the missing tags error.
|
|
await waitForRedbox(browser)
|
|
}
|
|
|
|
let errors = await createRedboxSnapshot(browser, next)
|
|
if (!isClientNav) {
|
|
// In SSR, we expect a "Switched to client rendering ..." error because we deliberately throw in a client component.
|
|
// However, the timing of when it appears is inconsistent -- sometimes it's before validation errors,
|
|
// and sometimes it's after.
|
|
// To avoid flakiness, we filter it out (but assert that it appears in the redbox)
|
|
errors = removeExpectedError(errors, (err) => {
|
|
return (
|
|
err.label === 'Runtime Error' &&
|
|
err.description.startsWith(
|
|
'Missing <html> and <body> tags in the root layout.'
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
expect(errors).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"description": "Route "/suspense-in-root/static/invalid-client-error-in-parent-sibling": Could not validate \`instant\` because the target segment was prevented from rendering, likely due to the following error.",
|
|
"environmentLabel": "Server",
|
|
"label": "Console Error",
|
|
"source": "app/suspense-in-root/static/invalid-client-error-in-parent-sibling/page.tsx (1:24) @ instant
|
|
> 1 | export const instant = { level: 'experimental-error' }
|
|
| ^",
|
|
"stack": [
|
|
"instant app/suspense-in-root/static/invalid-client-error-in-parent-sibling/page.tsx (1:24)",
|
|
],
|
|
},
|
|
{
|
|
"cause": [
|
|
{
|
|
"label": "Caused by: Error",
|
|
"message": "No SSR please",
|
|
"source": "app/suspense-in-root/static/invalid-client-error-in-parent-sibling/client.tsx (5:11) @ ErrorInSSR
|
|
> 5 | throw new Error('No SSR please')
|
|
| ^",
|
|
"stack": [
|
|
"ErrorInSSR app/suspense-in-root/static/invalid-client-error-in-parent-sibling/client.tsx (5:11)",
|
|
],
|
|
},
|
|
],
|
|
"code": "E1118",
|
|
"description": "An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.",
|
|
"environmentLabel": "Server",
|
|
"label": "Console Error",
|
|
"source": "app/suspense-in-root/static/invalid-client-error-in-parent-sibling/layout.tsx (20:7) @ Layout
|
|
> 20 | <ErrorInSSR />
|
|
| ^",
|
|
"stack": [
|
|
"Layout app/suspense-in-root/static/invalid-client-error-in-parent-sibling/layout.tsx (20:7)",
|
|
],
|
|
},
|
|
]
|
|
`)
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/invalid-client-error-in-parent-sibling'
|
|
)
|
|
expect(extractBuildValidationError(result.cliOutput))
|
|
.toMatchInlineSnapshot(`
|
|
"Error: Route "/suspense-in-root/static/invalid-client-error-in-parent-sibling": Could not validate \`instant\` because the target segment was prevented from rendering, likely due to the following error.
|
|
at ignore-listed frames
|
|
Error: An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.
|
|
at <unknown> (app/suspense-in-root/static/invalid-client-error-in-parent-sibling/client.tsx:5:11)
|
|
at body (<anonymous>)
|
|
at html (<anonymous>)
|
|
at a (<anonymous>)
|
|
3 | export function ErrorInSSR() {
|
|
4 | if (typeof window === 'undefined') {
|
|
> 5 | throw new Error('No SSR please')
|
|
| ^
|
|
6 | }
|
|
7 | return <div>Hello, browser!</div>
|
|
8 | } {
|
|
[cause]: Error: No SSR please
|
|
at <unknown> (app/suspense-in-root/static/invalid-client-error-in-parent-sibling/client.tsx:5:11)
|
|
3 | export function ErrorInSSR() {
|
|
4 | if (typeof window === 'undefined') {
|
|
> 5 | throw new Error('No SSR please')
|
|
| ^
|
|
6 | }
|
|
7 | return <div>Hello, browser!</div>
|
|
8 | }
|
|
}
|
|
Build-time instant validation failed for route "/suspense-in-root/static/invalid-client-error-in-parent-sibling".
|
|
To get a more detailed stack trace and pinpoint the issue, try one of the following:
|
|
- Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/static/invalid-client-error-in-parent-sibling" in your browser to investigate the error.
|
|
- Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.
|
|
Stopping prerender due to instant validation errors."
|
|
`)
|
|
expect(result.exitCode).toBe(1)
|
|
}
|
|
})
|
|
|
|
it('valid - client error from sibling of children slot with suspense', async () => {
|
|
if (isNextDev) {
|
|
const browser = await navigateTo(
|
|
'/suspense-in-root/static/valid-client-error-in-parent-does-not-block-validation'
|
|
)
|
|
await waitForValidation(await browser.url(), getCliOutputSinceMark)
|
|
if (isClientNav) {
|
|
// In a client nav, no errors should be reported.
|
|
await waitForNoErrorToast(browser, NO_VALIDATION_ERRORS_WAIT)
|
|
} else {
|
|
// In SSR, we expect to only see the error coming from react.
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
{
|
|
"description": "Switched to client rendering because the server rendering errored:
|
|
|
|
No SSR please",
|
|
"environmentLabel": null,
|
|
"label": "Recoverable Error",
|
|
"source": "app/suspense-in-root/static/valid-client-error-in-parent-does-not-block-validation/client.tsx (5:11) @ ErrorInSSR
|
|
> 5 | throw new Error('No SSR please')
|
|
| ^",
|
|
"stack": [
|
|
"ErrorInSSR app/suspense-in-root/static/valid-client-error-in-parent-does-not-block-validation/client.tsx (5:11)",
|
|
],
|
|
}
|
|
`)
|
|
}
|
|
} else {
|
|
const result = await prerender(
|
|
'/suspense-in-root/static/valid-client-error-in-parent-does-not-block-validation'
|
|
)
|
|
expectNoBuildValidationErrors(result)
|
|
}
|
|
})
|
|
})
|
|
}
|