From 5f85233fb683962643adaa1d6423648fc24e7bac Mon Sep 17 00:00:00 2001 From: Tony Du Date: Sat, 28 Sep 2024 18:45:53 -0700 Subject: [PATCH 1/5] add stringifying errors --- .../src/lib/__tests__/serializeToJSON.test.ts | 49 +++++++++++++++++++ packages/jest-core/src/lib/serializeToJSON.ts | 35 +++++++++++++ packages/jest-core/src/runJest.ts | 6 ++- 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 packages/jest-core/src/lib/__tests__/serializeToJSON.test.ts create mode 100644 packages/jest-core/src/lib/serializeToJSON.ts diff --git a/packages/jest-core/src/lib/__tests__/serializeToJSON.test.ts b/packages/jest-core/src/lib/__tests__/serializeToJSON.test.ts new file mode 100644 index 000000000000..f20a796fcb37 --- /dev/null +++ b/packages/jest-core/src/lib/__tests__/serializeToJSON.test.ts @@ -0,0 +1,49 @@ +/** + * 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. + */ + +import serializeToJSON from '../serializeToJSON'; + +// populate an object with all basic JavaScript datatypes +const object = { + species: 'capybara', + ok: true, + i: ['pull up'], + hopOut: { + atThe: 'after party', + when: new Date('2000-07-14'), + }, + chillness: 100, + weight: 9.5, + flaws: null, + location: undefined, +}; + +it('serializes regular objects like JSON.stringify', () => { + expect(serializeToJSON(object)).toEqual(JSON.stringify(object)); +}); + +it('serializes errors', () => { + const objectWithError = { + ...object, + error: new Error('too cool'), + }; + const withError = serializeToJSON(objectWithError); + const withoutError = JSON.stringify(objectWithError); + + expect(withoutError).not.toEqual(withError); + + expect(withError).toContain(`"message":"too cool"`); + expect(withError).toContain(`"name":"Error"`); + expect(withError).toContain(`"stack":"Error:`); + + expect(JSON.parse(withError)).toMatchObject({ + error: { + message: 'too cool', + name: 'Error', + }, + }); +}); diff --git a/packages/jest-core/src/lib/serializeToJSON.ts b/packages/jest-core/src/lib/serializeToJSON.ts new file mode 100644 index 000000000000..1228329848b0 --- /dev/null +++ b/packages/jest-core/src/lib/serializeToJSON.ts @@ -0,0 +1,35 @@ +/** + * 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. + */ + +/** + * When we're asked to give a JSON output with the --json flag or otherwise, + * some data we need to return don't serialize well with a basic + * `JSON.stringify`, particularly Errors returned in `.openHandles`. + * + * This function handles the extended serialization wanted above. + */ +export default function serializeToJSON( + value: any, + space?: string | number, +): string { + return JSON.stringify( + value, + (_, value) => { + // There might be more in Error, but pulling out just the message, name, + // and stack should be good enough + if (value instanceof Error) { + return { + message: value.message, + name: value.name, + stack: value.stack, + }; + } + return value; + }, + space, + ); +} diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index 28d97d83c222..29f975e10ec9 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -35,6 +35,7 @@ import collectNodeHandles, { import getNoTestsFoundMessage from './getNoTestsFoundMessage'; import runGlobalHook from './runGlobalHook'; import type {Filter, TestRunData} from './types'; +import serializeToJSON from './lib/serializeToJSON'; const getTestPaths = async ( globalConfig: Config.GlobalConfig, @@ -111,20 +112,21 @@ const processResults = async ( runResults = await processor(runResults); } if (isJSON) { + const jsonString = serializeToJSON(formatTestResults(runResults)); if (outputFile) { const cwd = tryRealpath(process.cwd()); const filePath = path.resolve(cwd, outputFile); fs.writeFileSync( filePath, - `${JSON.stringify(formatTestResults(runResults))}\n`, + `${jsonString}\n`, ); outputStream.write( `Test results written to: ${path.relative(cwd, filePath)}\n`, ); } else { process.stdout.write( - `${JSON.stringify(formatTestResults(runResults))}\n`, + `${jsonString}\n`, ); } } From 2cbba58bcadd9eef891ccf7cbc7b5d3f26b504fc Mon Sep 17 00:00:00 2001 From: Tony Du Date: Sat, 28 Sep 2024 19:13:16 -0700 Subject: [PATCH 2/5] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 363abee8a97e..c766eababc0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- [jest-core] Stringify Errors properly with --json flag ([#15329](https://github.com/jestjs/jest/pull/15329)) - `[babel-jest]` Add option `excludeJestPreset` to allow opting out of `babel-preset-jest` ([#15164](https://github.com/jestjs/jest/pull/15164)) - `[jest-circus, jest-cli, jest-config]` Add `waitNextEventLoopTurnForUnhandledRejectionEvents` flag to minimise performance impact of correct detection of unhandled promise rejections introduced in [#14315](https://github.com/jestjs/jest/pull/14315) ([#14681](https://github.com/jestjs/jest/pull/14681)) - `[jest-circus]` Add a `waitBeforeRetry` option to `jest.retryTimes` ([#14738](https://github.com/jestjs/jest/pull/14738)) From 24bd16d215c1a8cab8e159233d9f8e4570ddb597 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Wed, 15 Jan 2025 13:30:42 +0100 Subject: [PATCH 3/5] sort changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f2033c071b..aa86dbf20cb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ### Features -- [jest-core] Stringify Errors properly with --json flag ([#15329](https://github.com/jestjs/jest/pull/15329)) - `[babel-jest]` Add option `excludeJestPreset` to allow opting out of `babel-preset-jest` ([#15164](https://github.com/jestjs/jest/pull/15164)) - `[jest-circus, jest-cli, jest-config]` Add `waitNextEventLoopTurnForUnhandledRejectionEvents` flag to minimise performance impact of correct detection of unhandled promise rejections introduced in [#14315](https://github.com/jestjs/jest/pull/14315) ([#14681](https://github.com/jestjs/jest/pull/14681)) - `[jest-circus]` Add a `waitBeforeRetry` option to `jest.retryTimes` ([#14738](https://github.com/jestjs/jest/pull/14738)) @@ -21,7 +20,9 @@ - `[@jest/core]` Add `perfStats` to surface test setup overhead ([#14622](https://github.com/jestjs/jest/pull/14622)) - `[@jest/core]` [**BREAKING**] Changed `--filter` to accept an object with shape `{ filtered: Array }` to match [documentation](https://jestjs.io/docs/cli#--filterfile) ([#13319](https://github.com/jestjs/jest/pull/13319)) - `[@jest/core]` Support `--outputFile` option for [`--listTests`](https://jestjs.io/docs/cli#--listtests) ([#14980](https://github.com/jestjs/jest/pull/14980)) +- `[@jest/core]` Stringify Errors properly with `--json` flag ([#15329](https://github.com/jestjs/jest/pull/15329)) - `[@jest/core, @jest/test-sequencer]` [**BREAKING**] Exposes `globalConfig` & `contexts` to `TestSequencer` ([#14535](https://github.com/jestjs/jest/pull/14535), & [#14543](https://github.com/jestjs/jest/pull/14543)) +- `[jest-each]` Introduce `%$` option to add number of the test to its title ([#14710](https://github.com/jestjs/jest/pull/14710)) - `[@jest/environment]` [**BREAKING**] Remove deprecated `jest.genMockFromModule()` ([#15042](https://github.com/jestjs/jest/pull/15042)) - `[@jest/environment]` [**BREAKING**] Remove unnecessary defensive code ([#15045](https://github.com/jestjs/jest/pull/15045)) - `[jest-environment-jsdom]` [**BREAKING**] Upgrade JSDOM to v22 ([#13825](https://github.com/jestjs/jest/pull/13825)) @@ -40,6 +41,7 @@ - `[jest-runtime]` Support `import.meta.resolve` ([#14930](https://github.com/jestjs/jest/pull/14930)) - `[jest-runtime]` [**BREAKING**] Make it mandatory to pass `globalConfig` to the `Runtime` constructor ([#15044](https://github.com/jestjs/jest/pull/15044)) - `[jest-runtime]` Add `unstable_unmockModule` ([#15080](https://github.com/jestjs/jest/pull/15080)) +- `[jest-runtime]` Add `onGenerateMock` transformer callback for auto generated callbacks ([#15433](https://github.com/jestjs/jest/pull/15433)) - `[@jest/schemas]` Upgrade `@sinclair/typebox` to v0.34 ([#15450](https://github.com/jestjs/jest/pull/15450)) - `[@jest/types]` `test.each()`: Accept a readonly (`as const`) table properly ([#14565](https://github.com/jestjs/jest/pull/14565)) - `[@jest/types]` Improve argument type inference passed to `test` and `describe` callback functions from `each` tables ([#14920](https://github.com/jestjs/jest/pull/14920)) @@ -47,8 +49,6 @@ - `[jest-snapshot]` Support Prettier 3 ([#14566](https://github.com/facebook/jest/pull/14566)) - `[@jest/util-snapshot]` Extract utils used by tooling from `jest-snapshot` into its own package ([#15095](https://github.com/facebook/jest/pull/15095)) - `[pretty-format]` [**BREAKING**] Do not render empty string children (`''`) in React plugin ([#14470](https://github.com/facebook/jest/pull/14470)) -- `[jest-each]` Introduce `%$` option to add number of the test to its title ([#14710](https://github.com/jestjs/jest/pull/14710)) -- `[jest-runtime]` Add `onGenerateMock` transformer callback for auto generated callbacks ([#15433](https://github.com/jestjs/jest/pull/15433)) ### Fixes From 59cf0558f37621956a80062b7ab39eb4550cfc09 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Wed, 15 Jan 2025 13:31:45 +0100 Subject: [PATCH 4/5] eslint --- .../src/lib/__tests__/serializeToJSON.test.ts | 18 +++++++++--------- packages/jest-core/src/lib/serializeToJSON.ts | 2 +- packages/jest-core/src/runJest.ts | 11 +++-------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/jest-core/src/lib/__tests__/serializeToJSON.test.ts b/packages/jest-core/src/lib/__tests__/serializeToJSON.test.ts index f20a796fcb37..220401524310 100644 --- a/packages/jest-core/src/lib/__tests__/serializeToJSON.test.ts +++ b/packages/jest-core/src/lib/__tests__/serializeToJSON.test.ts @@ -9,17 +9,17 @@ import serializeToJSON from '../serializeToJSON'; // populate an object with all basic JavaScript datatypes const object = { - species: 'capybara', - ok: true, - i: ['pull up'], + chillness: 100, + flaws: null, hopOut: { atThe: 'after party', when: new Date('2000-07-14'), }, - chillness: 100, - weight: 9.5, - flaws: null, + i: ['pull up'], location: undefined, + ok: true, + species: 'capybara', + weight: 9.5, }; it('serializes regular objects like JSON.stringify', () => { @@ -36,9 +36,9 @@ it('serializes errors', () => { expect(withoutError).not.toEqual(withError); - expect(withError).toContain(`"message":"too cool"`); - expect(withError).toContain(`"name":"Error"`); - expect(withError).toContain(`"stack":"Error:`); + expect(withError).toContain('"message":"too cool"'); + expect(withError).toContain('"name":"Error"'); + expect(withError).toContain('"stack":"Error:'); expect(JSON.parse(withError)).toMatchObject({ error: { diff --git a/packages/jest-core/src/lib/serializeToJSON.ts b/packages/jest-core/src/lib/serializeToJSON.ts index 1228329848b0..ce9798dc4cc1 100644 --- a/packages/jest-core/src/lib/serializeToJSON.ts +++ b/packages/jest-core/src/lib/serializeToJSON.ts @@ -13,7 +13,7 @@ * This function handles the extended serialization wanted above. */ export default function serializeToJSON( - value: any, + value: unknown, space?: string | number, ): string { return JSON.stringify( diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index 29f975e10ec9..fe73415807d2 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -33,9 +33,9 @@ import collectNodeHandles, { type HandleCollectionResult, } from './collectHandles'; import getNoTestsFoundMessage from './getNoTestsFoundMessage'; +import serializeToJSON from './lib/serializeToJSON'; import runGlobalHook from './runGlobalHook'; import type {Filter, TestRunData} from './types'; -import serializeToJSON from './lib/serializeToJSON'; const getTestPaths = async ( globalConfig: Config.GlobalConfig, @@ -117,17 +117,12 @@ const processResults = async ( const cwd = tryRealpath(process.cwd()); const filePath = path.resolve(cwd, outputFile); - fs.writeFileSync( - filePath, - `${jsonString}\n`, - ); + fs.writeFileSync(filePath, `${jsonString}\n`); outputStream.write( `Test results written to: ${path.relative(cwd, filePath)}\n`, ); } else { - process.stdout.write( - `${jsonString}\n`, - ); + process.stdout.write(`${jsonString}\n`); } } From 78771159651b332bb18bcde5d3aba70c69e3e71b Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Wed, 15 Jan 2025 13:33:31 +0100 Subject: [PATCH 5/5] handle cross-realm errors --- packages/jest-core/src/lib/serializeToJSON.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/jest-core/src/lib/serializeToJSON.ts b/packages/jest-core/src/lib/serializeToJSON.ts index ce9798dc4cc1..9b0c9bf6748a 100644 --- a/packages/jest-core/src/lib/serializeToJSON.ts +++ b/packages/jest-core/src/lib/serializeToJSON.ts @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import {isNativeError} from 'node:util/types'; + /** * When we're asked to give a JSON output with the --json flag or otherwise, * some data we need to return don't serialize well with a basic @@ -21,7 +23,7 @@ export default function serializeToJSON( (_, value) => { // There might be more in Error, but pulling out just the message, name, // and stack should be good enough - if (value instanceof Error) { + if (isNativeError(value)) { return { message: value.message, name: value.name,