Skip to content

Commit

Permalink
feat(jest-diff, pretty-format): Add compareKeys option for sorting ob…
Browse files Browse the repository at this point in the history
…ject keys (#11992)
  • Loading branch information
D-Andreev authored Oct 28, 2021
1 parent fe5e91c commit 27b89ec
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 43 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
57 changes: 57 additions & 0 deletions packages/jest-diff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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",
}
```
39 changes: 39 additions & 0 deletions packages/jest-diff/src/__tests__/diff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
81 changes: 44 additions & 37 deletions packages/jest-diff/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 =
Expand All @@ -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<string, any>,
b: Record<string, any>,
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,
);
}
}
8 changes: 8 additions & 0 deletions packages/jest-diff/src/normalizeDiffOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +25,7 @@ const OPTIONS_DEFAULT: DiffOptionsNormalized = {
commonColor: chalk.dim,
commonIndicator: ' ',
commonLineTrailingSpaceColor: noColor,
compareKeys: undefined,
contextLines: DIFF_CONTEXT_DEFAULT,
emptyFirstOrLastLinePlaceholder: '',
expand: true,
Expand All @@ -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) &&
Expand All @@ -45,5 +52,6 @@ export const normalizeDiffOptions = (
): DiffOptionsNormalized => ({
...OPTIONS_DEFAULT,
...options,
compareKeys: getCompareKeys(options.compareKeys),
contextLines: getContextLines(options.contextLines),
});
3 changes: 3 additions & 0 deletions packages/jest-diff/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -25,6 +26,7 @@ export type DiffOptions = {
includeChangeCounts?: boolean;
omitAnnotationLines?: boolean;
patchColor?: DiffOptionsColor;
compareKeys?: CompareKeys;
};

export type DiffOptionsNormalized = {
Expand All @@ -39,6 +41,7 @@ export type DiffOptionsNormalized = {
commonColor: DiffOptionsColor;
commonIndicator: string;
commonLineTrailingSpaceColor: DiffOptionsColor;
compareKeys: CompareKeys;
contextLines: number;
emptyFirstOrLastLinePlaceholder: string;
expand: boolean;
Expand Down
2 changes: 2 additions & 0 deletions packages/pretty-format/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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 |
Expand Down
20 changes: 18 additions & 2 deletions packages/pretty-format/src/__tests__/prettyFormat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/');
Expand Down
11 changes: 7 additions & 4 deletions packages/pretty-format/src/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
*
*/

import type {Config, Printer, Refs} from './types';
import type {CompareKeys, Config, Printer, Refs} from './types';

const getKeysOfEnumerableProperties = (object: Record<string, unknown>) => {
const keys: Array<string | symbol> = Object.keys(object).sort();
const getKeysOfEnumerableProperties = (
object: Record<string, unknown>,
compareKeys: CompareKeys,
) => {
const keys: Array<string | symbol> = Object.keys(object).sort(compareKeys);

if (Object.getOwnPropertySymbols) {
Object.getOwnPropertySymbols(object).forEach(symbol => {
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 27b89ec

Please sign in to comment.