Skip to content

Commit

Permalink
expect: Highlight substring differences when matcher fails, part 1 (#…
Browse files Browse the repository at this point in the history
…8448)

* expect: Improve report when matcher fails, part 20

* Added newlines to cleanupSemantic

* Add Facebook copyright header to declarations

* Added exception to checkCopyrightHeaders script

* Update CHANGELOG.md

* Make substring highlight explicit in CHANGELOG.md

* Convert cleanupSemantic to TypeScript

* Add diff-sequences to references in tsconfig.json

* Delete unneeded toString method of Diff class

* Add condition for edge case of empty string

* Decide not to rename DIFF_EQUAL

* Rewrite complicated condition as its plain meaning

* Encapsulate substring diff in array subclass with toJSON method

* Move printDiffOrStringify to jest-matcher-utils

* Add copyright header to 2 added files
  • Loading branch information
pedrottimark committed May 27, 2019
1 parent a5c7380 commit 3cb3274
Show file tree
Hide file tree
Showing 14 changed files with 841 additions and 89 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Expand Up @@ -4,6 +4,7 @@ bin/
flow-typed/**
packages/*/build/**
packages/*/build-es5/**
packages/jest-matcher-utils/src/cleanupSemantic.ts
website/blog
website/build
website/node_modules
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
@@ -1,3 +1,4 @@
fixtures/failing-jsons/
packages/jest-matcher-utils/src/cleanupSemantic.ts
packages/jest-config/src/__tests__/jest-preset.json
packages/pretty-format/perf/world.geo.json
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

### Features

- `[expect]` Highlight substring differences when matcher fails, part 1 ([#8448](https://github.com/facebook/jest/pull/8448))
- `[jest-cli]` Improve chai support (with detailed output, to match jest exceptions) ([#8454](https://github.com/facebook/jest/pull/8454))

### Fixes
Expand Down
48 changes: 39 additions & 9 deletions packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap
Expand Up @@ -278,6 +278,13 @@ exports[`.toBe() fails for 'undefined' with '.not' 1`] = `
Expected: not <green>undefined</>"
`;

exports[`.toBe() fails for: "" and "compare one-line string to empty string" 1`] = `
"<dim>expect(</><red>received</><dim>).</>toBe<dim>(</><green>expected</><dim>) // Object.is equality</>

Expected: <green>\\"compare one-line string to empty string\\"</>
Received: <red>\\"\\"</>"
`;

exports[`.toBe() fails for: "abc" and "cde" 1`] = `
"<dim>expect(</><red>received</><dim>).</>toBe<dim>(</><green>expected</><dim>) // Object.is equality</>

Expand All @@ -303,6 +310,13 @@ string" 1`] = `
<dim> string</>"
`;

exports[`.toBe() fails for: "painless JavaScript testing" and "delightful JavaScript testing" 1`] = `
"<dim>expect(</><red>received</><dim>).</>toBe<dim>(</><green>expected</><dim>) // Object.is equality</>

Expected: <green>\\"<inverse>delightful</> JavaScript testing\\"</>
Received: <red>\\"<inverse>painless</> JavaScript testing\\"</>"
`;

exports[`.toBe() fails for: "with
trailing space" and "without trailing space" 1`] = `
"<dim>expect(</><red>received</><dim>).</>toBe<dim>(</><green>expected</><dim>) // Object.is equality</>
Expand Down Expand Up @@ -1922,6 +1936,13 @@ exports[`.toContain(), .toContainEqual() error cases for toContainEqual 1`] = `
Received has value: <red>null</>"
`;

exports[`.toEqual() {pass: false} expect("1 234,57 $").toEqual("1 234,57 $") 1`] = `
"<dim>expect(</><red>received</><dim>).</>toEqual<dim>(</><green>expected</><dim>) // deep equality</>

Expected: <green>\\"1<inverse> </>234,57<inverse> </>$\\"</>
Received: <red>\\"1<inverse> </>234,57<inverse> </>$\\"</>"
`;

exports[`.toEqual() {pass: false} expect("Eve").toEqual({"asymmetricMatch": [Function asymmetricMatch]}) 1`] = `
"<dim>expect(</><red>received</><dim>).</>toEqual<dim>(</><green>expected</><dim>) // deep equality</>

Expand Down Expand Up @@ -1985,15 +2006,8 @@ exports[`.toEqual() {pass: false} expect([1]).toEqual([2]) 1`] = `
exports[`.toEqual() {pass: false} expect({"a": 1, "b": 2}).toEqual(ObjectContaining {"a": 2}) 1`] = `
"<dim>expect(</><red>received</><dim>).</>toEqual<dim>(</><green>expected</><dim>) // deep equality</>

<green>- Expected</>
<red>+ Received</>

<green>- ObjectContaining {</>
<green>- \\"a\\": 2,</>
<red>+ Object {</>
<red>+ \\"a\\": 1,</>
<red>+ \\"b\\": 2,</>
<dim> }</>"
Expected: <green>ObjectContaining {\\"a\\": 2}</>
Received: <red>{\\"a\\": 1, \\"b\\": 2}</>"
`;

exports[`.toEqual() {pass: false} expect({"a": 1}).toEqual({"a": 2}) 1`] = `
Expand Down Expand Up @@ -3057,6 +3071,15 @@ Expected value: <green>2</>
Received value: <red>1</>"
`;

exports[`.toHaveProperty() {pass: false} expect({"children": ["\\"That cartoon\\""], "props": null, "type": "p"}).toHaveProperty('children,0', "\\"That cat cartoon\\"") 1`] = `
"<dim>expect(</><red>received</><dim>).</>toHaveProperty<dim>(</><green>path</><dim>, </><green>value</><dim>)</>

Expected path: <green>[\\"children\\", 0]</>

Expected value: <green>\\"\\\\\\"That <inverse>cat </>cartoon\\\\\\"\\"</>
Received value: <red>\\"\\\\\\"That cartoon\\\\\\"\\"</>"
`;

exports[`.toHaveProperty() {pass: false} expect({"key": 1}).toHaveProperty('not') 1`] = `
"<dim>expect(</><red>received</><dim>).</>toHaveProperty<dim>(</><green>path</><dim>)</>

Expand Down Expand Up @@ -3497,6 +3520,13 @@ Expected substring: <green>\\"foo\\"</>
Received string: <red>\\"bar\\"</>"
`;

exports[`.toStrictEqual() displays substring diff 1`] = `
"<dim>expect(</><red>received</><dim>).</>toStrictEqual<dim>(</><green>expected</><dim>) // deep equality</>

Expected: <green>\\"<inverse>Another caveat is that</> Jest will not typecheck your tests.\\"</>
Received: <red>\\"<inverse>Because TypeScript support in Babel is just transpilation,</> Jest will not type<inverse>-</>check your tests<inverse> as they run</>.\\"</>"
`;

exports[`.toStrictEqual() matches the expected snapshot when it fails 1`] = `
"<dim>expect(</><red>received</><dim>).</>toStrictEqual<dim>(</><green>expected</><dim>) // deep equality</>

Expand Down
22 changes: 22 additions & 0 deletions packages/expect/src/__tests__/matchers.test.js
Expand Up @@ -215,6 +215,8 @@ describe('.toBe()', () => {
[Symbol('received'), Symbol('expected')],
[new Error('received'), new Error('expected')],
['abc', 'cde'],
['painless JavaScript testing', 'delightful JavaScript testing'],
['', 'compare one-line string to empty string'],
['with \ntrailing space', 'without trailing space'],
['four\n4\nline\nstring', '3\nline\nstring'],
[[], []],
Expand Down Expand Up @@ -318,6 +320,16 @@ describe('.toStrictEqual()', () => {
).toThrowErrorMatchingSnapshot();
});

it('displays substring diff', () => {
const expected =
'Another caveat is that Jest will not typecheck your tests.';
const received =
'Because TypeScript support in Babel is just transpilation, Jest will not type-check your tests as they run.';
expect(() =>
jestExpect(received).toStrictEqual(expected),
).toThrowErrorMatchingSnapshot();
});

it('does not pass for different types', () => {
expect({
test: new TestClassA(1, 2),
Expand Down Expand Up @@ -358,6 +370,7 @@ describe('.toEqual()', () => {
[{a: 1}, {a: 2}],
[{a: 5}, {b: 6}],
['banana', 'apple'],
['1\u{00A0}234,57\u{00A0}$', '1 234,57 $'], // issues/6881
[null, undefined],
[[1], [2]],
[[1, 2], [2, 1]],
Expand Down Expand Up @@ -1348,6 +1361,14 @@ describe('.toHaveProperty()', () => {
const memoized = function() {};
memoized.memo = [];

const receivedDiff = {
children: ['"That cartoon"'],
props: null,
type: 'p',
};
const pathDiff = ['children', 0];
const valueDiff = '"That cat cartoon"';

[
[{a: {b: {c: {d: 1}}}}, 'a.b.c.d', 1],
[{a: {b: {c: {d: 1}}}}, ['a', 'b', 'c', 'd'], 1],
Expand Down Expand Up @@ -1384,6 +1405,7 @@ describe('.toHaveProperty()', () => {
[{a: {b: {c: {d: 1}}}}, 'a.b.c.d', 2],
[{'a.b.c.d': 1}, 'a.b.c.d', 2],
[{'a.b.c.d': 1}, ['a.b.c.d'], 2],
[receivedDiff, pathDiff, valueDiff],
[{a: {b: {c: {d: 1}}}}, ['a', 'b', 'c', 'd'], 2],
[{a: {b: {c: {}}}}, 'a.b.c.d', 1],
[{a: 1}, 'a.b.c.d', 5],
Expand Down
15 changes: 9 additions & 6 deletions packages/expect/src/matchers.ts
Expand Up @@ -18,6 +18,7 @@ import {
getLabelPrinter,
matcherErrorMessage,
matcherHint,
printDiffOrStringify,
printReceived,
printExpected,
printWithType,
Expand All @@ -26,7 +27,6 @@ import {
} from 'jest-matcher-utils';
import {MatchersObject, MatcherState} from './types';
import {
printDiffOrStringify,
printExpectedConstructorName,
printExpectedConstructorNameNot,
printReceivedArrayContainExpectedItem,
Expand All @@ -51,6 +51,9 @@ const RECEIVED_LABEL = 'Received';
const EXPECTED_VALUE_LABEL = 'Expected value';
const RECEIVED_VALUE_LABEL = 'Received value';

// The optional property of matcher context is true if undefined.
const isExpand = (expand?: boolean): boolean => expand !== false;

const toStrictEqualTesters = [
iterableEquality,
typeEquality,
Expand Down Expand Up @@ -107,7 +110,7 @@ const matchers: MatchersObject = {
received,
EXPECTED_LABEL,
RECEIVED_LABEL,
this.expand,
isExpand(this.expand),
)
);
};
Expand Down Expand Up @@ -577,7 +580,7 @@ const matchers: MatchersObject = {
received,
EXPECTED_LABEL,
RECEIVED_LABEL,
this.expand,
isExpand(this.expand),
);

// Passing the actual and expected objects so that a custom reporter
Expand Down Expand Up @@ -751,7 +754,7 @@ const matchers: MatchersObject = {
receivedValue,
EXPECTED_VALUE_LABEL,
RECEIVED_VALUE_LABEL,
this.expand,
isExpand(this.expand),
)
: `Received path: ${printReceived(
expectedPathType === 'array' || receivedPath.length === 0
Expand Down Expand Up @@ -886,7 +889,7 @@ const matchers: MatchersObject = {
getObjectSubset(received, expected),
EXPECTED_LABEL,
RECEIVED_LABEL,
this.expand,
isExpand(this.expand),
);

return {message, pass};
Expand Down Expand Up @@ -918,7 +921,7 @@ const matchers: MatchersObject = {
received,
EXPECTED_LABEL,
RECEIVED_LABEL,
this.expand,
isExpand(this.expand),
);

// Passing the actual and expected objects so that a custom reporter
Expand Down
73 changes: 0 additions & 73 deletions packages/expect/src/print.ts
Expand Up @@ -6,18 +6,13 @@
*
*/

import getType, {isPrimitive} from 'jest-get-type';
import {
EXPECTED_COLOR,
INVERTED_COLOR,
RECEIVED_COLOR,
diff,
getLabelPrinter,
printExpected,
printReceived,
stringify,
} from 'jest-matcher-utils';
import {isOneline} from './utils';

// Format substring but do not enclose in double quote marks.
// The replacement is compatible with pretty-format package.
Expand Down Expand Up @@ -66,74 +61,6 @@ export const printReceivedArrayContainExpectedItem = (
']',
);

const shouldPrintDiff = (expected: unknown, received: unknown): boolean => {
const expectedType = getType(expected);
const receivedType = getType(received);

if (expectedType !== receivedType) {
return false;
}

if (isPrimitive(expected)) {
// Print diff only if both strings have more than one line.
return expectedType === 'string' && !isOneline(expected, received);
}

if (
expectedType === 'date' ||
expectedType === 'function' ||
expectedType === 'regexp'
) {
return false;
}

if (expected instanceof Error && received instanceof Error) {
return false;
}

return true;
};

export const printDiffOrStringify = (
expected: unknown,
received: unknown,
expectedLabel: string, // include colon and one or more spaces,
receivedLabel: string, // same as returned by getLabelPrinter
expand?: boolean, // diff option: true if `--expand` CLI option
): string => {
// Cannot use same serialization as shortcut to avoid diff,
// because stringify (that is, pretty-format with min option)
// omits constructor name for array or object, too bad so sad :(
const difference = shouldPrintDiff(expected, received)
? diff(expected, received, {
aAnnotation: expectedLabel,
bAnnotation: receivedLabel,
expand,
}) // string | null
: null;

// Cannot reuse value of stringify(received) in report string,
// because printReceived does inverse highlight space at end of line,
// but RECEIVED_COLOR does not (it refers to a plain chalk method).
if (
typeof difference === 'string' &&
difference.includes('- ' + expectedLabel) &&
difference.includes('+ ' + receivedLabel)
) {
return difference;
}

const printLabel = getLabelPrinter(expectedLabel, receivedLabel);
return (
`${printLabel(expectedLabel)}${printExpected(expected)}\n` +
`${printLabel(receivedLabel)}${
stringify(expected) === stringify(received)
? 'serializes to the same string'
: printReceived(received)
}`
);
};

export const printExpectedConstructorName = (
label: string,
expected: Function,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-matcher-utils/package.json
Expand Up @@ -14,6 +14,7 @@
"main": "build/index.js",
"dependencies": {
"chalk": "^2.0.1",
"diff-sequences": "^24.3.0",
"jest-diff": "^24.8.0",
"jest-get-type": "^24.8.0",
"pretty-format": "^24.8.0"
Expand Down

0 comments on commit 3cb3274

Please sign in to comment.