diff --git a/CHANGELOG.md b/CHANGELOG.md index 01481ec5f85f..bd9009ea09b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - `[jest-core]` Add support for `testResultsProcessor` written in ESM ([#12006](https://github.com/facebook/jest/pull/12006)) +- `[jest-diff, pretty-format]` Add `compareKeys` option for custom sorting of object keys ([#11992](https://github.com/facebook/jest/pull/11992)) ### Fixes diff --git a/packages/jest-diff/README.md b/packages/jest-diff/README.md index e01116fd68cb..d52f821789a3 100644 --- a/packages/jest-diff/README.md +++ b/packages/jest-diff/README.md @@ -394,6 +394,7 @@ For other applications, you can provide an options object as a third argument: | `commonColor` | `chalk.dim` | | `commonIndicator` | `' '` | | `commonLineTrailingSpaceColor` | `string => string` | +| `compareKeys` | `undefined` | | `contextLines` | `5` | | `emptyFirstOrLastLinePlaceholder` | `''` | | `expand` | `true` | @@ -612,3 +613,59 @@ If a content line is empty, then the corresponding comparison line is automatica | `aIndicator` | `'-·'` | `'-'` | | `bIndicator` | `'+·'` | `'+'` | | `commonIndicator` | `' ·'` | `''` | + +### Example of option for sorting object keys + +When two objects are compared their keys are printed in alphabetical order by default. If this was not the original order of the keys the diff becomes harder to read as the keys are not in their original position. + +Use `compareKeys` to pass a function which will be used when sorting the object keys. + +```js +const a = {c: 'c', b: 'b1', a: 'a'}; +const b = {c: 'c', b: 'b2', a: 'a'}; + +const options = { + // The keys will be in their original order + compareKeys: () => 0, +}; + +const difference = diff(a, b, options); +``` + +```diff +- Expected ++ Received + + Object { + "c": "c", +- "b": "b1", ++ "b": "b2", + "a": "a", + } +``` + +Depending on the implementation of `compareKeys` any sort order can be used. + +```js +const a = {c: 'c', b: 'b1', a: 'a'}; +const b = {c: 'c', b: 'b2', a: 'a'}; + +const options = { + // The keys will be in reverse order + compareKeys: (a, b) => (a > b ? -1 : 1), +}; + +const difference = diff(a, b, options); +``` + +```diff +- Expected ++ Received + + Object { + "a": "a", +- "b": "b1", ++ "b": "b2", + "c": "c", + } +``` diff --git a/packages/jest-diff/src/__tests__/diff.test.ts b/packages/jest-diff/src/__tests__/diff.test.ts index 08be4d94e2c7..3d111e4720a6 100644 --- a/packages/jest-diff/src/__tests__/diff.test.ts +++ b/packages/jest-diff/src/__tests__/diff.test.ts @@ -1120,4 +1120,43 @@ describe('options', () => { expect(diffStringsUnified(aEmpty, bEmpty, options)).toBe(expected); }); }); + + describe('compare keys', () => { + const a = {a: {d: 1, e: 1, f: 1}, b: 1, c: 1}; + const b = {a: {d: 1, e: 2, f: 1}, b: 1, c: 1}; + + test('keeps the object keys in their original order', () => { + const compareKeys = () => 0; + const expected = [ + ' Object {', + ' "a": Object {', + ' "d": 1,', + '- "e": 1,', + '+ "e": 2,', + ' "f": 1,', + ' },', + ' "b": 1,', + ' "c": 1,', + ' }', + ].join('\n'); + expect(diff(a, b, {...optionsBe, compareKeys})).toBe(expected); + }); + + test('sorts the object keys in reverse order', () => { + const compareKeys = (a: string, b: string) => (a > b ? -1 : 1); + const expected = [ + ' Object {', + ' "c": 1,', + ' "b": 1,', + ' "a": Object {', + ' "f": 1,', + '- "e": 1,', + '+ "e": 2,', + ' "d": 1,', + ' },', + ' }', + ].join('\n'); + expect(diff(a, b, {...optionsBe, compareKeys})).toBe(expected); + }); + }); }); diff --git a/packages/jest-diff/src/index.ts b/packages/jest-diff/src/index.ts index 719760f9003c..399b5cc5776c 100644 --- a/packages/jest-diff/src/index.ts +++ b/packages/jest-diff/src/index.ts @@ -11,6 +11,7 @@ import { format as prettyFormat, plugins as prettyFormatPlugins, } from 'pretty-format'; +import type {PrettyFormatOptions} from 'pretty-format'; import {DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff} from './cleanupSemantic'; import {NO_DIFF_MESSAGE, SIMILAR_MESSAGE} from './constants'; import {diffLinesRaw, diffLinesUnified, diffLinesUnified2} from './diffLines'; @@ -49,13 +50,11 @@ const PLUGINS = [ const FORMAT_OPTIONS = { plugins: PLUGINS, }; -const FORMAT_OPTIONS_0 = {...FORMAT_OPTIONS, indent: 0}; const FALLBACK_FORMAT_OPTIONS = { callToJSON: false, maxDepth: 10, plugins: PLUGINS, }; -const FALLBACK_FORMAT_OPTIONS_0 = {...FALLBACK_FORMAT_OPTIONS, indent: 0}; // Generate a string that will highlight the difference between two values // with green and red. (similar to how github does code diffing) @@ -137,50 +136,20 @@ function compareObjects( ) { let difference; let hasThrown = false; - const noDiffMessage = getCommonMessage(NO_DIFF_MESSAGE, options); try { - const aCompare = prettyFormat(a, FORMAT_OPTIONS_0); - const bCompare = prettyFormat(b, FORMAT_OPTIONS_0); - - if (aCompare === bCompare) { - difference = noDiffMessage; - } else { - const aDisplay = prettyFormat(a, FORMAT_OPTIONS); - const bDisplay = prettyFormat(b, FORMAT_OPTIONS); - - difference = diffLinesUnified2( - aDisplay.split('\n'), - bDisplay.split('\n'), - aCompare.split('\n'), - bCompare.split('\n'), - options, - ); - } + const formatOptions = getFormatOptions(FORMAT_OPTIONS, options); + difference = getObjectsDifference(a, b, formatOptions, options); } catch { hasThrown = true; } + const noDiffMessage = getCommonMessage(NO_DIFF_MESSAGE, options); // If the comparison yields no results, compare again but this time // without calling `toJSON`. It's also possible that toJSON might throw. if (difference === undefined || difference === noDiffMessage) { - const aCompare = prettyFormat(a, FALLBACK_FORMAT_OPTIONS_0); - const bCompare = prettyFormat(b, FALLBACK_FORMAT_OPTIONS_0); - - if (aCompare === bCompare) { - difference = noDiffMessage; - } else { - const aDisplay = prettyFormat(a, FALLBACK_FORMAT_OPTIONS); - const bDisplay = prettyFormat(b, FALLBACK_FORMAT_OPTIONS); - - difference = diffLinesUnified2( - aDisplay.split('\n'), - bDisplay.split('\n'), - aCompare.split('\n'), - bCompare.split('\n'), - options, - ); - } + const formatOptions = getFormatOptions(FALLBACK_FORMAT_OPTIONS, options); + difference = getObjectsDifference(a, b, formatOptions, options); if (difference !== noDiffMessage && !hasThrown) { difference = @@ -190,3 +159,41 @@ function compareObjects( return difference; } + +function getFormatOptions( + formatOptions: PrettyFormatOptions, + options?: DiffOptions, +): PrettyFormatOptions { + const {compareKeys} = normalizeDiffOptions(options); + + return { + ...formatOptions, + compareKeys, + }; +} + +function getObjectsDifference( + a: Record, + b: Record, + formatOptions: PrettyFormatOptions, + options?: DiffOptions, +): string { + const formatOptionsZeroIndent = {...formatOptions, indent: 0}; + const aCompare = prettyFormat(a, formatOptionsZeroIndent); + const bCompare = prettyFormat(b, formatOptionsZeroIndent); + + if (aCompare === bCompare) { + return getCommonMessage(NO_DIFF_MESSAGE, options); + } else { + const aDisplay = prettyFormat(a, formatOptions); + const bDisplay = prettyFormat(b, formatOptions); + + return diffLinesUnified2( + aDisplay.split('\n'), + bDisplay.split('\n'), + aCompare.split('\n'), + bCompare.split('\n'), + options, + ); + } +} diff --git a/packages/jest-diff/src/normalizeDiffOptions.ts b/packages/jest-diff/src/normalizeDiffOptions.ts index 0f4aedc8ceaf..5e7cb9336e1a 100644 --- a/packages/jest-diff/src/normalizeDiffOptions.ts +++ b/packages/jest-diff/src/normalizeDiffOptions.ts @@ -6,6 +6,7 @@ */ import chalk = require('chalk'); +import type {CompareKeys} from 'pretty-format'; import type {DiffOptions, DiffOptionsNormalized} from './types'; export const noColor = (string: string): string => string; @@ -24,6 +25,7 @@ const OPTIONS_DEFAULT: DiffOptionsNormalized = { commonColor: chalk.dim, commonIndicator: ' ', commonLineTrailingSpaceColor: noColor, + compareKeys: undefined, contextLines: DIFF_CONTEXT_DEFAULT, emptyFirstOrLastLinePlaceholder: '', expand: true, @@ -32,6 +34,11 @@ const OPTIONS_DEFAULT: DiffOptionsNormalized = { patchColor: chalk.yellow, }; +const getCompareKeys = (compareKeys?: CompareKeys): CompareKeys => + compareKeys && typeof compareKeys === 'function' + ? compareKeys + : OPTIONS_DEFAULT.compareKeys; + const getContextLines = (contextLines?: number): number => typeof contextLines === 'number' && Number.isSafeInteger(contextLines) && @@ -45,5 +52,6 @@ export const normalizeDiffOptions = ( ): DiffOptionsNormalized => ({ ...OPTIONS_DEFAULT, ...options, + compareKeys: getCompareKeys(options.compareKeys), contextLines: getContextLines(options.contextLines), }); diff --git a/packages/jest-diff/src/types.ts b/packages/jest-diff/src/types.ts index 58d72f55c5c4..da33eb3131b5 100644 --- a/packages/jest-diff/src/types.ts +++ b/packages/jest-diff/src/types.ts @@ -4,6 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ +import type {CompareKeys} from 'pretty-format'; export type DiffOptionsColor = (arg: string) => string; // subset of Chalk type @@ -25,6 +26,7 @@ export type DiffOptions = { includeChangeCounts?: boolean; omitAnnotationLines?: boolean; patchColor?: DiffOptionsColor; + compareKeys?: CompareKeys; }; export type DiffOptionsNormalized = { @@ -39,6 +41,7 @@ export type DiffOptionsNormalized = { commonColor: DiffOptionsColor; commonIndicator: string; commonLineTrailingSpaceColor: DiffOptionsColor; + compareKeys: CompareKeys; contextLines: number; emptyFirstOrLastLinePlaceholder: string; expand: boolean; diff --git a/packages/pretty-format/README.md b/packages/pretty-format/README.md index 315866f71929..c5cb0041dbfa 100755 --- a/packages/pretty-format/README.md +++ b/packages/pretty-format/README.md @@ -69,6 +69,7 @@ console.log(prettyFormat(onClick, options)); | key | type | default | description | | :-------------------- | :-------- | :--------- | :------------------------------------------------------ | | `callToJSON` | `boolean` | `true` | call `toJSON` method (if it exists) on objects | +| `compareKeys` | `function`| `undefined`| compare function used when sorting object keys | | `escapeRegex` | `boolean` | `false` | escape special characters in regular expressions | | `escapeString` | `boolean` | `true` | escape special characters in strings | | `highlight` | `boolean` | `false` | highlight syntax with colors in terminal (some plugins) | @@ -207,6 +208,7 @@ Write `serialize` to return a string, given the arguments: | key | type | description | | :------------------ | :-------- | :------------------------------------------------------ | | `callToJSON` | `boolean` | call `toJSON` method (if it exists) on objects | +| `compareKeys` | `function`| compare function used when sorting object keys | | `colors` | `Object` | escape codes for colors to highlight syntax | | `escapeRegex` | `boolean` | escape special characters in regular expressions | | `escapeString` | `boolean` | escape special characters in strings | diff --git a/packages/pretty-format/src/__tests__/prettyFormat.test.ts b/packages/pretty-format/src/__tests__/prettyFormat.test.ts index fde20e6ac839..ab9d0b9da214 100644 --- a/packages/pretty-format/src/__tests__/prettyFormat.test.ts +++ b/packages/pretty-format/src/__tests__/prettyFormat.test.ts @@ -329,12 +329,28 @@ describe('prettyFormat()', () => { }); it('prints an object with sorted properties', () => { - /* eslint-disable sort-keys */ + // eslint-disable-next-line sort-keys const val = {b: 1, a: 2}; - /* eslint-enable sort-keys */ expect(prettyFormat(val)).toEqual('Object {\n "a": 2,\n "b": 1,\n}'); }); + it('prints an object with keys in their original order', () => { + // eslint-disable-next-line sort-keys + const val = {b: 1, a: 2}; + const compareKeys = () => 0; + expect(prettyFormat(val, {compareKeys})).toEqual( + 'Object {\n "b": 1,\n "a": 2,\n}', + ); + }); + + it('prints an object with keys sorted in reverse order', () => { + const val = {a: 1, b: 2}; + const compareKeys = (a: string, b: string) => (a > b ? -1 : 1); + expect(prettyFormat(val, {compareKeys})).toEqual( + 'Object {\n "b": 2,\n "a": 1,\n}', + ); + }); + it('prints regular expressions from constructors', () => { const val = new RegExp('regexp'); expect(prettyFormat(val)).toEqual('/regexp/'); diff --git a/packages/pretty-format/src/collections.ts b/packages/pretty-format/src/collections.ts index 6884059fe307..97fae10163ba 100644 --- a/packages/pretty-format/src/collections.ts +++ b/packages/pretty-format/src/collections.ts @@ -6,10 +6,13 @@ * */ -import type {Config, Printer, Refs} from './types'; +import type {CompareKeys, Config, Printer, Refs} from './types'; -const getKeysOfEnumerableProperties = (object: Record) => { - const keys: Array = Object.keys(object).sort(); +const getKeysOfEnumerableProperties = ( + object: Record, + compareKeys: CompareKeys, +) => { + const keys: Array = Object.keys(object).sort(compareKeys); if (Object.getOwnPropertySymbols) { Object.getOwnPropertySymbols(object).forEach(symbol => { @@ -175,7 +178,7 @@ export function printObjectProperties( printer: Printer, ): string { let result = ''; - const keys = getKeysOfEnumerableProperties(val); + const keys = getKeysOfEnumerableProperties(val, config.compareKeys); if (keys.length) { result += config.spacingOuter; diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index 80cd8af4850a..4969eced6499 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -35,6 +35,7 @@ import type { export type { Colors, + CompareKeys, Config, Options, OptionsReceived, @@ -42,6 +43,7 @@ export type { NewPlugin, Plugin, Plugins, + PrettyFormatOptions, Printer, Refs, Theme, @@ -396,6 +398,7 @@ const DEFAULT_THEME_KEYS = Object.keys(DEFAULT_THEME) as Array< export const DEFAULT_OPTIONS: Options = { callToJSON: true, + compareKeys: undefined, escapeRegex: false, escapeString: true, highlight: false, @@ -485,6 +488,10 @@ const getConfig = (options?: OptionsReceived): Config => ({ options && options.highlight ? getColorsHighlight(options) : getColorsEmpty(), + compareKeys: + options && typeof options.compareKeys === 'function' + ? options.compareKeys + : DEFAULT_OPTIONS.compareKeys, escapeRegex: getEscapeRegex(options), escapeString: getEscapeString(options), indent: diff --git a/packages/pretty-format/src/types.ts b/packages/pretty-format/src/types.ts index cb7413069436..4723bef1b380 100644 --- a/packages/pretty-format/src/types.ts +++ b/packages/pretty-format/src/types.ts @@ -32,8 +32,11 @@ type ThemeReceived = { value?: string; }; +export type CompareKeys = ((a: string, b: string) => number) | undefined; + export type Options = { callToJSON: boolean; + compareKeys: CompareKeys; escapeRegex: boolean; escapeString: boolean; highlight: boolean; @@ -48,6 +51,7 @@ export type Options = { export interface PrettyFormatOptions { callToJSON?: boolean; + compareKeys?: CompareKeys; escapeRegex?: boolean; escapeString?: boolean; highlight?: boolean; @@ -64,6 +68,7 @@ export type OptionsReceived = PrettyFormatOptions; export type Config = { callToJSON: boolean; + compareKeys: CompareKeys; colors: Colors; escapeRegex: boolean; escapeString: boolean;