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`
}