From c99f1e6ba5909bf9db8b38e8466fc5392a02d6bd Mon Sep 17 00:00:00 2001 From: Dimitar Andreev Date: Sun, 24 Oct 2021 14:07:36 +0300 Subject: [PATCH] feat(jest-diff, pretty-format): Add compareKeys option (#11938) --- CHANGELOG.md | 3 + packages/jest-diff/README.md | 59 +++++++++++++- packages/jest-diff/src/__tests__/diff.test.ts | 39 +++++++++ packages/jest-diff/src/index.ts | 81 ++++++++++--------- .../jest-diff/src/normalizeDiffOptions.ts | 9 ++- packages/jest-diff/src/types.ts | 4 + packages/pretty-format/README.md | 2 + .../src/__tests__/prettyFormat.test.ts | 18 +++++ packages/pretty-format/src/collections.ts | 10 ++- packages/pretty-format/src/index.ts | 5 ++ packages/pretty-format/src/types.ts | 5 ++ 11 files changed, 193 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c37f53b3699e..442a390029f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ ## 27.3.1 +### Features +- `[jest-diff, pretty-format]` Add `compareKeys` option for custom sorting of object keys + ### Fixes - `[expect]` Make `expect` extension properties `configurable` ([#11978](https://github.com/facebook/jest/pull/11978)) diff --git a/packages/jest-diff/README.md b/packages/jest-diff/README.md index e01116fd68cb..ca8fd3cad02c 100644 --- a/packages/jest-diff/README.md +++ b/packages/jest-diff/README.md @@ -394,13 +394,13 @@ 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` | | `includeChangeCounts` | `false` | | `omitAnnotationLines` | `false` | | `patchColor` | `chalk.yellow` | - For more information about the options, see the following examples. ### Example of options for labels @@ -612,3 +612,60 @@ 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..fb21fddc7eca 100644 --- a/packages/jest-diff/src/index.ts +++ b/packages/jest-diff/src/index.ts @@ -6,6 +6,7 @@ */ import chalk = require('chalk'); +import type {PrettyFormatOptions} from 'pretty-format/src/types'; import {getType} from 'jest-get-type'; import { format as prettyFormat, @@ -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..ddca87fda454 100644 --- a/packages/jest-diff/src/normalizeDiffOptions.ts +++ b/packages/jest-diff/src/normalizeDiffOptions.ts @@ -6,7 +6,7 @@ */ import chalk = require('chalk'); -import type {DiffOptions, DiffOptionsNormalized} from './types'; +import type {CompareKeys, DiffOptions, DiffOptionsNormalized} from './types'; export const noColor = (string: string): string => string; @@ -24,6 +24,7 @@ const OPTIONS_DEFAULT: DiffOptionsNormalized = { commonColor: chalk.dim, commonIndicator: ' ', commonLineTrailingSpaceColor: noColor, + compareKeys: undefined, contextLines: DIFF_CONTEXT_DEFAULT, emptyFirstOrLastLinePlaceholder: '', expand: true, @@ -32,6 +33,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 +51,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..f303188b753d 100644 --- a/packages/jest-diff/src/types.ts +++ b/packages/jest-diff/src/types.ts @@ -7,6 +7,8 @@ export type DiffOptionsColor = (arg: string) => string; // subset of Chalk type +export type CompareKeys = ((a: string, b: string) => number) | undefined; + export type DiffOptions = { aAnnotation?: string; aColor?: DiffOptionsColor; @@ -25,6 +27,7 @@ export type DiffOptions = { includeChangeCounts?: boolean; omitAnnotationLines?: boolean; patchColor?: DiffOptionsColor; + compareKeys?: CompareKeys; }; export type DiffOptionsNormalized = { @@ -39,6 +42,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..f738f0264743 100644 --- a/packages/pretty-format/src/__tests__/prettyFormat.test.ts +++ b/packages/pretty-format/src/__tests__/prettyFormat.test.ts @@ -335,6 +335,24 @@ describe('prettyFormat()', () => { expect(prettyFormat(val)).toEqual('Object {\n "a": 2,\n "b": 1,\n}'); }); + it('prints an object with keys in their original order', () => { + /* eslint-disable sort-keys */ + const val = {b: 1, a: 2}; + /* eslint-enable sort-keys */ + 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..9e44768ea397 100644 --- a/packages/pretty-format/src/collections.ts +++ b/packages/pretty-format/src/collections.ts @@ -6,10 +6,14 @@ * */ +import type {CompareKeys} from 'jest-diff/src/types'; import type {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 +179,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..4b2a67c02bb0 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -396,6 +396,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 +486,10 @@ const getConfig = (options?: OptionsReceived): Config => ({ options && options.highlight ? getColorsHighlight(options) : getColorsEmpty(), + compareKeys: + options && options.compareKeys !== undefined + ? 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..3cd07c4cf995 100644 --- a/packages/pretty-format/src/types.ts +++ b/packages/pretty-format/src/types.ts @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import type {CompareKeys} from 'jest-diff/src/types'; + export type Colors = { comment: {close: string; open: string}; content: {close: string; open: string}; @@ -34,6 +36,7 @@ type ThemeReceived = { 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;