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

expect: Highlight substring differences when matcher fails, part 1 #8448

Merged
merged 16 commits into from May 27, 2019
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 .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