Files
next.js/test/e2e/app-dir/use-cache-hanging-inputs/use-cache-hanging-inputs.test.ts
3f4db01dbc [Cache Components] Discriminate static shell validation errors by type (#85747)
Prior to this change any "hole" in a prerender that would block the
shell was considered an error and you would be presented with a very
generic message explaining all the different ways you could have failed
this validation check.

With this change we use a new technique to validate the static shell
which can now tell the difference between waiting on uncached data or
runtime data. It also improves the heuristics around generateMetadata
and generateViewport errors.

Added new error pages for runtime sync IO and ensure we only validate
sync IO after runtime data if the page will be validating runtime
prefetches.

Restored the validation on HMR update so you can get feedback after
saving a new file.

---

We've also discovered that hanging inputs are not handled correctly.
Fixing this is non-trivial and will be done in a follow-up, so for now,
we're disabling the failing tests.

---------

Co-authored-by: Josh Story <[email protected]>
Co-authored-by: Hendrik Liebau <[email protected]>
2025-11-20 17:13:37 +01:00

198 lines
6.8 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import escapeStringRegexp from 'escape-string-regexp'
import {
getRedboxDescription,
getRedboxSource,
openRedbox,
waitForRedbox,
getRedboxTitle,
getRedboxTotalErrorCount,
} from 'next-test-utils'
import stripAnsi from 'strip-ansi'
const expectedTimeoutErrorMessage =
'Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or dynamic data were used inside "use cache".'
describe('use-cache-hanging-inputs', () => {
const { next, isNextDev, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
skipStart: process.env.NEXT_TEST_MODE !== 'dev',
})
if (skipped) {
return
}
if (isNextDev) {
// TODO(restart-on-cache-miss): reenable when fixed
describe.skip('when an uncached promise is used inside of "use cache"', () => {
it('should show an error toast after a timeout', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/uncached-promise')
// The request is pending while we stall on the hanging inputs, and
// playwright will wait for the load even before continuing. So we don't
// need to wait for the "use cache" timeout of 50 seconds here.
await openRedbox(browser)
const errorCount = await getRedboxTotalErrorCount(browser)
const errorDescription = await getRedboxDescription(browser)
const errorSource = await getRedboxSource(browser)
expect(errorCount).toBe(1)
expect(errorDescription).toBe(expectedTimeoutErrorMessage)
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
expect(errorSource).toMatchInlineSnapshot(`
"app/uncached-promise/page.tsx (10:13) @ Foo
8 | }
9 |
> 10 | const Foo = async ({ promise }) => {
| ^
11 | 'use cache'
12 |
13 | return ("
`)
expect(cliOutput).toContain(`Error: ${expectedTimeoutErrorMessage}
at Foo (app/uncached-promise/page.tsx:10:13)`)
}, 180_000)
})
// TODO(restart-on-cache-miss): reenable when fixed
describe.skip('when an uncached promise is used inside of a nested "use cache"', () => {
it('should show an error toast after a timeout', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/uncached-promise-nested')
// The request is pending while we stall on the hanging inputs, and
// playwright will wait for the load even before continuing. So we don't
// need to wait for the "use cache" timeout of 50 seconds here.
await openRedbox(browser)
const errorCount = await getRedboxTotalErrorCount(browser)
const errorDescription = await getRedboxDescription(browser)
const errorSource = await getRedboxSource(browser)
expect(errorCount).toBe(1)
expect(errorDescription).toBe(expectedTimeoutErrorMessage)
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
expect(errorSource).toMatchInlineSnapshot(`
"app/uncached-promise-nested/page.tsx (16:1) @ indirection
14 | }
15 |
> 16 | async function indirection(promise: Promise<number>) {
| ^
17 | 'use cache'
18 |
19 | return getCachedData(promise)"
`)
expect(cliOutput).toContain(`Error: ${expectedTimeoutErrorMessage}
at indirection (app/uncached-promise-nested/page.tsx:16:1)
at Page (app/uncached-promise-nested/page.tsx:23:22)`)
}, 180_000)
})
// TODO(restart-on-cache-miss): reenable when fixed
describe.skip('when a "use cache" function is closing over an uncached promise', () => {
it('should show an error toast after a timeout', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/bound-args')
// The request is pending while we stall on the hanging inputs, and
// playwright will wait for the load even before continuing. So we don't
// need to wait for the "use cache" timeout of 50 seconds here.
await openRedbox(browser)
const errorCount = await getRedboxTotalErrorCount(browser)
const errorDescription = await getRedboxDescription(browser)
const errorSource = await getRedboxSource(browser)
expect(errorCount).toBe(1)
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
expect(errorDescription).toBe(expectedTimeoutErrorMessage)
expect(errorSource).toMatchInlineSnapshot(`
"app/bound-args/page.tsx (13:15) @ Foo
11 | const uncachedDataPromise = fetchUncachedData()
12 |
> 13 | const Foo = async () => {
| ^
14 | 'use cache'
15 |
16 | return ("
`)
expect(cliOutput).toContain(`Error: ${expectedTimeoutErrorMessage}
at Foo (app/bound-args/page.tsx:13:15)`)
}, 180_000)
})
describe('when an error is thrown', () => {
it('should show an error overlay with only one error', async () => {
const browser = await next.browser('/error')
await waitForRedbox(browser)
const count = await getRedboxTotalErrorCount(browser)
const title = await getRedboxTitle(browser)
const description = await getRedboxDescription(browser)
expect({ count, title, description }).toEqual({
count: 1,
// TODO(restart-on-cache-miss): fix environment labelling
// title: 'Runtime Error\nCache',
title: 'Runtime Error\nPrerender',
description: 'kaputt!',
})
})
})
} else {
// TODO: Be more precise about the expected error messages and stacks.
it('should fail the build with errors after a timeout', async () => {
const { cliOutput } = await next.build()
expect(cliOutput).toInclude(createExpectedBuildErrorMessage('/error'))
expect(cliOutput).toInclude('Error: kaputt!')
expect(cliOutput).toIncludeRepeated(
escapeStringRegexp(expectedTimeoutErrorMessage),
4
)
expect(cliOutput).toInclude(
createExpectedBuildErrorMessage('/bound-args')
)
expect(cliOutput).toInclude(
createExpectedBuildErrorMessage('/transformed-params/[slug]')
)
expect(cliOutput).toInclude(
createExpectedBuildErrorMessage('/uncached-promise')
)
expect(cliOutput).toInclude(
createExpectedBuildErrorMessage('/uncached-promise-nested')
)
}, 180_000)
}
})
function createExpectedBuildErrorMessage(pathname: string) {
return `Error occurred prerendering page "${pathname}". Read more: https://nextjs.org/docs/messages/prerender-error`
}