Files
next.js/test/development/app-dir/cache-components-dev-errors/cache-components-dev-errors.test.ts
e9bc6190bd Redesign blocking route dev overlay and build errors (#92638)
### What?

Redesigns the blocking-route error overlay for instant navigation errors
with a distinct "Instant" overlay path, visual technique cards, and
updated error wording framed around navigation impact.

### Why?

The current overlay dumps every possible cause and fix in one block of
text. The new design is friendlier — amber "Instant" badge, a short
headline framed around navigation, and responsive code snippet cards
showing each fix pattern.

### Demo

- **Runtime template** (e.g. `cookies()`):
https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/26-cookies-ssr-no-instant
- **Dynamic template** (e.g. uncached `fetch`):
https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/27-fetch-ssr-no-instant

### How?

**Overlay**
- New early-return path in `errors.tsx` for `blocking-route` errors
without refinement — renders `InstantRuntimeError` with CodeFrame →
description → technique cards → CallStack → ErrorCause
- `InstantGuidance` component with responsive CSS grid of fix technique
cards (3 per variant)
- Color-coded cards with colored borders and matching highlight text
(blue, purple, red)
- "Make route params static" card (runtime only) has a dashed border
indicating it's conditional

**Build & CLI messages**
- Build output messages extracted into `blocking-route-messages.ts` and
deduplicated across `dynamic-rendering.ts`
- `dynamicOrRuntimeBodyMessage` added for build-time static validation
where the specific cause can't be pinpointed — lists all APIs
(`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`,
`connection()`)
- `isRuntimeVariant()` replaces the old `includes('cookies()')`
heuristic which broke because both templates mention `cookies()`
- `logBuildDebugHint()` extracted and shared between
`logDisallowedDynamicError` and instant validation — adds "run `next
dev`" and "`next build --debug-prerender`" hints to instant validation
build output

Results:
<img width="2094" height="1478" alt="Google Chrome 2026-04-17 16 37 00"
src="https://github.com/user-attachments/assets/04f126c5-250c-4e6e-bad0-d6960496cd13"
/>
<img width="1978" height="1512" alt="Google Chrome 2026-04-17 16 36 37"
src="https://github.com/user-attachments/assets/46a502d8-9500-4055-814b-3efb739949db"
/>

---------

Co-authored-by: Janka Uryga <[email protected]>
2026-04-17 18:43:59 +02:00

186 lines
7.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import stripAnsi from 'strip-ansi'
import { nextTestSetup } from 'e2e-utils'
import {
waitForNoRedbox,
waitForNoErrorToast,
hasErrorToast,
retry,
} from 'next-test-utils'
import { outdent } from 'outdent'
describe('Cache Components Dev Errors', () => {
const { isTurbopack, next, isRspack } = nextTestSetup({
files: __dirname,
})
it('should show a red box error on the SSR render', async () => {
const browser = await next.browser('/error')
// TODO(veil): The "Page <anonymous>" frame should be omitted.
// Interestingly, it only appears on initial load, and not when
// soft-navigating to the page (see test below).
await expect(browser).toDisplayCollapsedRedbox(`
{
"code": "E394",
"description": "Route "/error" used \`Math.random()\` before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-random",
"environmentLabel": "Server",
"label": "Console Error",
"source": "app/error/page.tsx (2:23) @ Page
> 2 | const random = Math.random()
| ^",
"stack": [
"Page app/error/page.tsx (2:23)",
"Page <anonymous>",
],
}
`)
})
it('should not show a red box error on client navigations', async () => {
const browser = await next.browser('/no-error')
await retry(async () => {
expect(await hasErrorToast(browser)).toBe(false)
})
await browser.elementByCss("[href='/error']").click()
await waitForNoErrorToast(browser)
await browser.loadPage(`${next.url}/error`)
// TODO: React should not include the anon stack in the Owner Stack.
await expect(browser).toDisplayCollapsedRedbox(`
{
"code": "E394",
"description": "Route "/error" used \`Math.random()\` before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-random",
"environmentLabel": "Server",
"label": "Console Error",
"source": "app/error/page.tsx (2:23) @ Page
> 2 | const random = Math.random()
| ^",
"stack": [
"Page app/error/page.tsx (2:23)",
"Page <anonymous>",
],
}
`)
})
it('should not log unhandled rejections for persistently thrown top-level errors', async () => {
const cliOutputLength = next.cliOutput.length
const res = await next.fetch('/top-level-error')
expect(res.status).toBe(500)
await retry(() => {
const cliOutput = stripAnsi(next.cliOutput.slice(cliOutputLength))
expect(cliOutput).toContain('GET /top-level-error 500')
})
expect(next.cliOutput.slice(cliOutputLength)).not.toContain(
'unhandledRejection'
)
})
// NOTE: when update this snapshot, use `pnpm build` in packages/next to avoid next source code get mapped to source.
it('should display error when component accessed data without suspense boundary', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/no-accessed-data')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: Route "/no-accessed-data"'
)
})
expect(stripAnsi(next.cliOutput.slice(outputIndex))).toContain(
'https://nextjs.org/docs/messages/blocking-route'
)
await expect(browser).toDisplayCollapsedRedbox(`
{
"code": "E1164",
"description": "Next.js encountered uncached data during the initial render.",
"environmentLabel": "Server",
"label": "Instant",
"source": "app/no-accessed-data/page.js (2:9) @ Page
> 2 | await new Promise((r) => setTimeout(r, 200))
| ^",
"stack": [
"Page app/no-accessed-data/page.js (2:9)",
],
}
`)
})
it('should clear segment errors after correcting them', async () => {
let browser: any
await next.patchFile(
'app/page.tsx',
outdent`
export const revalidate = 10
export default function Page() {
return (
<div>Hello World</div>
);
}
`,
async () => {
browser = await next.browser('/')
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Route segment config "revalidate" is not compatible with \`nextConfig.cacheComponents\`. Please remove it.",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.tsx (1:14)
Route segment config "revalidate" is not compatible with \`nextConfig.cacheComponents\`. Please remove it.
> 1 | export const revalidate = 10
| ^^^^^^^^^^",
"stack": [],
}
`)
} else if (isRspack) {
await expect(browser).toDisplayRedbox(`
{
"description": " ╰─▶ × Error: x Route segment config "revalidate" is not compatible with \`nextConfig.cacheComponents\`. Please remove it.",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.tsx
╰─▶ × Error: x Route segment config "revalidate" is not compatible with \`nextConfig.cacheComponents\`. Please remove it.
│ ,-[1:1]
│ 1 | export const revalidate = 10
│ : ^^^^^^^^^^
│ 2 | export default function Page() {
│ 3 | return (
│ 4 | <div>Hello World</div>
│ \`----
│",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": " x Route segment config "revalidate" is not compatible with \`nextConfig.cacheComponents\`. Please remove it.",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.tsx
Error: x Route segment config "revalidate" is not compatible with \`nextConfig.cacheComponents\`. Please remove it.
,-[1:1]
1 | export const revalidate = 10
: ^^^^^^^^^^
2 | export default function Page() {
3 | return (
4 | <div>Hello World</div>
\`----",
"stack": [],
}
`)
}
}
)
await waitForNoRedbox(browser)
})
})