Skip to content

Commit 27b89ec

Browse files
authored
feat(jest-diff, pretty-format): Add compareKeys option for sorting object keys (#11992)
1 parent fe5e91c commit 27b89ec

File tree

11 files changed

+191
-43
lines changed

11 files changed

+191
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Features
44

55
- `[jest-core]` Add support for `testResultsProcessor` written in ESM ([#12006](https://github.com/facebook/jest/pull/12006))
6+
- `[jest-diff, pretty-format]` Add `compareKeys` option for custom sorting of object keys ([#11992](https://github.com/facebook/jest/pull/11992))
67

78
### Fixes
89

packages/jest-diff/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ For other applications, you can provide an options object as a third argument:
394394
| `commonColor` | `chalk.dim` |
395395
| `commonIndicator` | `' '` |
396396
| `commonLineTrailingSpaceColor` | `string => string` |
397+
| `compareKeys` | `undefined` |
397398
| `contextLines` | `5` |
398399
| `emptyFirstOrLastLinePlaceholder` | `''` |
399400
| `expand` | `true` |
@@ -612,3 +613,59 @@ If a content line is empty, then the corresponding comparison line is automatica
612613
| `aIndicator` | `'-·'` | `'-'` |
613614
| `bIndicator` | `'+·'` | `'+'` |
614615
| `commonIndicator` | `' ·'` | `''` |
616+
617+
### Example of option for sorting object keys
618+
619+
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.
620+
621+
Use `compareKeys` to pass a function which will be used when sorting the object keys.
622+
623+
```js
624+
const a = {c: 'c', b: 'b1', a: 'a'};
625+
const b = {c: 'c', b: 'b2', a: 'a'};
626+
627+
const options = {
628+
// The keys will be in their original order
629+
compareKeys: () => 0,
630+
};
631+
632+
const difference = diff(a, b, options);
633+
```
634+
635+
```diff
636+
- Expected
637+
+ Received
638+
639+
Object {
640+
"c": "c",
641+
- "b": "b1",
642+
+ "b": "b2",
643+
"a": "a",
644+
}
645+
```
646+
647+
Depending on the implementation of `compareKeys` any sort order can be used.
648+
649+
```js
650+
const a = {c: 'c', b: 'b1', a: 'a'};
651+
const b = {c: 'c', b: 'b2', a: 'a'};
652+
653+
const options = {
654+
// The keys will be in reverse order
655+
compareKeys: (a, b) => (a > b ? -1 : 1),
656+
};
657+
658+
const difference = diff(a, b, options);
659+
```
660+
661+
```diff
662+
- Expected
663+
+ Received
664+
665+
Object {
666+
"a": "a",
667+
- "b": "b1",
668+
+ "b": "b2",
669+
"c": "c",
670+
}
671+
```

packages/jest-diff/src/__tests__/diff.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,4 +1120,43 @@ describe('options', () => {
11201120
expect(diffStringsUnified(aEmpty, bEmpty, options)).toBe(expected);
11211121
});
11221122
});
1123+
1124+
describe('compare keys', () => {
1125+
const a = {a: {d: 1, e: 1, f: 1}, b: 1, c: 1};
1126+
const b = {a: {d: 1, e: 2, f: 1}, b: 1, c: 1};
1127+
1128+
test('keeps the object keys in their original order', () => {
1129+
const compareKeys = () => 0;
1130+
const expected = [
1131+
' Object {',
1132+
' "a": Object {',
1133+
' "d": 1,',
1134+
'- "e": 1,',
1135+
'+ "e": 2,',
1136+
' "f": 1,',
1137+
' },',
1138+
' "b": 1,',
1139+
' "c": 1,',
1140+
' }',
1141+
].join('\n');
1142+
expect(diff(a, b, {...optionsBe, compareKeys})).toBe(expected);
1143+
});
1144+
1145+
test('sorts the object keys in reverse order', () => {
1146+
const compareKeys = (a: string, b: string) => (a > b ? -1 : 1);
1147+
const expected = [
1148+
' Object {',
1149+
' "c": 1,',
1150+
' "b": 1,',
1151+
' "a": Object {',
1152+
' "f": 1,',
1153+
'- "e": 1,',
1154+
'+ "e": 2,',
1155+
' "d": 1,',
1156+
' },',
1157+
' }',
1158+
].join('\n');
1159+
expect(diff(a, b, {...optionsBe, compareKeys})).toBe(expected);
1160+
});
1161+
});
11231162
});

packages/jest-diff/src/index.ts

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
format as prettyFormat,
1212
plugins as prettyFormatPlugins,
1313
} from 'pretty-format';
14+
import type {PrettyFormatOptions} from 'pretty-format';
1415
import {DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff} from './cleanupSemantic';
1516
import {NO_DIFF_MESSAGE, SIMILAR_MESSAGE} from './constants';
1617
import {diffLinesRaw, diffLinesUnified, diffLinesUnified2} from './diffLines';
@@ -49,13 +50,11 @@ const PLUGINS = [
4950
const FORMAT_OPTIONS = {
5051
plugins: PLUGINS,
5152
};
52-
const FORMAT_OPTIONS_0 = {...FORMAT_OPTIONS, indent: 0};
5353
const FALLBACK_FORMAT_OPTIONS = {
5454
callToJSON: false,
5555
maxDepth: 10,
5656
plugins: PLUGINS,
5757
};
58-
const FALLBACK_FORMAT_OPTIONS_0 = {...FALLBACK_FORMAT_OPTIONS, indent: 0};
5958

6059
// Generate a string that will highlight the difference between two values
6160
// with green and red. (similar to how github does code diffing)
@@ -137,50 +136,20 @@ function compareObjects(
137136
) {
138137
let difference;
139138
let hasThrown = false;
140-
const noDiffMessage = getCommonMessage(NO_DIFF_MESSAGE, options);
141139

142140
try {
143-
const aCompare = prettyFormat(a, FORMAT_OPTIONS_0);
144-
const bCompare = prettyFormat(b, FORMAT_OPTIONS_0);
145-
146-
if (aCompare === bCompare) {
147-
difference = noDiffMessage;
148-
} else {
149-
const aDisplay = prettyFormat(a, FORMAT_OPTIONS);
150-
const bDisplay = prettyFormat(b, FORMAT_OPTIONS);
151-
152-
difference = diffLinesUnified2(
153-
aDisplay.split('\n'),
154-
bDisplay.split('\n'),
155-
aCompare.split('\n'),
156-
bCompare.split('\n'),
157-
options,
158-
);
159-
}
141+
const formatOptions = getFormatOptions(FORMAT_OPTIONS, options);
142+
difference = getObjectsDifference(a, b, formatOptions, options);
160143
} catch {
161144
hasThrown = true;
162145
}
163146

147+
const noDiffMessage = getCommonMessage(NO_DIFF_MESSAGE, options);
164148
// If the comparison yields no results, compare again but this time
165149
// without calling `toJSON`. It's also possible that toJSON might throw.
166150
if (difference === undefined || difference === noDiffMessage) {
167-
const aCompare = prettyFormat(a, FALLBACK_FORMAT_OPTIONS_0);
168-
const bCompare = prettyFormat(b, FALLBACK_FORMAT_OPTIONS_0);
169-
170-
if (aCompare === bCompare) {
171-
difference = noDiffMessage;
172-
} else {
173-
const aDisplay = prettyFormat(a, FALLBACK_FORMAT_OPTIONS);
174-
const bDisplay = prettyFormat(b, FALLBACK_FORMAT_OPTIONS);
175-
176-
difference = diffLinesUnified2(
177-
aDisplay.split('\n'),
178-
bDisplay.split('\n'),
179-
aCompare.split('\n'),
180-
bCompare.split('\n'),
181-
options,
182-
);
183-
}
151+
const formatOptions = getFormatOptions(FALLBACK_FORMAT_OPTIONS, options);
152+
difference = getObjectsDifference(a, b, formatOptions, options);
184153

185154
if (difference !== noDiffMessage && !hasThrown) {
186155
difference =
@@ -190,3 +159,41 @@ function compareObjects(
190159

191160
return difference;
192161
}
162+
163+
function getFormatOptions(
164+
formatOptions: PrettyFormatOptions,
165+
options?: DiffOptions,
166+
): PrettyFormatOptions {
167+
const {compareKeys} = normalizeDiffOptions(options);
168+
169+
return {
170+
...formatOptions,
171+
compareKeys,
172+
};
173+
}
174+
175+
function getObjectsDifference(
176+
a: Record<string, any>,
177+
b: Record<string, any>,
178+
formatOptions: PrettyFormatOptions,
179+
options?: DiffOptions,
180+
): string {
181+
const formatOptionsZeroIndent = {...formatOptions, indent: 0};
182+
const aCompare = prettyFormat(a, formatOptionsZeroIndent);
183+
const bCompare = prettyFormat(b, formatOptionsZeroIndent);
184+
185+
if (aCompare === bCompare) {
186+
return getCommonMessage(NO_DIFF_MESSAGE, options);
187+
} else {
188+
const aDisplay = prettyFormat(a, formatOptions);
189+
const bDisplay = prettyFormat(b, formatOptions);
190+
191+
return diffLinesUnified2(
192+
aDisplay.split('\n'),
193+
bDisplay.split('\n'),
194+
aCompare.split('\n'),
195+
bCompare.split('\n'),
196+
options,
197+
);
198+
}
199+
}

packages/jest-diff/src/normalizeDiffOptions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import chalk = require('chalk');
9+
import type {CompareKeys} from 'pretty-format';
910
import type {DiffOptions, DiffOptionsNormalized} from './types';
1011

1112
export const noColor = (string: string): string => string;
@@ -24,6 +25,7 @@ const OPTIONS_DEFAULT: DiffOptionsNormalized = {
2425
commonColor: chalk.dim,
2526
commonIndicator: ' ',
2627
commonLineTrailingSpaceColor: noColor,
28+
compareKeys: undefined,
2729
contextLines: DIFF_CONTEXT_DEFAULT,
2830
emptyFirstOrLastLinePlaceholder: '',
2931
expand: true,
@@ -32,6 +34,11 @@ const OPTIONS_DEFAULT: DiffOptionsNormalized = {
3234
patchColor: chalk.yellow,
3335
};
3436

37+
const getCompareKeys = (compareKeys?: CompareKeys): CompareKeys =>
38+
compareKeys && typeof compareKeys === 'function'
39+
? compareKeys
40+
: OPTIONS_DEFAULT.compareKeys;
41+
3542
const getContextLines = (contextLines?: number): number =>
3643
typeof contextLines === 'number' &&
3744
Number.isSafeInteger(contextLines) &&
@@ -45,5 +52,6 @@ export const normalizeDiffOptions = (
4552
): DiffOptionsNormalized => ({
4653
...OPTIONS_DEFAULT,
4754
...options,
55+
compareKeys: getCompareKeys(options.compareKeys),
4856
contextLines: getContextLines(options.contextLines),
4957
});

packages/jest-diff/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*/
7+
import type {CompareKeys} from 'pretty-format';
78

89
export type DiffOptionsColor = (arg: string) => string; // subset of Chalk type
910

@@ -25,6 +26,7 @@ export type DiffOptions = {
2526
includeChangeCounts?: boolean;
2627
omitAnnotationLines?: boolean;
2728
patchColor?: DiffOptionsColor;
29+
compareKeys?: CompareKeys;
2830
};
2931

3032
export type DiffOptionsNormalized = {
@@ -39,6 +41,7 @@ export type DiffOptionsNormalized = {
3941
commonColor: DiffOptionsColor;
4042
commonIndicator: string;
4143
commonLineTrailingSpaceColor: DiffOptionsColor;
44+
compareKeys: CompareKeys;
4245
contextLines: number;
4346
emptyFirstOrLastLinePlaceholder: string;
4447
expand: boolean;

packages/pretty-format/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ console.log(prettyFormat(onClick, options));
6969
| key | type | default | description |
7070
| :-------------------- | :-------- | :--------- | :------------------------------------------------------ |
7171
| `callToJSON` | `boolean` | `true` | call `toJSON` method (if it exists) on objects |
72+
| `compareKeys` | `function`| `undefined`| compare function used when sorting object keys |
7273
| `escapeRegex` | `boolean` | `false` | escape special characters in regular expressions |
7374
| `escapeString` | `boolean` | `true` | escape special characters in strings |
7475
| `highlight` | `boolean` | `false` | highlight syntax with colors in terminal (some plugins) |
@@ -207,6 +208,7 @@ Write `serialize` to return a string, given the arguments:
207208
| key | type | description |
208209
| :------------------ | :-------- | :------------------------------------------------------ |
209210
| `callToJSON` | `boolean` | call `toJSON` method (if it exists) on objects |
211+
| `compareKeys` | `function`| compare function used when sorting object keys |
210212
| `colors` | `Object` | escape codes for colors to highlight syntax |
211213
| `escapeRegex` | `boolean` | escape special characters in regular expressions |
212214
| `escapeString` | `boolean` | escape special characters in strings |

packages/pretty-format/src/__tests__/prettyFormat.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,12 +329,28 @@ describe('prettyFormat()', () => {
329329
});
330330

331331
it('prints an object with sorted properties', () => {
332-
/* eslint-disable sort-keys */
332+
// eslint-disable-next-line sort-keys
333333
const val = {b: 1, a: 2};
334-
/* eslint-enable sort-keys */
335334
expect(prettyFormat(val)).toEqual('Object {\n "a": 2,\n "b": 1,\n}');
336335
});
337336

337+
it('prints an object with keys in their original order', () => {
338+
// eslint-disable-next-line sort-keys
339+
const val = {b: 1, a: 2};
340+
const compareKeys = () => 0;
341+
expect(prettyFormat(val, {compareKeys})).toEqual(
342+
'Object {\n "b": 1,\n "a": 2,\n}',
343+
);
344+
});
345+
346+
it('prints an object with keys sorted in reverse order', () => {
347+
const val = {a: 1, b: 2};
348+
const compareKeys = (a: string, b: string) => (a > b ? -1 : 1);
349+
expect(prettyFormat(val, {compareKeys})).toEqual(
350+
'Object {\n "b": 2,\n "a": 1,\n}',
351+
);
352+
});
353+
338354
it('prints regular expressions from constructors', () => {
339355
const val = new RegExp('regexp');
340356
expect(prettyFormat(val)).toEqual('/regexp/');

packages/pretty-format/src/collections.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
*
77
*/
88

9-
import type {Config, Printer, Refs} from './types';
9+
import type {CompareKeys, Config, Printer, Refs} from './types';
1010

11-
const getKeysOfEnumerableProperties = (object: Record<string, unknown>) => {
12-
const keys: Array<string | symbol> = Object.keys(object).sort();
11+
const getKeysOfEnumerableProperties = (
12+
object: Record<string, unknown>,
13+
compareKeys: CompareKeys,
14+
) => {
15+
const keys: Array<string | symbol> = Object.keys(object).sort(compareKeys);
1316

1417
if (Object.getOwnPropertySymbols) {
1518
Object.getOwnPropertySymbols(object).forEach(symbol => {
@@ -175,7 +178,7 @@ export function printObjectProperties(
175178
printer: Printer,
176179
): string {
177180
let result = '';
178-
const keys = getKeysOfEnumerableProperties(val);
181+
const keys = getKeysOfEnumerableProperties(val, config.compareKeys);
179182

180183
if (keys.length) {
181184
result += config.spacingOuter;

0 commit comments

Comments
 (0)