Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jest-diff, pretty-format): Add compareKeys option for sorting object keys #11992

Merged
merged 6 commits into from Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
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
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
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
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
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
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
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
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
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