From eccb9b0b2903bb1ab138ead30458c4113473dfa7 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Fri, 24 Feb 2023 08:23:17 +0100 Subject: [PATCH] feat(jest-message-util): Add support for Error cause in `test` and `it` (#13935) --- CHANGELOG.md | 1 + .../__snapshots__/failures.test.ts.snap | 124 ++++++++++++++ e2e/__tests__/failures.test.ts | 18 ++ e2e/failures/__tests__/errorWithCause.test.js | 45 +++++ .../errorWithCauseInDescribe.test.js | 33 ++++ .../transform-esm-testrunner/test-runner.mjs | 1 + .../transform-testrunner/test-runner.ts | 1 + .../__snapshots__/messages.test.ts.snap | 50 +++++- .../src/__tests__/messages.test.ts | 154 +++++++++++++++++- packages/jest-message-util/src/index.ts | 80 +++++++-- 10 files changed, 487 insertions(+), 20 deletions(-) create mode 100644 e2e/failures/__tests__/errorWithCause.test.js create mode 100644 e2e/failures/__tests__/errorWithCauseInDescribe.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ca0f7ac92736..f93141244e99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `[jest-config]` Add `openHandlesTimeout` option to configure possible open handles warning. ([#13875](https://github.com/facebook/jest/pull/13875)) - `[@jest/create-cache-key-function]` Allow passing `length` argument to `createCacheKey()` function and set its default value to `16` on Windows ([#13827](https://github.com/facebook/jest/pull/13827)) - `[jest-message-util]` Add support for [AggregateError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError) ([#13946](https://github.com/facebook/jest/pull/13946) & [#13947](https://github.com/facebook/jest/pull/13947)) +- `[jest-message-util]` Add support for [Error causes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) in `test` and `it` ([#13935](https://github.com/facebook/jest/pull/13935)) - `[jest-worker]` Add `start` method to worker farms ([#13937](https://github.com/facebook/jest/pull/13937)) ### Fixes diff --git a/e2e/__tests__/__snapshots__/failures.test.ts.snap b/e2e/__tests__/__snapshots__/failures.test.ts.snap index 4e71842dc24b..4a3f562eb25f 100644 --- a/e2e/__tests__/__snapshots__/failures.test.ts.snap +++ b/e2e/__tests__/__snapshots__/failures.test.ts.snap @@ -404,6 +404,130 @@ exports[`works with async failures 1`] = ` at Object.test (__tests__/asyncFailures.test.js:22:1)" `; +exports[`works with error with cause 1`] = ` +"FAIL __tests__/errorWithCause.test.js + ✕ error with cause in test + describe block + ✕ error with cause in describe/it + ✕ error with string cause in describe/it + + ● error with cause in test + + error during f + + 10 | + 11 | function buildErrorWithCause(message: string, opts: {cause: unknown}): Error { + > 12 | const error = new Error(message, opts); + | ^ + 13 | if (opts.cause !== error.cause) { + 14 | // Error with cause not supported in legacy versions of node, we just polyfill it + 15 | Object.assign(error, opts); + + at buildErrorWithCause (__tests__/errorWithCause.test.js:12:17) + at buildErrorWithCause (__tests__/errorWithCause.test.js:27:11) + at Object.f (__tests__/errorWithCause.test.js:32:3) + + Cause: + error during g + + 19 | + 20 | function g() { + > 21 | throw new Error('error during g'); + | ^ + 22 | } + 23 | function f() { + 24 | try { + + at g (__tests__/errorWithCause.test.js:21:9) + at g (__tests__/errorWithCause.test.js:25:5) + at Object.f (__tests__/errorWithCause.test.js:32:3) + + ● describe block › error with cause in describe/it + + error during f + + 10 | + 11 | function buildErrorWithCause(message: string, opts: {cause: unknown}): Error { + > 12 | const error = new Error(message, opts); + | ^ + 13 | if (opts.cause !== error.cause) { + 14 | // Error with cause not supported in legacy versions of node, we just polyfill it + 15 | Object.assign(error, opts); + + at buildErrorWithCause (__tests__/errorWithCause.test.js:12:17) + at buildErrorWithCause (__tests__/errorWithCause.test.js:27:11) + at Object.f (__tests__/errorWithCause.test.js:37:5) + + Cause: + error during g + + 19 | + 20 | function g() { + > 21 | throw new Error('error during g'); + | ^ + 22 | } + 23 | function f() { + 24 | try { + + at g (__tests__/errorWithCause.test.js:21:9) + at g (__tests__/errorWithCause.test.js:25:5) + at Object.f (__tests__/errorWithCause.test.js:37:5) + + ● describe block › error with string cause in describe/it + + with string cause + + 10 | + 11 | function buildErrorWithCause(message: string, opts: {cause: unknown}): Error { + > 12 | const error = new Error(message, opts); + | ^ + 13 | if (opts.cause !== error.cause) { + 14 | // Error with cause not supported in legacy versions of node, we just polyfill it + 15 | Object.assign(error, opts); + + at buildErrorWithCause (__tests__/errorWithCause.test.js:12:17) + at Object.buildErrorWithCause (__tests__/errorWithCause.test.js:41:11) + + Cause: + here is the cause" +`; + +exports[`works with error with cause thrown outside tests 1`] = ` +"FAIL __tests__/errorWithCauseInDescribe.test.js + ● Test suite failed to run + + error during f + + 10 | + 11 | function buildErrorWithCause(message: string, opts: {cause: unknown}): Error { + > 12 | const error = new Error(message, opts); + | ^ + 13 | if (opts.cause !== error.cause) { + 14 | // Error with cause not supported in legacy versions of node, we just polyfill it + 15 | Object.assign(error, opts); + + at buildErrorWithCause (__tests__/errorWithCauseInDescribe.test.js:12:17) + at buildErrorWithCause (__tests__/errorWithCauseInDescribe.test.js:27:11) + at f (__tests__/errorWithCauseInDescribe.test.js:32:3) + at Object.describe (__tests__/errorWithCauseInDescribe.test.js:31:1) + + Cause: + error during g + + 19 | + 20 | function g() { + > 21 | throw new Error('error during g'); + | ^ + 22 | } + 23 | function f() { + 24 | try { + + at g (__tests__/errorWithCauseInDescribe.test.js:21:9) + at g (__tests__/errorWithCauseInDescribe.test.js:25:5) + at f (__tests__/errorWithCauseInDescribe.test.js:32:3) + at Object.describe (__tests__/errorWithCauseInDescribe.test.js:31:1)" +`; + exports[`works with node assert 1`] = ` "FAIL __tests__/assertionError.test.js ✕ assert diff --git a/e2e/__tests__/failures.test.ts b/e2e/__tests__/failures.test.ts index ca4c1cdd12c0..98f72a0257f3 100644 --- a/e2e/__tests__/failures.test.ts +++ b/e2e/__tests__/failures.test.ts @@ -6,6 +6,7 @@ */ import * as path from 'path'; +import {isJestJasmineRun} from '@jest/test-utils'; import {extractSummary, runYarnInstall} from '../Utils'; import runJest from '../runJest'; @@ -93,6 +94,23 @@ test('works with snapshot failures with hint', () => { ).toMatchSnapshot(); }); +(isJestJasmineRun() ? test.skip : test)('works with error with cause', () => { + const {stderr} = runJest(dir, ['errorWithCause.test.js']); + const summary = normalizeDots(cleanStderr(stderr)); + + expect(summary).toMatchSnapshot(); +}); + +(isJestJasmineRun() ? test.skip : test)( + 'works with error with cause thrown outside tests', + () => { + const {stderr} = runJest(dir, ['errorWithCauseInDescribe.test.js']); + const summary = normalizeDots(cleanStderr(stderr)); + + expect(summary).toMatchSnapshot(); + }, +); + test('errors after test has completed', () => { const {stderr} = runJest(dir, ['errorAfterTestComplete.test.js']); diff --git a/e2e/failures/__tests__/errorWithCause.test.js b/e2e/failures/__tests__/errorWithCause.test.js new file mode 100644 index 000000000000..4ff87c40f681 --- /dev/null +++ b/e2e/failures/__tests__/errorWithCause.test.js @@ -0,0 +1,45 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +function buildErrorWithCause(message: string, opts: {cause: unknown}): Error { + const error = new Error(message, opts); + if (opts.cause !== error.cause) { + // Error with cause not supported in legacy versions of node, we just polyfill it + Object.assign(error, opts); + } + return error; +} + +function g() { + throw new Error('error during g'); +} +function f() { + try { + g(); + } catch (err) { + throw buildErrorWithCause('error during f', {cause: err}); + } +} + +test('error with cause in test', () => { + f(); +}); + +describe('describe block', () => { + it('error with cause in describe/it', () => { + f(); + }); + + it('error with string cause in describe/it', () => { + throw buildErrorWithCause('with string cause', { + cause: 'here is the cause', + }); + }); +}); diff --git a/e2e/failures/__tests__/errorWithCauseInDescribe.test.js b/e2e/failures/__tests__/errorWithCauseInDescribe.test.js new file mode 100644 index 000000000000..e58731e43229 --- /dev/null +++ b/e2e/failures/__tests__/errorWithCauseInDescribe.test.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +function buildErrorWithCause(message: string, opts: {cause: unknown}): Error { + const error = new Error(message, opts); + if (opts.cause !== error.cause) { + // Error with cause not supported in legacy versions of node, we just polyfill it + Object.assign(error, opts); + } + return error; +} + +function g() { + throw new Error('error during g'); +} +function f() { + try { + g(); + } catch (err) { + throw buildErrorWithCause('error during f', {cause: err}); + } +} + +describe('error with cause in describe', () => { + f(); +}); diff --git a/e2e/transform/transform-esm-testrunner/test-runner.mjs b/e2e/transform/transform-esm-testrunner/test-runner.mjs index 9f5e016375bd..bd401cd4a800 100644 --- a/e2e/transform/transform-esm-testrunner/test-runner.mjs +++ b/e2e/transform/transform-esm-testrunner/test-runner.mjs @@ -23,6 +23,7 @@ export default async function testRunner( { ancestorTitles: [], duration: 2, + failureDetails: [], failureMessages: [], fullName: 'sample test', location: null, diff --git a/e2e/transform/transform-testrunner/test-runner.ts b/e2e/transform/transform-testrunner/test-runner.ts index ab90debd3e30..4d5ee7302f76 100644 --- a/e2e/transform/transform-testrunner/test-runner.ts +++ b/e2e/transform/transform-testrunner/test-runner.ts @@ -24,6 +24,7 @@ export default async function testRunner( { ancestorTitles: [], duration: 2, + failureDetails: [], failureMessages: [], fullName: 'sample test', location: null, diff --git a/packages/jest-message-util/src/__tests__/__snapshots__/messages.test.ts.snap b/packages/jest-message-util/src/__tests__/__snapshots__/messages.test.ts.snap index 2fcb42f8fee4..1daf195036db 100644 --- a/packages/jest-message-util/src/__tests__/__snapshots__/messages.test.ts.snap +++ b/packages/jest-message-util/src/__tests__/__snapshots__/messages.test.ts.snap @@ -40,6 +40,46 @@ exports[`formatStackTrace prints code frame and stacktrace 1`] = ` " `; +exports[`formatStackTrace should properly handle deeply nested causes 1`] = ` +" Error with cause test + + intercepted by f + + at f (cause.test.js:15:11) + at Object.f (cause.test.js:20:5) + + Cause: + intercepted by g + + at g (cause.test.js:8:11) + at g (cause.test.js:13:5) + at Object.f (cause.test.js:20:5) + + Cause: + boom + + at h (cause.test.js:2:9) + at h (cause.test.js:6:5) + at g (cause.test.js:13:5) + at Object.f (cause.test.js:20:5) +" +`; + +exports[`formatStackTrace should properly handle string causes 1`] = ` +" Error with string cause test + + boom + + at f (cause.test.js:15:11) + at Object.f (cause.test.js:20:5) + + Cause: + string cause + + +" +`; + exports[`formatStackTrace should strip node internals 1`] = ` " Unix test @@ -76,16 +116,16 @@ exports[`on node >=15.0.0 should return the inner errors of an AggregateError 1` AggregateError: - at Object. (packages/jest-message-util/src/__tests__/messages.test.ts:440:22) + at Object. (packages/jest-message-util/src/__tests__/messages.test.ts:588:22) Errors contained in AggregateError: Err 1 - at Object. (packages/jest-message-util/src/__tests__/messages.test.ts:441:7) + at Object. (packages/jest-message-util/src/__tests__/messages.test.ts:589:7) Err 2 - at Object. (packages/jest-message-util/src/__tests__/messages.test.ts:442:7) + at Object. (packages/jest-message-util/src/__tests__/messages.test.ts:590:7) " `; @@ -137,12 +177,12 @@ exports[`should return the error cause if there is one 1`] = ` Test exception - at Object. (packages/jest-message-util/src/__tests__/messages.test.ts:419:17) + at Object. (packages/jest-message-util/src/__tests__/messages.test.ts:567:17) Cause: Cause Error - at Object. (packages/jest-message-util/src/__tests__/messages.test.ts:422:17) + at Object. (packages/jest-message-util/src/__tests__/messages.test.ts:570:17) " `; diff --git a/packages/jest-message-util/src/__tests__/messages.test.ts b/packages/jest-message-util/src/__tests__/messages.test.ts index f76f6f7d7e34..9916b9c58096 100644 --- a/packages/jest-message-util/src/__tests__/messages.test.ts +++ b/packages/jest-message-util/src/__tests__/messages.test.ts @@ -36,6 +36,8 @@ const unixStackTrace = at Object.it (build/__tests__/messages-test.js:45:41) at Object. (../jest-jasmine2/build/jasmine-pit.js:35:32) at attemptAsync (../jest-jasmine2/build/jasmine-2.4.1.js:1919:24)`; +const unixError = new Error(unixStackTrace.replace(/\n\s*at [\s\s]*/m, '')); +unixError.stack = unixStackTrace; const assertionStack = ' ' + @@ -57,6 +59,10 @@ const assertionStack = at process._tickCallback (internal/process/next_tick.js:188:7) at internal/process/next_tick.js:188:7 `; +const assertionError = new Error( + assertionStack.replace(/\n\s*at [\s\s]*/m, ''), +); +assertionError.stack = assertionStack; const vendorStack = ' ' + @@ -84,6 +90,90 @@ const babelStack = \u001b[90m 22 | \u001b[39m )\u001b[33m;\u001b[39m \u001b[90m 23 | \u001b[39m } \u001b[36melse\u001b[39m \u001b[36mif\u001b[39m (\u001b[36mtypeof\u001b[39m render \u001b[33m!==\u001b[39m \u001b[32m'function'\u001b[39m) {\u001b[0m `; +const babelError = new Error(babelStack.replace(/\n\s*at [\s\s]*/m, '')); +babelError.stack = babelStack; + +function buildErrorWithCause(message: string, opts: {cause: unknown}): Error { + const error = new Error(message, opts); + if (opts.cause !== error.cause) { + // Error with cause not supported in legacy versions of node, we just polyfill it + Object.assign(error, opts); + } + return error; +} + +const errorWithCauseNestedNested = new Error('boom'); +errorWithCauseNestedNested.stack = `Error: boom + at h (cause.test.js:2:9) + at h (cause.test.js:6:5) + at g (cause.test.js:13:5) + at Object.f (cause.test.js:20:5) + at Promise.then.completed (node_modules/jest-circus/build/utils.js:293:28) + at new Promise () + at callAsyncCircusFn (node_modules/jest-circus/build/utils.js:226:10) + at _callCircusTest (node_modules/jest-circus/build/run.js:248:40) + at _runTest (node_modules/jest-circus/build/run.js:184:3) + at _runTestsForDescribeBlock (node_modules/jest-circus/build/run.js:86:9) + at run (node_modules/jest-circus/build/run.js:26:3) + at runAndTransformResultsToJestFormat (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:120:21) + at jestAdapter (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:74:19) + at runTestInternal (node_modules/jest-runner/build/runTest.js:281:16) + at runTest (node_modules/jest-runner/build/runTest.js:341:7)`; + +const errorWithCauseNested = buildErrorWithCause('intercepted by g', { + cause: errorWithCauseNestedNested, +}); +errorWithCauseNested.stack = `Error: intercepted by g + at g (cause.test.js:8:11) + at g (cause.test.js:13:5) + at Object.f (cause.test.js:20:5) + at Promise.then.completed (node_modules/jest-circus/build/utils.js:293:28) + at new Promise () + at callAsyncCircusFn (node_modules/jest-circus/build/utils.js:226:10) + at _callCircusTest (node_modules/jest-circus/build/run.js:248:40) + at _runTest (node_modules/jest-circus/build/run.js:184:3) + at _runTestsForDescribeBlock (node_modules/jest-circus/build/run.js:86:9) + at run (node_modules/jest-circus/build/run.js:26:3) + at runAndTransformResultsToJestFormat (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:120:21) + at jestAdapter (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:74:19) + at runTestInternal (node_modules/jest-runner/build/runTest.js:281:16) + at runTest (node_modules/jest-runner/build/runTest.js:341:7)`; + +const errorWithCause = buildErrorWithCause('intercepted by f', { + cause: errorWithCauseNested, +}); +errorWithCause.stack = `Error: intercepted by f + at f (cause.test.js:15:11) + at Object.f (cause.test.js:20:5) + at Promise.then.completed (node_modules/jest-circus/build/utils.js:293:28) + at new Promise () + at callAsyncCircusFn (node_modules/jest-circus/build/utils.js:226:10) + at _callCircusTest (node_modules/jest-circus/build/run.js:248:40) + at _runTest (node_modules/jest-circus/build/run.js:184:3) + at _runTestsForDescribeBlock (node_modules/jest-circus/build/run.js:86:9) + at run (node_modules/jest-circus/build/run.js:26:3) + at runAndTransformResultsToJestFormat (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:120:21) + at jestAdapter (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:74:19) + at runTestInternal (node_modules/jest-runner/build/runTest.js:281:16) + at runTest (node_modules/jest-runner/build/runTest.js:341:7)`; + +const errorWithStringCause = buildErrorWithCause('boom', { + cause: 'string cause', +}); +errorWithStringCause.stack = `Error: boom + at f (cause.test.js:15:11) + at Object.f (cause.test.js:20:5) + at Promise.then.completed (node_modules/jest-circus/build/utils.js:293:28) + at new Promise () + at callAsyncCircusFn (node_modules/jest-circus/build/utils.js:226:10) + at _callCircusTest (node_modules/jest-circus/build/run.js:248:40) + at _runTest (node_modules/jest-circus/build/run.js:184:3) + at _runTestsForDescribeBlock (node_modules/jest-circus/build/run.js:86:9) + at run (node_modules/jest-circus/build/run.js:26:3) + at runAndTransformResultsToJestFormat (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:120:21) + at jestAdapter (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:74:19) + at runTestInternal (node_modules/jest-runner/build/runTest.js:281:16) + at runTest (node_modules/jest-runner/build/runTest.js:341:7)`; beforeEach(() => { jest.clearAllMocks(); @@ -95,7 +185,7 @@ it('should exclude jasmine from stack trace for Unix paths.', () => { { ancestorTitles: [], duration: undefined, - failureDetails: [], + failureDetails: [unixError], failureMessages: [unixStackTrace], fullName: 'full name', invocations: undefined, @@ -143,7 +233,7 @@ it('formatStackTrace should strip node internals', () => { { ancestorTitles: [], duration: undefined, - failureDetails: [], + failureDetails: [assertionError], failureMessages: [assertionStack], fullName: 'full name', invocations: undefined, @@ -201,7 +291,7 @@ it('retains message in babel code frame error', () => { { ancestorTitles: [], duration: undefined, - failureDetails: [], + failureDetails: [babelError], failureMessages: [babelStack], fullName: 'full name', invocations: undefined, @@ -224,6 +314,64 @@ it('retains message in babel code frame error', () => { expect(messages).toMatchSnapshot(); }); +it('formatStackTrace should properly handle deeply nested causes', () => { + const messages = formatResultsErrors( + [ + { + ancestorTitles: [], + duration: undefined, + failureDetails: [errorWithCause], + failureMessages: [errorWithCause.stack || ''], + fullName: 'full name', + invocations: undefined, + location: null, + numPassingAsserts: 0, + retryReasons: undefined, + status: 'failed', + title: 'Error with cause test', + }, + ], + { + rootDir: '', + testMatch: [], + }, + { + noStackTrace: false, + }, + ); + + expect(messages).toMatchSnapshot(); +}); + +it('formatStackTrace should properly handle string causes', () => { + const messages = formatResultsErrors( + [ + { + ancestorTitles: [], + duration: undefined, + failureDetails: [errorWithStringCause], + failureMessages: [errorWithStringCause.stack || ''], + fullName: 'full name', + invocations: undefined, + location: null, + numPassingAsserts: 0, + retryReasons: undefined, + status: 'failed', + title: 'Error with string cause test', + }, + ], + { + rootDir: '', + testMatch: [], + }, + { + noStackTrace: false, + }, + ); + + expect(messages).toMatchSnapshot(); +}); + it('codeframe', () => { jest .mocked(readFileSync) diff --git a/packages/jest-message-util/src/index.ts b/packages/jest-message-util/src/index.ts index 6636bc525a0a..de851ea1d05f 100644 --- a/packages/jest-message-util/src/index.ts +++ b/packages/jest-message-util/src/index.ts @@ -379,10 +379,60 @@ export const formatStackTrace = ( }; type FailedResults = Array<{ + /** Stringified version of the error */ content: string; + /** Details related to the failure */ + failureDetails: unknown; + /** Execution result */ result: TestResult.AssertionResult; }>; +function isErrorOrStackWithCause( + errorOrStack: Error | string, +): errorOrStack is Error & {cause: Error | string} { + return ( + typeof errorOrStack !== 'string' && + 'cause' in errorOrStack && + (typeof errorOrStack.cause === 'string' || + types.isNativeError(errorOrStack.cause) || + errorOrStack.cause instanceof Error) + ); +} + +function formatErrorStack( + errorOrStack: Error | string, + config: StackTraceConfig, + options: StackTraceOptions, + testPath?: string, +): string { + // The stack of new Error('message') contains both the message and the stack, + // thus we need to sanitize and clean it for proper display using separateMessageFromStack. + const sourceStack = + typeof errorOrStack === 'string' ? errorOrStack : errorOrStack.stack || ''; + let {message, stack} = separateMessageFromStack(sourceStack); + stack = options.noStackTrace + ? '' + : `${STACK_TRACE_COLOR( + formatStackTrace(stack, config, options, testPath), + )}\n`; + + message = checkForCommonEnvironmentErrors(message); + message = indentAllLines(message); + + let cause = ''; + if (isErrorOrStackWithCause(errorOrStack)) { + const nestedCause = formatErrorStack( + errorOrStack.cause, + config, + options, + testPath, + ); + cause = `\n${MESSAGE_INDENT}Cause:\n${nestedCause}`; + } + + return `${message}\n${stack}${cause}`; +} + export const formatResultsErrors = ( testResults: Array, config: StackTraceConfig, @@ -391,8 +441,12 @@ export const formatResultsErrors = ( ): string | null => { const failedResults: FailedResults = testResults.reduce( (errors, result) => { - result.failureMessages.forEach(item => { - errors.push({content: checkForCommonEnvironmentErrors(item), result}); + result.failureMessages.forEach((item, index) => { + errors.push({ + content: item, + failureDetails: result.failureDetails[index], + result, + }); }); return errors; }, @@ -404,15 +458,12 @@ export const formatResultsErrors = ( } return failedResults - .map(({result, content}) => { - let {message, stack} = separateMessageFromStack(content); - stack = options.noStackTrace - ? '' - : `${STACK_TRACE_COLOR( - formatStackTrace(stack, config, options, testPath), - )}\n`; - - message = indentAllLines(message); + .map(({result, content, failureDetails}) => { + const rootErrorOrStack: Error | string = + failureDetails && + (types.isNativeError(failureDetails) || failureDetails instanceof Error) + ? failureDetails + : content; const title = `${chalk.bold.red( TITLE_INDENT + @@ -422,7 +473,12 @@ export const formatResultsErrors = ( result.title, )}\n`; - return `${title}\n${message}\n${stack}`; + return `${title}\n${formatErrorStack( + rootErrorOrStack, + config, + options, + testPath, + )}`; }) .join('\n'); };