diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c2c70a5f0e6..64178c38b6f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - `[jest-environment-jsdom]` [**BREAKING**] Upgrade JSDOM to v22 ([#13825](https://github.com/jestjs/jest/pull/13825)) - `[@jest/fake-timers]` [**BREAKING**] Upgrade `@sinonjs/fake-timers` to v11 ([#14544](https://github.com/jestjs/jest/pull/14544)) - `[@jest/schemas]` Upgrade `@sinclair/typebox` to v0.31 ([#14072](https://github.com/jestjs/jest/pull/14072)) +- `[jest-snapshot]` [**BREAKING**] Add support for [Error causes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) in snapshots ([#13965](https://github.com/facebook/jest/pull/13965)) - `[pretty-format]` [**BREAKING**] Do not render empty string children (`''`) in React plugin ([#14470](https://github.com/facebook/jest/pull/14470)) ### Fixes diff --git a/e2e/__tests__/__snapshots__/toThrowErrorMatchingInlineSnapshot.test.ts.snap b/e2e/__tests__/__snapshots__/toThrowErrorMatchingInlineSnapshot.test.ts.snap index c7e0123f5ee3..7a21b7968e93 100644 --- a/e2e/__tests__/__snapshots__/toThrowErrorMatchingInlineSnapshot.test.ts.snap +++ b/e2e/__tests__/__snapshots__/toThrowErrorMatchingInlineSnapshot.test.ts.snap @@ -18,6 +18,50 @@ exports[`updates existing snapshot: updated snapshot 1`] = ` " `; +exports[`works fine when function throws error with cause: initial write with cause 1`] = ` +"test('works fine when function throws error', () => { + function ErrorWithCause(message, cause) { + const err = new Error(message, {cause}); + if (err.cause !== cause) { + // cause does not exist in old versions of node + err.cause = cause; + } + return err; + } + expect(() => { + throw ErrorWithCause( + 'apple', + ErrorWithCause('banana', ErrorWithCause('orange')), + ); + }).toThrowErrorMatchingInlineSnapshot(\` + "apple + Cause: banana + Cause: orange" + \`); +}); +" +`; + +exports[`works fine when function throws error with string cause: initial write with cause 1`] = ` +"test('works fine when function throws error', () => { + function ErrorWithCause(message, cause) { + const err = new Error(message, {cause}); + if (err.cause !== cause) { + // cause does not exist in old versions of node + err.cause = cause; + } + return err; + } + expect(() => { + throw ErrorWithCause('apple', 'here is a cause'); + }).toThrowErrorMatchingInlineSnapshot(\` + "apple + Cause: here is a cause" + \`); +}); +" +`; + exports[`works fine when function throws error: initial write 1`] = ` "test('works fine when function throws error', () => { expect(() => { diff --git a/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts b/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts index 0784e8d19cb2..88b86838a036 100644 --- a/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts +++ b/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts @@ -43,6 +43,69 @@ test('works fine when function throws error', () => { } }); +test('works fine when function throws error with cause', () => { + const filename = 'works-fine-when-function-throws-error-with-cause.test.js'; + const template = makeTemplate(` + test('works fine when function throws error', () => { + function ErrorWithCause(message, cause) { + const err = new Error(message, {cause}); + if (err.cause !== cause) { + // cause does not exist in old versions of node + err.cause = cause; + } + return err; + } + expect(() => { + throw ErrorWithCause('apple', + ErrorWithCause('banana', + ErrorWithCause('orange') + ) + ); + }) + .toThrowErrorMatchingInlineSnapshot(); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(fileAfter).toMatchSnapshot('initial write with cause'); + expect(exitCode).toBe(0); + } +}); + +test('works fine when function throws error with string cause', () => { + const filename = + 'works-fine-when-function-throws-error-with-string-cause.test.js'; + const template = makeTemplate(` + test('works fine when function throws error', () => { + function ErrorWithCause(message, cause) { + const err = new Error(message, {cause}); + if (err.cause !== cause) { + // cause does not exist in old versions of node + err.cause = cause; + } + return err; + } + expect(() => { + throw ErrorWithCause('apple', 'here is a cause'); + }) + .toThrowErrorMatchingInlineSnapshot(); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(fileAfter).toMatchSnapshot('initial write with cause'); + expect(exitCode).toBe(0); + } +}); + test('updates existing snapshot', () => { const filename = 'updates-existing-snapshot.test.js'; const template = makeTemplate(` diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index a77f80a3c64b..452704f03567 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import {types} from 'util'; import * as fs from 'graceful-fs'; import type {Config} from '@jest/types'; import type {MatcherFunctionWithContext} from 'expect'; @@ -518,12 +519,25 @@ const _toThrowErrorMatchingSnapshot = ( ); } + let message = error.message; + while ('cause' in error) { + error = error.cause; + if (types.isNativeError(error) || error instanceof Error) { + message += `\nCause: ${error.message}`; + } else { + if (typeof error === 'string') { + message += `\nCause: ${error}`; + } + break; + } + } + return _toMatchSnapshot({ context, hint, inlineSnapshot, isInline, matcherName, - received: error.message, + received: message, }); };