From ccf39807f9cab09cb24d4d9e20238acc022d22f2 Mon Sep 17 00:00:00 2001 From: BondarenkoAlex Date: Tue, 8 Oct 2024 17:33:02 +0300 Subject: [PATCH] fix: error cause bug Closes jestjs/jest#15316 jestjs/jest#15111 --- CHANGELOG.md | 1 + .../src/__tests__/toThrowMatchers.test.ts | 31 ++++++++++++++ packages/expect/src/toThrowMatchers.ts | 41 ++++++++++++++----- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 363abee8a97e..c5f8f0c78e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ ### Fixes +- `[expect]` Fix `error causes` bug ([#15339](https://github.com/jestjs/jest/pull/15339)) - `[babel-plugin-jest-hoist]` Use `denylist` instead of the deprecated `blacklist` for Babel 8 support ([#14109](https://github.com/jestjs/jest/pull/14109)) - `[expect]` Check error instance type for `toThrow/toThrowError` ([#14576](https://github.com/jestjs/jest/pull/14576)) - `[expect]` Improve diff for failing `expect.objectContaining` ([#15038](https://github.com/jestjs/jest/pull/15038)) diff --git a/packages/expect/src/__tests__/toThrowMatchers.test.ts b/packages/expect/src/__tests__/toThrowMatchers.test.ts index 2a7d19f68da8..54339e38b74c 100644 --- a/packages/expect/src/__tests__/toThrowMatchers.test.ts +++ b/packages/expect/src/__tests__/toThrowMatchers.test.ts @@ -307,6 +307,37 @@ describe('toThrow', () => { throw new Error('good', {cause: errorA}); }).not.toThrow(expected); }); + + test('isNot false, compare Error with object', () => { + jestExpect(() => { + throw errorB; + }).toThrow({ + cause: { + message: 'A', + }, + message: 'B', + }); + }); + + test('isNot false, cause is string', () => { + jestExpect(() => { + throw new Error('Message', {cause: 'line 123'}); + }).toThrow({ + cause: 'line 123', + message: 'Message', + }); + }); + + test('isNot false, cause is object', () => { + jestExpect(() => { + throw new Error('Message', { + cause: {prop1: true, prop2: false, prop3: null, prop4: undefined}, + }); + }).toThrow({ + cause: {prop1: true, prop2: false, prop3: null, prop4: undefined}, + message: 'Message', + }); + }); }); describe('fail', () => { diff --git a/packages/expect/src/toThrowMatchers.ts b/packages/expect/src/toThrowMatchers.ts index dfda024afb8c..10653c602907 100644 --- a/packages/expect/src/toThrowMatchers.ts +++ b/packages/expect/src/toThrowMatchers.ts @@ -467,22 +467,43 @@ const formatStack = (thrown: Thrown | null) => }, ); -function createMessageAndCauseMessage(error: Error): string { - if (error.cause instanceof Error) { - return `{ message: ${error.message}, cause: ${createMessageAndCauseMessage( - error.cause, - )}}`; +function createMessageAndCause(error: Error) { + if (error.cause) { + const seen = new WeakSet(); + return JSON.stringify(buildSerializeError(error), (_, value) => { + if (value !== null && value === undefined && typeof value === 'object') { + if (seen.has(value)) return; + seen.add(value); // stop circular references + } + return value === undefined ? String(undefined) : value; + }); } - return `{ message: ${error.message} }`; + return error.message; } -function createMessageAndCause(error: Error) { - if (error.cause instanceof Error) { - return createMessageAndCauseMessage(error); +function buildSerializeError(error: {[key: string]: any}) { + if (!isObject(error)) { + return error; } - return error.message; + const result: {[key: string]: any} = {}; + for (const name of Object.getOwnPropertyNames(error).sort()) { + if (['stack', 'fileName', 'lineNumber'].includes(name)) { + continue; + } + if (name === 'cause') { + result[name] = buildSerializeError(error['cause']); + continue; + } + result[name] = error[name]; + } + + return result; +} + +function isObject(obj: unknown) { + return !!obj && typeof obj === 'object'; } function messageAndCause(error: Error) {