Files
next.js/test/e2e/app-dir/errors/index.test.ts

416 lines
15 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 { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import stripAnsi from 'strip-ansi'
describe('app-dir - errors', () => {
const { next, isNextDev, isNextStart, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})
if (skipped) {
return
}
describe('error component', () => {
it('should trigger error component when an error happens during rendering', async () => {
const pageErrors: unknown[] = []
const browser = await next.browser('/client-component', {
beforePageLoad: (page) => {
page.on('pageerror', (error: unknown) => {
pageErrors.push(error)
})
},
})
await browser.elementByCss('#error-trigger-button').click()
if (isNextDev) {
// TODO: investigate desired behavior here as it is currently
// minimized by default
// await waitForRedbox(browser)
// expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
expect(
await browser.waitForElementByCss('#error-boundary-message').text()
).toBe('An error occurred: this is a test')
}
// Handled by custom error boundary.
expect(pageErrors).toEqual([])
})
it('should trigger error component when an error happens during server components rendering', async () => {
const pageErrors: unknown[] = []
const browser = await next.browser('/server-component', {
beforePageLoad: (page) => {
page.on('pageerror', (error: unknown) => {
pageErrors.push(error)
})
},
})
expect(
await browser.waitForElementByCss('#error-boundary-message').text()
).toBe(
isNextDev
? 'this is a test'
: 'Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.'
)
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
// Digest of the error message should be stable.
).not.toBe('')
if (isNextDev) {
// TODO-APP: ensure error overlay is shown for errors that happened before/during hydration
// await waitForRedbox(browser)
// expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
}
// Handled by custom error boundary.
expect(pageErrors).toEqual([])
})
it('should preserve custom digests', async () => {
const browser = await next.browser('/server-component/custom-digest')
expect(
await browser.waitForElementByCss('#error-boundary-message').text()
).toBe(
isNextDev
? 'this is a test'
: 'Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.'
)
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
).toBe('custom')
expect(stripAnsi(next.cliOutput)).toEqual(
expect.stringMatching(
isNextDev
? /Error: this is a test.*digest: 'custom'/s
: /Error: this is a test.*digest: 'custom'/s
)
)
})
it('should trigger error component when undefined is thrown during server components rendering', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/server-component/throw-undefined')
expect(
await browser.waitForElementByCss('#error-boundary-message').text()
).toBe(
isNextDev
? 'undefined'
: 'Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.'
)
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
// Digest of the error message should be stable.
).not.toBe('')
const cleanCliOutput = stripAnsi(
next.cliOutput.slice(outputIndex)
).replaceAll(/digest: '\d+(@E\d+)'/g, "digest: '<digest>$1'")
if (isNextDev) {
expect(cleanCliOutput).toEqual(
expect.stringMatching(
/Error: An undefined error was thrown.*digest: '<digest>@E98'/s
)
)
} else {
expect(cleanCliOutput).toMatchInlineSnapshot(`
"⨯ Error: undefined
at stringify (<anonymous>) {
digest: '<digest>@E394'
}
"
`)
}
})
it('should trigger error component when null is thrown during server components rendering', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/server-component/throw-null')
expect(
await browser.waitForElementByCss('#error-boundary-message').text()
).toBe(
isNextDev
? 'null'
: 'Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.'
)
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
// Digest of the error message should be stable.
).not.toBe('')
const cleanCliOutput = stripAnsi(
next.cliOutput.slice(outputIndex)
).replaceAll(/digest: '\d+(@E\d+)'/g, "digest: '<digest>$1'")
if (isNextDev) {
expect(cleanCliOutput).toEqual(
expect.stringMatching(
/Error: A null error was thrown.*digest: '<digest>@E336'/s
)
)
} else {
expect(cleanCliOutput).toMatchInlineSnapshot(`
"⨯ Error: null
at stringify (<anonymous>) {
digest: '<digest>@E394'
}
"
`)
}
})
it('should trigger error component when a string is thrown during server components rendering', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/server-component/throw-string')
expect(
await browser.waitForElementByCss('#error-boundary-message').text()
).toBe(
isNextDev
? 'this is a test'
: 'Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.'
)
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
// Digest of the error message should be stable.
).not.toBe('')
const cleanCliOutput = stripAnsi(
next.cliOutput.slice(outputIndex)
).replaceAll(/digest: '\d+'/g, "digest: '<digest>'")
if (isNextDev) {
expect(cleanCliOutput).toEqual(
expect.stringMatching(/Error: this is a test.*digest: '<digest>'/s)
)
} else {
expect(cleanCliOutput).toMatchInlineSnapshot(`
"⨯ Error: this is a test
at stringify (<anonymous>) {
digest: '<digest>'
}
"
`)
}
})
it('should use default error boundary for prod and overlay for dev when no error component specified', async () => {
const pageErrors: unknown[] = []
const browser = await next.browser('/global-error-boundary/client', {
beforePageLoad: (page) => {
page.on('pageerror', (error: unknown) => {
pageErrors.push(error)
})
},
})
await browser.elementByCss('#error-trigger-button').click()
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
{
"description": "this is a test",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/global-error-boundary/client/page.js (8:11) @ Page
> 8 | throw new Error('this is a test')
| ^",
"stack": [
"Page app/global-error-boundary/client/page.js (8:11)",
],
}
`)
} else {
expect(
await browser.waitForElementByCss('body').elementByCss('h1').text()
).toBe('This page couldn\u2019t load')
}
expect(pageErrors).toEqual([
expect.objectContaining({
message: 'this is a test',
}),
])
})
it('should display error digest for error in server component with default error boundary', async () => {
const pageErrors: unknown[] = []
const browser = await next.browser('/global-error-boundary/server', {
beforePageLoad: (page) => {
page.on('pageerror', (error: unknown) => {
pageErrors.push(error)
})
},
})
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
{
"description": "custom server error",
"environmentLabel": "Server",
"label": "Runtime Error",
"source": "app/global-error-boundary/server/page.js (2:9) @ Page
> 2 | throw Error('custom server error')
| ^",
"stack": [
"Page app/global-error-boundary/server/page.js (2:9)",
],
}
`)
} else {
expect(
await browser.waitForElementByCss('body').elementByCss('h1').text()
).toBe('This page couldn\u2019t load')
// Check digest is displayed
const bodyText = await browser.waitForElementByCss('body').text()
expect(bodyText).toMatch(/ERROR \w+/)
}
expect(pageErrors).toEqual([
expect.objectContaining({
message: isNextDev
? 'custom server error'
: 'Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.',
}),
])
})
// production tests
if (isNextStart) {
it('should allow resetting error boundary', async () => {
const browser = await next.browser('/client-component')
// Try triggering and resetting a few times in a row
for (let i = 0; i < 5; i++) {
await browser
.elementByCss('#error-trigger-button')
.click()
.waitForElementByCss('#error-boundary-message')
expect(
await browser.elementByCss('#error-boundary-message').text()
).toBe('An error occurred: this is a test')
await browser
.elementByCss('#reset')
.click()
.waitForElementByCss('#error-trigger-button')
expect(
await browser.elementByCss('#error-trigger-button').text()
).toBe('Trigger Error!')
}
})
it('should hydrate empty shell to handle server-side rendering errors', async () => {
const pageErrors: unknown[] = []
await next.browser('/ssr-error-client-component', {
beforePageLoad: (page) => {
page.on('pageerror', (error: unknown) => {
pageErrors.push(error)
})
},
})
expect(pageErrors).toEqual([
expect.objectContaining({ message: 'Error during SSR' }),
])
})
it('should log the original RSC error trace in production', async () => {
const logIndex = next.cliOutput.length
const browser = await next.browser('/server-component')
const digest = await browser
.waitForElementByCss('#error-boundary-digest')
.text()
const output = stripAnsi(next.cliOutput.slice(logIndex))
// Log the original rsc error trace
expect(output).toContain('Error: this is a test')
// Does not include the react renderer error for server actions
expect(output).not.toContain(
'Error: An error occurred in the Server Components render'
)
expect(output).toContain(`digest: '${digest}'`)
})
it('should log the original Server Actions error trace in production', async () => {
const logIndex = next.cliOutput.length
const browser = await next.browser('/server-actions')
// trigger server action
await browser.elementByCss('#button').click()
// wait for response
let digest
await retry(async () => {
digest = await browser.waitForElementByCss('#digest').text()
expect(digest).toMatch(/\d+/)
})
const output = stripAnsi(next.cliOutput.slice(logIndex))
// Log the original rsc error trace
expect(output).toContain('Error: server action test error')
// Does not include the react renderer error for server actions
expect(output).not.toContain(
'Error: An error occurred in the Server Components render'
)
expect(output).toContain(`digest: '${digest}'`)
})
}
describe('unstable_retry', () => {
afterEach(async () => {
// Always restore __nextTestRecover so it doesn't leak between tests
await next.fetch('/server-component/recover/set-recover?enabled=false')
})
it('should recover Server Component error after unstable_retry', async () => {
const browser = await next.browser('/server-component/recover')
expect(
await browser.elementByCss('#error-boundary-message').text()
).toBe(
isNextDev
? 'this is a test'
: 'Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.'
)
// Enable recovery via globalThis.__nextTestRecover
await next.fetch('/server-component/recover/set-recover')
await browser
.elementByCss('#retry')
.click()
.waitForElementByCss('#recover')
expect(await browser.elementByCss('#recover').text()).toBe('Recovered')
})
it('should recover Client Component error after unstable_retry', async () => {
const browser = await next.browser('/client-component')
// Try triggering and retrying a few times in a row
for (let i = 0; i < 5; i++) {
await browser
.elementByCss('#error-trigger-button')
.click()
.waitForElementByCss('#error-boundary-message')
expect(
await browser.elementByCss('#error-boundary-message').text()
).toBe('An error occurred: this is a test')
await browser
.elementByCss('#retry')
.click()
.waitForElementByCss('#error-trigger-button')
expect(
await browser.elementByCss('#error-trigger-button').text()
).toBe('Trigger Error!')
}
})
})
})
})