diff --git a/CHANGELOG.md b/CHANGELOG.md index f4c3d7053925..47506e2c9b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - `[@jest/core]` Instrument significant lifecycle events with [`performance.mark()`](https://nodejs.org/docs/latest-v16.x/api/perf_hooks.html#performancemarkname-options) ([#13859](https://github.com/facebook/jest/pull/13859)) +- `[jest-message-util]` Add support for [error causes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) ### Fixes diff --git a/packages/jest-message-util/src/__tests__/messages.test.ts b/packages/jest-message-util/src/__tests__/messages.test.ts index 7eeabb8dc159..cff7b007bdd7 100644 --- a/packages/jest-message-util/src/__tests__/messages.test.ts +++ b/packages/jest-message-util/src/__tests__/messages.test.ts @@ -413,3 +413,20 @@ it('getTopFrame should return a path for mjs files', () => { expect(frame!.file).toBe(expectedFile); }); + +it('should return the error cause if there is one', () => { + const error = new Error('Test exception'); + // We need to do it this way because in oder Javascript engines the constructor does not allow supplying a cause. + error.cause = new Error('Cause Error'); + const message = formatExecError( + error, + { + rootDir: '', + testMatch: [], + }, + { + noStackTrace: false, + }, + ); + expect(message).toEqual(expect.stringContaining('Cause')); +}); diff --git a/packages/jest-message-util/src/index.ts b/packages/jest-message-util/src/index.ts index 7ef98dda1931..fcbf63041e64 100644 --- a/packages/jest-message-util/src/index.ts +++ b/packages/jest-message-util/src/index.ts @@ -122,11 +122,12 @@ function warnAboutWrongTestEnvironment(error: string, env: 'jsdom' | 'node') { // `before/after each` hooks). If it's thrown, none of the tests in the file // are executed. export const formatExecError = ( - error: Error | TestResult.SerializableError | string | undefined, + error: Error | TestResult.SerializableError | string | undefined | number, config: StackTraceConfig, options: StackTraceOptions, testPath?: string, reuseMessage?: boolean, + noTitle?: boolean, ): string => { if (!error || typeof error === 'number') { error = new Error(`Expected an Error, but "${String(error)}" was thrown`); @@ -134,6 +135,7 @@ export const formatExecError = ( } let message, stack; + let cause = ''; if (typeof error === 'string' || !error) { error || (error = 'EMPTY ERROR'); @@ -145,6 +147,27 @@ export const formatExecError = ( typeof error.stack === 'string' ? error.stack : `thrown: ${prettyFormat(error, {maxDepth: 3})}`; + if ('cause' in error) { + const prefix = '\n\nCause:\n'; + if (typeof error.cause === 'string') { + cause += `${prefix}${error.cause}`; + } else if (typeof error.cause === 'number') { + cause += `${prefix}${error.cause.toString()}`; + } else if (error.cause instanceof Error) { + const formatted = formatExecError( + error.cause, + config, + options, + testPath, + reuseMessage, + true, + ); + cause += `${prefix}${formatted}`; + } + } + } + if (cause !== '') { + cause = indentAllLines(cause); } const separated = separateMessageFromStack(stack || ''); @@ -174,13 +197,14 @@ export const formatExecError = ( let messageToUse; - if (reuseMessage) { + if (reuseMessage || noTitle) { messageToUse = ` ${message.trim()}`; } else { messageToUse = `${EXEC_ERROR_MESSAGE}\n\n${message}`; } + const title = noTitle ? '' : `${TITLE_INDENT + TITLE_BULLET}`; - return `${TITLE_INDENT + TITLE_BULLET + messageToUse + stack}\n`; + return `${title + messageToUse + stack + cause}\n`; }; const removeInternalStackEntries = (