Skip to content

Commit 2a45161

Browse files
committed
tests
1 parent 103c830 commit 2a45161

6 files changed

Lines changed: 305 additions & 5 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const instant = { level: 'experimental-error' }
2+
3+
export default async function Page() {
4+
await loadOuter()
5+
return (
6+
<main>
7+
<p>
8+
This page awaits a couple blocking (dynamic) things in sequence. We
9+
should point to the first one as the cause.
10+
</p>
11+
</main>
12+
)
13+
}
14+
15+
async function loadOuter() {
16+
await new Promise((resolve) => setTimeout(resolve)) // 1 (correct)
17+
await loadInner()
18+
}
19+
20+
async function loadInner() {
21+
await new Promise((resolve) => setTimeout(resolve)) // 2 (incorrect)
22+
await new Promise((resolve) => setTimeout(resolve)) // 3 (incorrect)
23+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { cookies } from 'next/headers'
2+
3+
export const instant = { level: 'experimental-error' }
4+
5+
export default async function Layout({ children, params }) {
6+
await params // 1 (correct)
7+
await cookies() // 2 (incorrect)
8+
return (
9+
<div>
10+
<p>This layout is blocked on params.</p>
11+
{children}
12+
</div>
13+
)
14+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Instant } from 'next'
2+
import { cookies } from 'next/headers'
3+
4+
export const prefetch = 'allow-runtime'
5+
6+
export const instant: Instant = {
7+
level: 'experimental-error',
8+
unstable_samples: [{ params: { slug: '123' } }],
9+
}
10+
11+
export default async function Page() {
12+
await cookies() // Not blocking due to allow-runtime
13+
return (
14+
<main>
15+
<p>
16+
This page component is valid because it's allow-runtime, but its parent
17+
layout is static and blocked on runtime data.
18+
</p>
19+
</main>
20+
)
21+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Instant } from 'next'
2+
import { cookies } from 'next/headers'
3+
4+
export const instant: Instant = { level: 'experimental-error' }
5+
6+
export default async function Page() {
7+
await loadOuter()
8+
return (
9+
<main>
10+
<p>
11+
This page awaits a couple blocking (runtime, then dynamic) things in
12+
sequence. We should point to the first one as the cause.
13+
</p>
14+
</main>
15+
)
16+
}
17+
18+
async function loadOuter() {
19+
await cookies() // 1 (correct)
20+
await loadInner()
21+
}
22+
23+
async function loadInner() {
24+
await new Promise((resolve) => setTimeout(resolve)) // 2 (not correct, but expected)
25+
await new Promise((resolve) => setTimeout(resolve)) // 3 (incorrect)
26+
}

test/e2e/app-dir/instant-validation/app/suspense-in-root/page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,19 @@ export default async function Page() {
289289
<DebugLinks href="/suspense-in-root/disable-validation/disable-build" />
290290
</li>
291291
</ul>
292+
293+
<h2>Blocking await attribution</h2>
294+
<ul>
295+
<li>
296+
<DebugLinks href="/suspense-in-root/blocking-attribution/dynamic-then-dynamic" />
297+
</li>
298+
<li>
299+
<DebugLinks href="/suspense-in-root/blocking-attribution/runtime-in-static/123" />
300+
</li>
301+
<li>
302+
<DebugLinks href="/suspense-in-root/blocking-attribution/runtime-then-dynamic" />
303+
</li>
304+
</ul>
292305
</main>
293306
)
294307
}

test/e2e/app-dir/instant-validation/instant-validation.test.ts

Lines changed: 208 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,19 @@ describe('instant validation', () => {
6161
return next.cliOutput.slice(currentCliOutputIndex)
6262
}
6363

64-
const prerender = async (pathname: string) => {
64+
const prerender = async (
65+
pathname: string,
66+
{ debugPrerender = false }: { debugPrerender?: boolean } = {}
67+
) => {
6568
const args = [
6669
'--experimental-build-mode',
6770
'generate',
6871
'--debug-build-paths',
6972
`app${pathname}/page.tsx`,
7073
]
74+
if (debugPrerender) {
75+
args.push('--debug-prerender')
76+
}
7177
return await next.build({ args })
7278
}
7379

@@ -2455,11 +2461,11 @@ describe('instant validation', () => {
24552461
"description": "Next.js encountered uncached data in generateViewport().",
24562462
"environmentLabel": "Server",
24572463
"label": "Blocking Route",
2458-
"source": "app/suspense-in-root/head/invalid-dynamic-viewport-in-blocking-inside-static/page.tsx (6:23) @ Module.generateViewport
2459-
> 6 | export async function generateViewport(): Promise<Viewport> {
2460-
| ^",
2464+
"source": "app/suspense-in-root/head/invalid-dynamic-viewport-in-blocking-inside-static/page.tsx (7:19) @ Module.generateViewport
2465+
> 7 | await connection()
2466+
| ^",
24612467
"stack": [
2462-
"Module.generateViewport app/suspense-in-root/head/invalid-dynamic-viewport-in-blocking-inside-static/page.tsx (6:23)",
2468+
"Module.generateViewport app/suspense-in-root/head/invalid-dynamic-viewport-in-blocking-inside-static/page.tsx (7:19)",
24632469
],
24642470
}
24652471
`)
@@ -3093,6 +3099,203 @@ describe('instant validation', () => {
30933099
}
30943100
})
30953101

3102+
describe('validation failures point to the first blocking await', () => {
3103+
it('multiple dynamic awaits in sequence', async () => {
3104+
if (isNextDev) {
3105+
const browser = await navigateTo(
3106+
'/suspense-in-root/blocking-attribution/dynamic-then-dynamic'
3107+
)
3108+
await expect(browser).toDisplayCollapsedRedbox(`
3109+
{
3110+
"cause": [
3111+
{
3112+
"label": "Caused by: Instant Validation",
3113+
"source": "app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx (1:24) @ instant
3114+
> 1 | export const instant = { level: 'experimental-error' }
3115+
| ^",
3116+
"stack": [
3117+
"instant app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx (1:24)",
3118+
"Set.forEach <anonymous>",
3119+
],
3120+
},
3121+
],
3122+
"code": "E1317",
3123+
"description": "Next.js encountered uncached data during a navigation.",
3124+
"environmentLabel": "Server",
3125+
"label": "Instant",
3126+
"source": "app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx (16:9) @ loadOuter
3127+
> 16 | await new Promise((resolve) => setTimeout(resolve)) // 1 (correct)
3128+
| ^",
3129+
"stack": [
3130+
"loadOuter app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx (16:9)",
3131+
"Page app/suspense-in-root/blocking-attribution/dynamic-then-dynamic/page.tsx (4:9)",
3132+
],
3133+
}
3134+
`)
3135+
} else {
3136+
const result = await prerender(
3137+
'/suspense-in-root/blocking-attribution/dynamic-then-dynamic'
3138+
)
3139+
expect(extractBuildValidationError(result.cliOutput))
3140+
.toMatchInlineSnapshot(`
3141+
"Error: Route "/suspense-in-root/blocking-attribution/dynamic-then-dynamic": Next.js encountered uncached data during prerendering or a navigation.
3142+
3143+
\`fetch(...)\` or \`connection()\` accessed outside of \`<Suspense>\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.
3144+
3145+
Ways to fix this:
3146+
- [stream] Provide a placeholder with \`<Suspense fallback={...}>\` around the data access
3147+
https://nextjs.org/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense
3148+
- [cache] Cache the data access with \`"use cache"\`
3149+
https://nextjs.org/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data
3150+
- [block] Set \`export const instant = false\` to silence this warning and allow a blocking route
3151+
https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route
3152+
at body (<anonymous>)
3153+
at html (<anonymous>)
3154+
at a (<anonymous>)
3155+
Build-time instant validation failed for route "/suspense-in-root/blocking-attribution/dynamic-then-dynamic".
3156+
To get a more detailed stack trace and pinpoint the issue, try one of the following:
3157+
- Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/blocking-attribution/dynamic-then-dynamic" in your browser to investigate the error.
3158+
- Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.
3159+
Stopping prerender due to instant validation errors."
3160+
`)
3161+
expect(result.exitCode).toBe(1)
3162+
}
3163+
})
3164+
3165+
it('runtime data then dynamic data', async () => {
3166+
// TODO(instant-validation): Arguably, we should point to `await cookies()` first,
3167+
// but due to our the discriminated error message strategy we favor holes that are still
3168+
// present in the runtime retry render. This can be confusing if we have runtime and then dynamic data
3169+
// in the same place. At least we can assert that we're pointing to the first await that was blocking in the runtime stage.
3170+
if (isNextDev) {
3171+
const browser = await navigateTo(
3172+
'/suspense-in-root/blocking-attribution/runtime-then-dynamic'
3173+
)
3174+
await expect(browser).toDisplayCollapsedRedbox(`
3175+
{
3176+
"cause": [
3177+
{
3178+
"label": "Caused by: Instant Validation",
3179+
"source": "app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (4:33) @ instant
3180+
> 4 | export const instant: Instant = { level: 'experimental-error' }
3181+
| ^",
3182+
"stack": [
3183+
"instant app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (4:33)",
3184+
"Set.forEach <anonymous>",
3185+
],
3186+
},
3187+
],
3188+
"code": "E1317",
3189+
"description": "Next.js encountered uncached data during a navigation.",
3190+
"environmentLabel": "Server",
3191+
"label": "Instant",
3192+
"source": "app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (24:9) @ loadInner
3193+
> 24 | await new Promise((resolve) => setTimeout(resolve)) // 2 (not correct, but expected)
3194+
| ^",
3195+
"stack": [
3196+
"loadInner app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (24:9)",
3197+
"loadOuter app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (20:9)",
3198+
"Page app/suspense-in-root/blocking-attribution/runtime-then-dynamic/page.tsx (7:3)",
3199+
],
3200+
}
3201+
`)
3202+
} else {
3203+
const result = await prerender(
3204+
'/suspense-in-root/blocking-attribution/runtime-then-dynamic'
3205+
)
3206+
expect(extractBuildValidationError(result.cliOutput))
3207+
.toMatchInlineSnapshot(`
3208+
"Error: Route "/suspense-in-root/blocking-attribution/runtime-then-dynamic": Next.js encountered uncached data during prerendering or a navigation.
3209+
3210+
\`fetch(...)\` or \`connection()\` accessed outside of \`<Suspense>\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.
3211+
3212+
Ways to fix this:
3213+
- [stream] Provide a placeholder with \`<Suspense fallback={...}>\` around the data access
3214+
https://nextjs.org/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense
3215+
- [cache] Cache the data access with \`"use cache"\`
3216+
https://nextjs.org/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data
3217+
- [block] Set \`export const instant = false\` to silence this warning and allow a blocking route
3218+
https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route
3219+
at body (<anonymous>)
3220+
at html (<anonymous>)
3221+
at a (<anonymous>)
3222+
Build-time instant validation failed for route "/suspense-in-root/blocking-attribution/runtime-then-dynamic".
3223+
To get a more detailed stack trace and pinpoint the issue, try one of the following:
3224+
- Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/blocking-attribution/runtime-then-dynamic" in your browser to investigate the error.
3225+
- Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.
3226+
Stopping prerender due to instant validation errors."
3227+
`)
3228+
expect(result.exitCode).toBe(1)
3229+
}
3230+
})
3231+
3232+
it('(failing) runtime data in static segment when a runtime prefetchable segment is also present', async () => {
3233+
// TODO(instant-validation): This test is here to show a problem with our implementation.
3234+
// If an 'allow-runtime' segment is in the tree, instant-validation.tsx has to use the runtime
3235+
// stage for the `endTime` (which cuts off debug info and prevents us from pointing beyond the first blocking await).
3236+
// `endTime` is ultimately shared across the whole render, which means that we'll show "too much" for the static segment
3237+
// and end up pointing to the second await instead of the first one.
3238+
if (isNextDev) {
3239+
const browser = await navigateTo(
3240+
'/suspense-in-root/blocking-attribution/runtime-in-static/123'
3241+
)
3242+
await expect(browser).toDisplayCollapsedRedbox(`
3243+
{
3244+
"cause": [
3245+
{
3246+
"label": "Caused by: Instant Validation",
3247+
"source": "app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx (3:24) @ instant
3248+
> 3 | export const instant = { level: 'experimental-error' }
3249+
| ^",
3250+
"stack": [
3251+
"instant app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx (3:24)",
3252+
"Set.forEach <anonymous>",
3253+
],
3254+
},
3255+
],
3256+
"code": "E1319",
3257+
"description": "Next.js encountered runtime data during a navigation.",
3258+
"environmentLabel": "Server",
3259+
"label": "Instant",
3260+
"source": "app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx (7:16) @ Layout
3261+
> 7 | await cookies() // 2 (incorrect)
3262+
| ^",
3263+
"stack": [
3264+
"Layout app/suspense-in-root/blocking-attribution/runtime-in-static/[slug]/layout.tsx (7:16)",
3265+
],
3266+
}
3267+
`)
3268+
} else {
3269+
const result = await prerender(
3270+
'/suspense-in-root/blocking-attribution/runtime-in-static/[slug]'
3271+
)
3272+
expect(extractBuildValidationError(result.cliOutput))
3273+
.toMatchInlineSnapshot(`
3274+
"Error: Route "/suspense-in-root/blocking-attribution/runtime-in-static/[slug]": Next.js encountered runtime data during prerendering or a navigation.
3275+
3276+
\`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` accessed outside of \`<Suspense>\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.
3277+
3278+
Ways to fix this:
3279+
- [stream] Provide a placeholder with \`<Suspense fallback={...}>\` around the data access
3280+
https://nextjs.org/docs/messages/blocking-prerender-runtime#wrap-in-or-move-into-suspense
3281+
- [cache] For \`params\`: if the params are known, prerender them with \`generateStaticParams\`
3282+
https://nextjs.org/docs/messages/blocking-prerender-runtime#for-known-params-prerender
3283+
- [block] Set \`export const instant = false\` to silence this warning and allow a blocking route
3284+
https://nextjs.org/docs/messages/blocking-prerender-runtime#allow-blocking-route
3285+
at body (<anonymous>)
3286+
at html (<anonymous>)
3287+
at a (<anonymous>)
3288+
Build-time instant validation failed for route "/suspense-in-root/blocking-attribution/runtime-in-static/[slug]".
3289+
To get a more detailed stack trace and pinpoint the issue, try one of the following:
3290+
- Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/blocking-attribution/runtime-in-static/[slug]" in your browser to investigate the error.
3291+
- Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.
3292+
Stopping prerender due to instant validation errors."
3293+
`)
3294+
expect(result.exitCode).toBe(1)
3295+
}
3296+
})
3297+
})
3298+
30963299
describe('config depth preference', () => {
30973300
// When multiple slots have instant configs at different depths,
30983301
// the deepest config is preferred as the root cause. At equal

0 commit comments

Comments
 (0)