rsnext/test/e2e/app-dir/errors/index.test.ts
Jiachi Liu 92e4a4b78c
Associate server error digest with browser logged one (#61592)
### What

#### Core
This PR respect the error's digest when recieves new error occurred from
server side, and it will be logged into client on production with the
same `digest` property.
If we discover the original RSC error in SSR error handler, retrieve the
original error

#### Tests

* Move the errors related tests from `test/e2e/app-dir/app` to a
separate test suite `test/e2e/app-dir/errors`
* Add a new test case for logging the original RSC error
* Add a new test case for logging the original Server Action error


### Why

This will help associate the `digest` property of the errors logged from
client with the actual generated server errors. Previously they're
different as we might re-compute the digest proper in handler that react
server renderer thinks it's a new error, which causes we have 2
different errors logged on server side, and 1 logged on client side. The
one on client side can associate to the server errors but it's from
react renderer which is not the original error.

Closes NEXT-2094
Fixes #60684
2024-02-06 13:39:12 +01:00

176 lines
6.9 KiB
TypeScript

import { createNextDescribe } from 'e2e-utils'
import { getRedboxHeader, hasRedbox, retry } from 'next-test-utils'
createNextDescribe(
'app-dir - errors',
{
files: __dirname,
skipDeployment: true,
},
({ next, isNextDev, isNextStart }) => {
describe('error component', () => {
it('should trigger error component when an error happens during rendering', async () => {
const browser = await next.browser('/client-component')
await browser.elementByCss('#error-trigger-button').click()
if (isNextDev) {
// TODO: investigate desired behavior here as it is currently
// minimized by default
// expect(await hasRedbox(browser)).toBe(true)
// expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
await browser
expect(
await browser
.waitForElementByCss('#error-boundary-message')
.elementByCss('#error-boundary-message')
.text()
).toBe('An error occurred: this is a test')
}
})
it('should trigger error component when an error happens during server components rendering', async () => {
const browser = await next.browser('/server-component')
if (isNextDev) {
expect(
await browser
.waitForElementByCss('#error-boundary-message')
.elementByCss('#error-boundary-message')
.text()
).toBe('this is a test')
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
// Digest of the error message should be stable.
).not.toBe('')
// TODO-APP: ensure error overlay is shown for errors that happened before/during hydration
// expect(await hasRedbox(browser)).toBe(true)
// expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
await browser
expect(
await browser.waitForElementByCss('#error-boundary-message').text()
).toBe(
'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'
)
expect(
await browser.waitForElementByCss('#error-boundary-digest').text()
// Digest of the error message should be stable.
).not.toBe('')
}
})
it('should use default error boundary for prod and overlay for dev when no error component specified', async () => {
const browser = await next.browser('/global-error-boundary/client')
await browser.elementByCss('#error-trigger-button').click()
if (isNextDev) {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(/this is a test/)
} else {
expect(
await browser.waitForElementByCss('body').elementByCss('h2').text()
).toBe(
'Application error: a client-side exception has occurred (see the browser console for more information).'
)
}
})
it('should display error digest for error in server component with default error boundary', async () => {
const browser = await next.browser('/global-error-boundary/server')
if (isNextDev) {
expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxHeader(browser)).toMatch(/custom server error/)
} else {
expect(
await browser.waitForElementByCss('body').elementByCss('h2').text()
).toBe(
'Application error: a server-side exception has occurred (see the server logs for more information).'
)
expect(
await browser.waitForElementByCss('body').elementByCss('p').text()
).toMatch(/Digest: \w+/)
}
})
// 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 browser = await next.browser('/ssr-error-client-component')
const logs = await browser.log()
const errors = logs
.filter((x) => x.source === 'error')
.map((x) => x.message)
.join('\n')
expect(errors).toInclude('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 = 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 = 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}'`)
})
}
})
}
)