diff --git a/CHANGELOG.md b/CHANGELOG.md index b4191f67606d..aa86dbf20cb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,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)) @@ -39,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)) @@ -46,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 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..220401524310 --- /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 = { + chillness: 100, + flaws: null, + hopOut: { + atThe: 'after party', + when: new Date('2000-07-14'), + }, + i: ['pull up'], + location: undefined, + ok: true, + species: 'capybara', + weight: 9.5, +}; + +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..9b0c9bf6748a --- /dev/null +++ b/packages/jest-core/src/lib/serializeToJSON.ts @@ -0,0 +1,37 @@ +/** + * 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 {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 + * `JSON.stringify`, particularly Errors returned in `.openHandles`. + * + * This function handles the extended serialization wanted above. + */ +export default function serializeToJSON( + value: unknown, + 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 (isNativeError(value)) { + 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..fe73415807d2 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -33,6 +33,7 @@ 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'; @@ -111,21 +112,17 @@ 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`, - ); + fs.writeFileSync(filePath, `${jsonString}\n`); outputStream.write( `Test results written to: ${path.relative(cwd, filePath)}\n`, ); } else { - process.stdout.write( - `${JSON.stringify(formatTestResults(runResults))}\n`, - ); + process.stdout.write(`${jsonString}\n`); } }