Skip to content

Commit

Permalink
feat(jest-diff, pretty-format): Add compareKeys option (jestjs#11938)
Browse files Browse the repository at this point in the history
  • Loading branch information
D-Andreev committed Oct 24, 2021
1 parent f41e128 commit c99f1e6
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 42 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
59 changes: 58 additions & 1 deletion packages/jest-diff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
}
```
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 @@ -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,
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,
);
}
}
9 changes: 8 additions & 1 deletion packages/jest-diff/src/normalizeDiffOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -24,6 +24,7 @@ const OPTIONS_DEFAULT: DiffOptionsNormalized = {
commonColor: chalk.dim,
commonIndicator: ' ',
commonLineTrailingSpaceColor: noColor,
compareKeys: undefined,
contextLines: DIFF_CONTEXT_DEFAULT,
emptyFirstOrLastLinePlaceholder: '',
expand: true,
Expand All @@ -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) &&
Expand All @@ -45,5 +51,6 @@ export const normalizeDiffOptions = (
): DiffOptionsNormalized => ({
...OPTIONS_DEFAULT,
...options,
compareKeys: getCompareKeys(options.compareKeys),
contextLines: getContextLines(options.contextLines),
});
4 changes: 4 additions & 0 deletions packages/jest-diff/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,6 +27,7 @@ export type DiffOptions = {
includeChangeCounts?: boolean;
omitAnnotationLines?: boolean;
patchColor?: DiffOptionsColor;
compareKeys?: CompareKeys;
};

export type DiffOptionsNormalized = {
Expand All @@ -39,6 +42,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
18 changes: 18 additions & 0 deletions packages/pretty-format/src/__tests__/prettyFormat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/');
Expand Down
10 changes: 7 additions & 3 deletions packages/pretty-format/src/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
*
*/

import type {CompareKeys} from 'jest-diff/src/types';
import type {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 +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;
Expand Down
5 changes: 5 additions & 0 deletions packages/pretty-format/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit c99f1e6

Please sign in to comment.