diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4926fe1557..d8a1714270f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- `[expect]` Improve report when matcher fails, part 15 ([#8281](https://github.com/facebook/jest/pull/8281)) + ### Fixes - `[jest-snapshot]` Inline snapshots: do not indent empty lines ([#8277](https://github.com/facebook/jest/pull/8277)) diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index b1234a0c609b..d565aa057f99 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -225,12 +225,7 @@ Received has value: undefined" `; exports[`.toBe() does not crash on circular references 1`] = ` -"expect(received).toBe(expected) // Object.is equality - -Expected: {} -Received: {\\"circular\\": [Circular]} - -Difference: +"expect(received).toBe(expected) // Object.is equality - Expected + Received @@ -242,99 +237,137 @@ Difference: `; exports[`.toBe() fails for '"a"' with '.not' 1`] = ` -"expect(received).not.toBe(expected) // Object.is equality +"expect(received).not.toBe(expected) // Object.is equality -Expected: \\"a\\" -Received: \\"a\\"" +Expected: not \\"a\\"" `; exports[`.toBe() fails for '[]' with '.not' 1`] = ` -"expect(received).not.toBe(expected) // Object.is equality +"expect(received).not.toBe(expected) // Object.is equality -Expected: [] -Received: []" +Expected: not []" `; exports[`.toBe() fails for '{}' with '.not' 1`] = ` -"expect(received).not.toBe(expected) // Object.is equality +"expect(received).not.toBe(expected) // Object.is equality -Expected: {} -Received: {}" +Expected: not {}" `; exports[`.toBe() fails for '1' with '.not' 1`] = ` -"expect(received).not.toBe(expected) // Object.is equality +"expect(received).not.toBe(expected) // Object.is equality -Expected: 1 -Received: 1" +Expected: not 1" `; exports[`.toBe() fails for 'false' with '.not' 1`] = ` -"expect(received).not.toBe(expected) // Object.is equality +"expect(received).not.toBe(expected) // Object.is equality -Expected: false -Received: false" +Expected: not false" `; exports[`.toBe() fails for 'null' with '.not' 1`] = ` -"expect(received).not.toBe(expected) // Object.is equality +"expect(received).not.toBe(expected) // Object.is equality -Expected: null -Received: null" +Expected: not null" `; exports[`.toBe() fails for 'undefined' with '.not' 1`] = ` -"expect(received).not.toBe(expected) // Object.is equality +"expect(received).not.toBe(expected) // Object.is equality -Expected: undefined -Received: undefined" +Expected: not undefined" `; exports[`.toBe() fails for: "abc" and "cde" 1`] = ` -"expect(received).toBe(expected) // Object.is equality +"expect(received).toBe(expected) // Object.is equality Expected: \\"cde\\" Received: \\"abc\\"" `; +exports[`.toBe() fails for: "four +4 +line +string" and "3 +line +string" 1`] = ` +"expect(received).toBe(expected) // Object.is equality + +- Expected ++ Received + +- 3 ++ four ++ 4 + line + string" +`; + exports[`.toBe() fails for: "with trailing space" and "without trailing space" 1`] = ` -"expect(received).toBe(expected) // Object.is equality +"expect(received).toBe(expected) // Object.is equality Expected: \\"without trailing space\\" Received: \\"with trailing space\\"" `; +exports[`.toBe() fails for: /received/ and /expected/ 1`] = ` +"expect(received).toBe(expected) // Object.is equality + +Expected: /expected/ +Received: /received/" +`; + exports[`.toBe() fails for: [] and [] 1`] = ` -"expect(received).toBe(expected) // Object.is equality +"expect(received).toBe(expected) // Object.is equality + +If it should pass with deep equality, replace \\"toBe\\" with \\"toStrictEqual\\" Expected: [] -Received: [] +Received value has no visual difference" +`; -Difference: +exports[`.toBe() fails for: [Error: received] and [Error: expected] 1`] = ` +"expect(received).toBe(expected) // Object.is equality -Compared values have no visual difference. Note that you are testing for equality with the stricter \`toBe\` matcher using \`Object.is\`. For deep equality only, use \`toEqual\` instead." +Expected: [Error: expected] +Received: [Error: received]" `; -exports[`.toBe() fails for: {"a": 1} and {"a": 1} 1`] = ` -"expect(received).toBe(expected) // Object.is equality +exports[`.toBe() fails for: [Function anonymous] and [Function anonymous] 1`] = ` +"expect(received).toBe(expected) // Object.is equality -Expected: {\\"a\\": 1} -Received: {\\"a\\": 1} +Expected: [Function anonymous] +Received value has no visual difference" +`; -Difference: +exports[`.toBe() fails for: {"a": [Function a], "b": 2} and {"a": Any, "b": 2} 1`] = ` +"expect(received).toBe(expected) // Object.is equality + +If it should pass with deep equality, replace \\"toBe\\" with \\"toStrictEqual\\" -Compared values have no visual difference. Note that you are testing for equality with the stricter \`toBe\` matcher using \`Object.is\`. For deep equality only, use \`toEqual\` instead." +- Expected ++ Received + + Object { +- \\"a\\": Any, ++ \\"a\\": [Function a], + \\"b\\": 2, + }" `; -exports[`.toBe() fails for: {"a": 1} and {"a": 5} 1`] = ` -"expect(received).toBe(expected) // Object.is equality +exports[`.toBe() fails for: {"a": 1} and {"a": 1} 1`] = ` +"expect(received).toBe(expected) // Object.is equality -Expected: {\\"a\\": 5} -Received: {\\"a\\": 1} +If it should pass with deep equality, replace \\"toBe\\" with \\"toStrictEqual\\" -Difference: +Expected: {\\"a\\": 1} +Received value has no visual difference" +`; + +exports[`.toBe() fails for: {"a": 1} and {"a": 5} 1`] = ` +"expect(received).toBe(expected) // Object.is equality - Expected + Received @@ -345,44 +378,75 @@ Difference: }" `; -exports[`.toBe() fails for: {} and {} 1`] = ` -"expect(received).toBe(expected) // Object.is equality +exports[`.toBe() fails for: {"a": undefined, "b": 2} and {"b": 2} 1`] = ` +"expect(received).toBe(expected) // Object.is equality -Expected: {} -Received: {} +If it should pass with deep equality, replace \\"toBe\\" with \\"toEqual\\" -Difference: +- Expected ++ Received + + Object { ++ \\"a\\": undefined, + \\"b\\": 2, + }" +`; -Compared values have no visual difference. Note that you are testing for equality with the stricter \`toBe\` matcher using \`Object.is\`. For deep equality only, use \`toEqual\` instead." +exports[`.toBe() fails for: {} and {} 1`] = ` +"expect(received).toBe(expected) // Object.is equality + +If it should pass with deep equality, replace \\"toBe\\" with \\"toStrictEqual\\" + +Expected: {} +Received value has no visual difference" `; exports[`.toBe() fails for: -0 and 0 1`] = ` -"expect(received).toBe(expected) // Object.is equality +"expect(received).toBe(expected) // Object.is equality Expected: 0 Received: -0" `; exports[`.toBe() fails for: 1 and 2 1`] = ` -"expect(received).toBe(expected) // Object.is equality +"expect(received).toBe(expected) // Object.is equality Expected: 2 Received: 1" `; -exports[`.toBe() fails for: null and undefined 1`] = ` -"expect(received).toBe(expected) // Object.is equality +exports[`.toBe() fails for: 2020-02-20T00:00:00.000Z and 2020-02-20T00:00:00.000Z 1`] = ` +"expect(received).toBe(expected) // Object.is equality -Expected: undefined -Received: null +If it should pass with deep equality, replace \\"toBe\\" with \\"toStrictEqual\\" -Difference: +Expected: 2020-02-20T00:00:00.000Z +Received value has no visual difference" +`; + +exports[`.toBe() fails for: 2020-02-21T00:00:00.000Z and 2020-02-20T00:00:00.000Z 1`] = ` +"expect(received).toBe(expected) // Object.is equality - Comparing two different types of values. Expected undefined but received null." +Expected: 2020-02-20T00:00:00.000Z +Received: 2020-02-21T00:00:00.000Z" +`; + +exports[`.toBe() fails for: Symbol(received) and Symbol(expected) 1`] = ` +"expect(received).toBe(expected) // Object.is equality + +Expected: Symbol(expected) +Received: Symbol(received)" +`; + +exports[`.toBe() fails for: null and undefined 1`] = ` +"expect(received).toBe(expected) // Object.is equality + +Expected: undefined +Received: null" `; exports[`.toBe() fails for: true and false 1`] = ` -"expect(received).toBe(expected) // Object.is equality +"expect(received).toBe(expected) // Object.is equality Expected: false Received: true" diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index 953de2f2fcaf..9331fb30e6cb 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -203,11 +203,20 @@ describe('.toBe()', () => { [ [1, 2], [true, false], + [() => {}, () => {}], [{}, {}], [{a: 1}, {a: 1}], [{a: 1}, {a: 5}], + [{a: () => {}, b: 2}, {a: expect.any(Function), b: 2}], + [{a: undefined, b: 2}, {b: 2}], + [new Date('2020-02-20'), new Date('2020-02-20')], + [new Date('2020-02-21'), new Date('2020-02-20')], + [/received/, /expected/], + [Symbol('received'), Symbol('expected')], + [new Error('received'), new Error('expected')], ['abc', 'cde'], ['with \ntrailing space', 'without trailing space'], + ['four\n4\nline\nstring', '3\nline\nstring'], [[], []], [null, undefined], [-0, +0], diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 6b94eaf11d81..405b479330cc 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -8,9 +8,9 @@ import getType, {isPrimitive} from 'jest-get-type'; import { + DIM_COLOR, EXPECTED_COLOR, RECEIVED_COLOR, - SUGGEST_TO_EQUAL, SUGGEST_TO_CONTAIN_EQUAL, diff, ensureExpectedIsNonNegativeInteger, @@ -22,6 +22,7 @@ import { printReceived, printExpected, printWithType, + stringify, MatcherHintOptions, } from 'jest-matcher-utils'; import {MatchersObject, MatcherState} from './types'; @@ -41,6 +42,12 @@ import { } from './utils'; import {equals} from './jasmineUtils'; +const toStrictEqualTesters = [ + iterableEquality, + typeEquality, + sparseArrayEquality, +]; + type ContainIterable = | Array | Set @@ -50,10 +57,11 @@ type ContainIterable = const matchers: MatchersObject = { toBe(this: MatcherState, received: unknown, expected: unknown) { - const matcherName = '.toBe'; + const matcherName = 'toBe'; const options: MatcherHintOptions = { comment: 'Object.is equality', isNot: this.isNot, + promise: this.promise, }; const pass = Object.is(received, expected); @@ -62,32 +70,57 @@ const matchers: MatchersObject = { ? () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - `Expected: ${printExpected(expected)}\n` + - `Received: ${printReceived(received)}` + `Expected: not ${printExpected(expected)}` : () => { const receivedType = getType(received); const expectedType = getType(expected); - const suggestToEqual = - receivedType === expectedType && - (receivedType === 'object' || expectedType === 'array') && - equals(received, expected, [iterableEquality]); - const oneline = isOneline(expected, received); - const diffString = diff(expected, received, {expand: this.expand}); + + let deepEqualityName = null; + if (expectedType !== 'map' && expectedType !== 'set') { + // If deep equality passes when referential identity fails, + // but exclude map and set until review of their equality logic. + if (equals(received, expected, toStrictEqualTesters, true)) { + deepEqualityName = 'toStrictEqual'; + } else if (equals(received, expected, [iterableEquality])) { + deepEqualityName = 'toEqual'; + } + } + + const hasConciseReport = + receivedType !== expectedType || + (isPrimitive(expected) && + (expectedType !== 'string' || isOneline(expected, received))) || + expectedType === 'date' || + expectedType === 'function' || + expectedType === 'regexp' || + (received instanceof Error && expected instanceof Error); + const hasDifference = stringify(expected) !== stringify(received); + const difference = + !hasConciseReport && hasDifference + ? diff(expected, received, {expand: this.expand}) // string | null + : null; return ( matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - `Expected: ${printExpected(expected)}\n` + - `Received: ${printReceived(received)}` + - (diffString && !oneline ? `\n\nDifference:\n\n${diffString}` : '') + - (suggestToEqual ? ` ${SUGGEST_TO_EQUAL}` : '') + (deepEqualityName !== null + ? DIM_COLOR( + `If it should pass with deep equality, replace "${matcherName}" with "${deepEqualityName}"`, + ) + '\n\n' + : '') + + (difference !== null + ? difference + : `Expected: ${printExpected(expected)}\n` + + (hasDifference + ? `Received: ${printReceived(received)}` + : 'Received value has no visual difference')) ); }; // Passing the actual and expected objects so that a custom reporter // could access them, for example in order to display a custom visual diff, // or create a different error message - return {actual: received, expected, message, name: 'toBe', pass}; + return {actual: received, expected, message, name: matcherName, pass}; }, toBeCloseTo( @@ -856,12 +889,7 @@ const matchers: MatchersObject = { isNot: this.isNot, }; - const pass = equals( - received, - expected, - [iterableEquality, typeEquality, sparseArrayEquality], - true, - ); + const pass = equals(received, expected, toStrictEqualTesters, true); const message = pass ? () => diff --git a/packages/jest-matcher-utils/src/index.ts b/packages/jest-matcher-utils/src/index.ts index 89fd60568e6b..9f9de13390ce 100644 --- a/packages/jest-matcher-utils/src/index.ts +++ b/packages/jest-matcher-utils/src/index.ts @@ -41,7 +41,7 @@ export const EXPECTED_COLOR = chalk.green; export const RECEIVED_COLOR = chalk.red; export const INVERTED_COLOR = chalk.inverse; export const BOLD_WEIGHT = chalk.bold; -const DIM_COLOR = chalk.dim; +export const DIM_COLOR = chalk.dim; const NUMBERS = [ 'zero', @@ -60,10 +60,6 @@ const NUMBERS = [ 'thirteen', ]; -export const SUGGEST_TO_EQUAL = chalk.dim( - 'Note that you are testing for equality with the stricter `toBe` matcher using `Object.is`. For deep equality only, use `toEqual` instead.', -); - export const SUGGEST_TO_CONTAIN_EQUAL = chalk.dim( 'Looks like you wanted to test for object/array equality with the stricter `toContain` matcher. You probably need to use `toContainEqual` instead.', );