From eacb65c215cb3e6327a12131d8a3dd81abd73ac6 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Fri, 26 Jul 2019 16:08:46 -0400 Subject: [PATCH 1/5] expect: Improve report when negative CalledWith assertion fails --- .../__snapshots__/spyMatchers.test.js.snap | 306 +++++++++++------- packages/expect/src/spyMatchers.ts | 184 ++++++++--- 2 files changed, 333 insertions(+), 157 deletions(-) diff --git a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap index c25156a340cd..a9aeb6eda19c 100644 --- a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap @@ -1,10 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`lastCalledWith includes the custom mock name in the error message 1`] = ` -"expect(named-mock).not.lastCalledWith(expected) +"expect(named-mock).not.lastCalledWith(...expected) -Expected mock function \\"named-mock\\" to not have been last called with: - [\\"foo\\", \\"bar\\"]" +Expected: not \\"foo\\", \\"bar\\" + +Number of calls: 1" `; exports[`lastCalledWith works only on spies or jest.fn 1`] = ` @@ -25,17 +26,19 @@ But it was not called." `; exports[`lastCalledWith works with Immutable.js objects 1`] = ` -"expect(jest.fn()).not.lastCalledWith(expected) +"expect(jest.fn()).not.lastCalledWith(...expected) + +Expected: not Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}} -Expected mock function to not have been last called with: - [Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}]" +Number of calls: 1" `; exports[`lastCalledWith works with Map 1`] = ` -"expect(jest.fn()).not.lastCalledWith(expected) +"expect(jest.fn()).not.lastCalledWith(...expected) -Expected mock function to not have been last called with: - [Map {1 => 2, 2 => 1}]" +Expected: not Map {1 => 2, 2 => 1} + +Number of calls: 1" `; exports[`lastCalledWith works with Map 2`] = ` @@ -60,10 +63,11 @@ Difference: `; exports[`lastCalledWith works with Set 1`] = ` -"expect(jest.fn()).not.lastCalledWith(expected) +"expect(jest.fn()).not.lastCalledWith(...expected) -Expected mock function to not have been last called with: - [Set {1, 2}]" +Expected: not Set {1, 2} + +Number of calls: 1" `; exports[`lastCalledWith works with Set 2`] = ` @@ -97,17 +101,22 @@ as argument 2, but it was called with `; exports[`lastCalledWith works with arguments that match 1`] = ` -"expect(jest.fn()).not.lastCalledWith(expected) +"expect(jest.fn()).not.lastCalledWith(...expected) + +Expected: not \\"foo\\", \\"bar\\" -Expected mock function to not have been last called with: - [\\"foo\\", \\"bar\\"]" +Number of calls: 1" `; exports[`lastCalledWith works with many arguments 1`] = ` -"expect(jest.fn()).not.lastCalledWith(expected) +"expect(jest.fn()).not.lastCalledWith(...expected) + +Expected: not \\"foo\\", \\"bar\\" +Received + 2: \\"foo\\", \\"bar1\\" + 3: \\"foo\\", \\"bar\\" -Expected mock function to not have been last called with: - [\\"foo\\", \\"bar\\"]" +Number of calls: 3" `; exports[`lastCalledWith works with many arguments that don't match 1`] = ` @@ -270,10 +279,12 @@ Number of returns: 1" `; exports[`nthCalledWith includes the custom mock name in the error message 1`] = ` -"expect(named-mock).not.nthCalledWith(expected) +"expect(named-mock).not.nthCalledWith(n, ...expected) -Expected mock function \\"named-mock\\" first call to not have been called with: - [\\"foo\\", \\"bar\\"]" +n: 1 +Expected: not \\"foo\\", \\"bar\\" + +Number of calls: 1" `; exports[`nthCalledWith negative throw matcher error for n that is not integer 1`] = ` @@ -313,10 +324,15 @@ as argument 1, but it was called with `; exports[`nthCalledWith should replace 1st, 2nd, 3rd with first, second, third 2`] = ` -"expect(jest.fn()).not.nthCalledWith(expected) +"expect(jest.fn()).not.nthCalledWith(n, ...expected) + +n: 1 +Expected: not \\"foo1\\", \\"bar\\" +Received +-> 1: \\"foo1\\", \\"bar\\" + 2: \\"foo\\", \\"bar1\\" -Expected mock function first call to not have been called with: - [\\"foo1\\", \\"bar\\"]" +Number of calls: 3" `; exports[`nthCalledWith works only on spies or jest.fn 1`] = ` @@ -337,17 +353,21 @@ But it was not called." `; exports[`nthCalledWith works with Immutable.js objects 1`] = ` -"expect(jest.fn()).not.nthCalledWith(expected) +"expect(jest.fn()).not.nthCalledWith(n, ...expected) -Expected mock function first call to not have been called with: - [Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}]" +n: 1 +Expected: not Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}} + +Number of calls: 1" `; exports[`nthCalledWith works with Map 1`] = ` -"expect(jest.fn()).not.nthCalledWith(expected) +"expect(jest.fn()).not.nthCalledWith(n, ...expected) + +n: 1 +Expected: not Map {1 => 2, 2 => 1} -Expected mock function first call to not have been called with: - [Map {1 => 2, 2 => 1}]" +Number of calls: 1" `; exports[`nthCalledWith works with Map 2`] = ` @@ -372,10 +392,12 @@ Difference: `; exports[`nthCalledWith works with Set 1`] = ` -"expect(jest.fn()).not.nthCalledWith(expected) +"expect(jest.fn()).not.nthCalledWith(n, ...expected) -Expected mock function first call to not have been called with: - [Set {1, 2}]" +n: 1 +Expected: not Set {1, 2} + +Number of calls: 1" `; exports[`nthCalledWith works with Set 2`] = ` @@ -409,17 +431,24 @@ as argument 2, but it was called with `; exports[`nthCalledWith works with arguments that match 1`] = ` -"expect(jest.fn()).not.nthCalledWith(expected) +"expect(jest.fn()).not.nthCalledWith(n, ...expected) -Expected mock function first call to not have been called with: - [\\"foo\\", \\"bar\\"]" +n: 1 +Expected: not \\"foo\\", \\"bar\\" + +Number of calls: 1" `; exports[`nthCalledWith works with three calls 1`] = ` -"expect(jest.fn()).not.nthCalledWith(expected) +"expect(jest.fn()).not.nthCalledWith(n, ...expected) + +n: 1 +Expected: not \\"foo1\\", \\"bar\\" +Received +-> 1: \\"foo1\\", \\"bar\\" + 2: \\"foo\\", \\"bar1\\" -Expected mock function first call to not have been called with: - [\\"foo1\\", \\"bar\\"]" +Number of calls: 3" `; exports[`nthCalledWith works with trailing undefined arguments 1`] = ` @@ -720,7 +749,7 @@ exports[`toBeCalled includes the custom mock name in the error message 1`] = ` Expected number of calls: 0 Received number of calls: 1 -1: called with no arguments" +1: called with 0 arguments" `; exports[`toBeCalled passes when called 1`] = ` @@ -886,10 +915,11 @@ Expected number of calls: not 2" `; exports[`toBeCalledWith includes the custom mock name in the error message 1`] = ` -"expect(named-mock).not.toBeCalledWith(expected) +"expect(named-mock).not.toBeCalledWith(...expected) -Expected mock function \\"named-mock\\" not to have been called with: - [\\"foo\\", \\"bar\\"]" +Expected: not \\"foo\\", \\"bar\\" + +Number of calls: 1" `; exports[`toBeCalledWith works only on spies or jest.fn 1`] = ` @@ -910,17 +940,19 @@ But it was not called." `; exports[`toBeCalledWith works with Immutable.js objects 1`] = ` -"expect(jest.fn()).not.toBeCalledWith(expected) +"expect(jest.fn()).not.toBeCalledWith(...expected) + +Expected: not Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}} -Expected mock function not to have been called with: - [Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}]" +Number of calls: 1" `; exports[`toBeCalledWith works with Map 1`] = ` -"expect(jest.fn()).not.toBeCalledWith(expected) +"expect(jest.fn()).not.toBeCalledWith(...expected) -Expected mock function not to have been called with: - [Map {1 => 2, 2 => 1}]" +Expected: not Map {1 => 2, 2 => 1} + +Number of calls: 1" `; exports[`toBeCalledWith works with Map 2`] = ` @@ -945,10 +977,11 @@ Difference: `; exports[`toBeCalledWith works with Set 1`] = ` -"expect(jest.fn()).not.toBeCalledWith(expected) +"expect(jest.fn()).not.toBeCalledWith(...expected) -Expected mock function not to have been called with: - [Set {1, 2}]" +Expected: not Set {1, 2} + +Number of calls: 1" `; exports[`toBeCalledWith works with Set 2`] = ` @@ -982,17 +1015,21 @@ as argument 2, but it was called with `; exports[`toBeCalledWith works with arguments that match 1`] = ` -"expect(jest.fn()).not.toBeCalledWith(expected) +"expect(jest.fn()).not.toBeCalledWith(...expected) + +Expected: not \\"foo\\", \\"bar\\" -Expected mock function not to have been called with: - [\\"foo\\", \\"bar\\"]" +Number of calls: 1" `; exports[`toBeCalledWith works with many arguments 1`] = ` -"expect(jest.fn()).not.toBeCalledWith(expected) +"expect(jest.fn()).not.toBeCalledWith(...expected) + +Expected: not \\"foo\\", \\"bar\\" +Received + 3: \\"foo\\", \\"bar\\" -Expected mock function not to have been called with: - [\\"foo\\", \\"bar\\"]" +Number of calls: 3" `; exports[`toBeCalledWith works with many arguments that don't match 1`] = ` @@ -1050,7 +1087,7 @@ exports[`toHaveBeenCalled includes the custom mock name in the error message 1`] Expected number of calls: 0 Received number of calls: 1 -1: called with no arguments" +1: called with 0 arguments" `; exports[`toHaveBeenCalled passes when called 1`] = ` @@ -1216,10 +1253,11 @@ Expected number of calls: not 2" `; exports[`toHaveBeenCalledWith includes the custom mock name in the error message 1`] = ` -"expect(named-mock).not.toHaveBeenCalledWith(expected) +"expect(named-mock).not.toHaveBeenCalledWith(...expected) -Expected mock function \\"named-mock\\" not to have been called with: - [\\"foo\\", \\"bar\\"]" +Expected: not \\"foo\\", \\"bar\\" + +Number of calls: 1" `; exports[`toHaveBeenCalledWith works only on spies or jest.fn 1`] = ` @@ -1240,17 +1278,19 @@ But it was not called." `; exports[`toHaveBeenCalledWith works with Immutable.js objects 1`] = ` -"expect(jest.fn()).not.toHaveBeenCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenCalledWith(...expected) + +Expected: not Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}} -Expected mock function not to have been called with: - [Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}]" +Number of calls: 1" `; exports[`toHaveBeenCalledWith works with Map 1`] = ` -"expect(jest.fn()).not.toHaveBeenCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenCalledWith(...expected) -Expected mock function not to have been called with: - [Map {1 => 2, 2 => 1}]" +Expected: not Map {1 => 2, 2 => 1} + +Number of calls: 1" `; exports[`toHaveBeenCalledWith works with Map 2`] = ` @@ -1275,10 +1315,11 @@ Difference: `; exports[`toHaveBeenCalledWith works with Set 1`] = ` -"expect(jest.fn()).not.toHaveBeenCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenCalledWith(...expected) -Expected mock function not to have been called with: - [Set {1, 2}]" +Expected: not Set {1, 2} + +Number of calls: 1" `; exports[`toHaveBeenCalledWith works with Set 2`] = ` @@ -1312,17 +1353,21 @@ as argument 2, but it was called with `; exports[`toHaveBeenCalledWith works with arguments that match 1`] = ` -"expect(jest.fn()).not.toHaveBeenCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenCalledWith(...expected) + +Expected: not \\"foo\\", \\"bar\\" -Expected mock function not to have been called with: - [\\"foo\\", \\"bar\\"]" +Number of calls: 1" `; exports[`toHaveBeenCalledWith works with many arguments 1`] = ` -"expect(jest.fn()).not.toHaveBeenCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenCalledWith(...expected) + +Expected: not \\"foo\\", \\"bar\\" +Received + 3: \\"foo\\", \\"bar\\" -Expected mock function not to have been called with: - [\\"foo\\", \\"bar\\"]" +Number of calls: 3" `; exports[`toHaveBeenCalledWith works with many arguments that don't match 1`] = ` @@ -1350,10 +1395,11 @@ Expected mock function to have been called with: `; exports[`toHaveBeenLastCalledWith includes the custom mock name in the error message 1`] = ` -"expect(named-mock).not.toHaveBeenLastCalledWith(expected) +"expect(named-mock).not.toHaveBeenLastCalledWith(...expected) -Expected mock function \\"named-mock\\" to not have been last called with: - [\\"foo\\", \\"bar\\"]" +Expected: not \\"foo\\", \\"bar\\" + +Number of calls: 1" `; exports[`toHaveBeenLastCalledWith works only on spies or jest.fn 1`] = ` @@ -1374,17 +1420,19 @@ But it was not called." `; exports[`toHaveBeenLastCalledWith works with Immutable.js objects 1`] = ` -"expect(jest.fn()).not.toHaveBeenLastCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenLastCalledWith(...expected) + +Expected: not Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}} -Expected mock function to not have been last called with: - [Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}]" +Number of calls: 1" `; exports[`toHaveBeenLastCalledWith works with Map 1`] = ` -"expect(jest.fn()).not.toHaveBeenLastCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenLastCalledWith(...expected) -Expected mock function to not have been last called with: - [Map {1 => 2, 2 => 1}]" +Expected: not Map {1 => 2, 2 => 1} + +Number of calls: 1" `; exports[`toHaveBeenLastCalledWith works with Map 2`] = ` @@ -1409,10 +1457,11 @@ Difference: `; exports[`toHaveBeenLastCalledWith works with Set 1`] = ` -"expect(jest.fn()).not.toHaveBeenLastCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenLastCalledWith(...expected) -Expected mock function to not have been last called with: - [Set {1, 2}]" +Expected: not Set {1, 2} + +Number of calls: 1" `; exports[`toHaveBeenLastCalledWith works with Set 2`] = ` @@ -1446,17 +1495,22 @@ as argument 2, but it was called with `; exports[`toHaveBeenLastCalledWith works with arguments that match 1`] = ` -"expect(jest.fn()).not.toHaveBeenLastCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenLastCalledWith(...expected) + +Expected: not \\"foo\\", \\"bar\\" -Expected mock function to not have been last called with: - [\\"foo\\", \\"bar\\"]" +Number of calls: 1" `; exports[`toHaveBeenLastCalledWith works with many arguments 1`] = ` -"expect(jest.fn()).not.toHaveBeenLastCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenLastCalledWith(...expected) + +Expected: not \\"foo\\", \\"bar\\" +Received + 2: \\"foo\\", \\"bar1\\" + 3: \\"foo\\", \\"bar\\" -Expected mock function to not have been last called with: - [\\"foo\\", \\"bar\\"]" +Number of calls: 3" `; exports[`toHaveBeenLastCalledWith works with many arguments that don't match 1`] = ` @@ -1476,10 +1530,12 @@ Expected mock function to have been last called with: `; exports[`toHaveBeenNthCalledWith includes the custom mock name in the error message 1`] = ` -"expect(named-mock).not.toHaveBeenNthCalledWith(expected) +"expect(named-mock).not.toHaveBeenNthCalledWith(n, ...expected) -Expected mock function \\"named-mock\\" first call to not have been called with: - [\\"foo\\", \\"bar\\"]" +n: 1 +Expected: not \\"foo\\", \\"bar\\" + +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith negative throw matcher error for n that is not integer 1`] = ` @@ -1519,10 +1575,15 @@ as argument 1, but it was called with `; exports[`toHaveBeenNthCalledWith should replace 1st, 2nd, 3rd with first, second, third 2`] = ` -"expect(jest.fn()).not.toHaveBeenNthCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenNthCalledWith(n, ...expected) + +n: 1 +Expected: not \\"foo1\\", \\"bar\\" +Received +-> 1: \\"foo1\\", \\"bar\\" + 2: \\"foo\\", \\"bar1\\" -Expected mock function first call to not have been called with: - [\\"foo1\\", \\"bar\\"]" +Number of calls: 3" `; exports[`toHaveBeenNthCalledWith works only on spies or jest.fn 1`] = ` @@ -1543,17 +1604,21 @@ But it was not called." `; exports[`toHaveBeenNthCalledWith works with Immutable.js objects 1`] = ` -"expect(jest.fn()).not.toHaveBeenNthCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenNthCalledWith(n, ...expected) -Expected mock function first call to not have been called with: - [Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}]" +n: 1 +Expected: not Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}} + +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with Map 1`] = ` -"expect(jest.fn()).not.toHaveBeenNthCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenNthCalledWith(n, ...expected) + +n: 1 +Expected: not Map {1 => 2, 2 => 1} -Expected mock function first call to not have been called with: - [Map {1 => 2, 2 => 1}]" +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with Map 2`] = ` @@ -1578,10 +1643,12 @@ Difference: `; exports[`toHaveBeenNthCalledWith works with Set 1`] = ` -"expect(jest.fn()).not.toHaveBeenNthCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenNthCalledWith(n, ...expected) -Expected mock function first call to not have been called with: - [Set {1, 2}]" +n: 1 +Expected: not Set {1, 2} + +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with Set 2`] = ` @@ -1615,17 +1682,24 @@ as argument 2, but it was called with `; exports[`toHaveBeenNthCalledWith works with arguments that match 1`] = ` -"expect(jest.fn()).not.toHaveBeenNthCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenNthCalledWith(n, ...expected) -Expected mock function first call to not have been called with: - [\\"foo\\", \\"bar\\"]" +n: 1 +Expected: not \\"foo\\", \\"bar\\" + +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with three calls 1`] = ` -"expect(jest.fn()).not.toHaveBeenNthCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenNthCalledWith(n, ...expected) + +n: 1 +Expected: not \\"foo1\\", \\"bar\\" +Received +-> 1: \\"foo1\\", \\"bar\\" + 2: \\"foo\\", \\"bar1\\" -Expected mock function first call to not have been called with: - [\\"foo1\\", \\"bar\\"]" +Number of calls: 3" `; exports[`toHaveBeenNthCalledWith works with trailing undefined arguments 1`] = ` diff --git a/packages/expect/src/spyMatchers.ts b/packages/expect/src/spyMatchers.ts index 3e6360332173..64b811c3c8e0 100644 --- a/packages/expect/src/spyMatchers.ts +++ b/packages/expect/src/spyMatchers.ts @@ -27,11 +27,21 @@ const PRINT_LIMIT = 3; const CALL_PRINT_LIMIT = 3; const LAST_CALL_PRINT_LIMIT = 1; +const NO_ARGUMENTS = 'called with 0 arguments'; + +const printExpectedArgs = (args: Array): string => + args.length === 0 + ? NO_ARGUMENTS + : args.map(arg => printExpected(arg)).join(', '); + const printReceivedArgs = (args: Array): string => args.length === 0 - ? 'called with no arguments' + ? NO_ARGUMENTS : args.map(arg => printReceived(arg)).join(', '); +const isEqualCall = (expected: unknown, args: any): boolean => + equals(expected, args, [iterableEquality]); + const isEqualReturn = (expected: unknown, result: any): boolean => result.type === 'return' && equals(expected, result.value, [iterableEquality]); @@ -68,6 +78,40 @@ const getRightAlignedPrinter = (label: string): PrintLabel => { suffix; }; +type IndexedCall = [number, Array]; + +// Return either empty string or one line per indexed result, +// so additional empty line can separate from `Number of returns` which follows. +const printReceivedCalls = ( + label: string, + indexedCalls: Array, + isOnlyCall: boolean, + iExpectedCall?: number, +) => { + if (indexedCalls.length === 0) { + return ''; + } + + if (isOnlyCall) { + return label + printReceivedArgs(indexedCalls[0]) + '\n'; + } + + const printAligned = getRightAlignedPrinter(label); + + return ( + label.replace(':', '').trim() + + '\n' + + indexedCalls.reduce( + (printed: string, [i, args]: IndexedCall) => + printed + + printAligned(String(i + 1), i === iExpectedCall) + + printReceivedArgs(args) + + '\n', + '', + ) + ); +}; + const printResult = (result: any) => result.type === 'throw' ? 'function call threw an error' @@ -309,7 +353,7 @@ const createToBeCalledWithMatcher = (matcherName: string) => isNot: this.isNot, promise: this.promise, }; - ensureMockOrSpy(received, matcherName.slice(1), expectedArgument, options); + ensureMockOrSpy(received, matcherName, expectedArgument, options); const receivedIsSpy = isSpy(received); const type = receivedIsSpy ? 'spy' : 'mock function'; @@ -323,19 +367,37 @@ const createToBeCalledWithMatcher = (matcherName: string) => ? received.calls.all().map((x: any) => x.args) : received.mock.calls; - const [match, fail] = partition(calls, call => - equals(call, expected, [iterableEquality]), - ); + const [match, fail] = partition(calls, call => isEqualCall(expected, call)); const pass = match.length > 0; const message = pass - ? () => - matcherHint('.not' + matcherName, receivedName) + - '\n\n' + - `Expected ${identifier} not to have been called with:\n` + - ` ${printExpected(expected)}` + ? () => { + // Some examples of calls that are equal to expected value. + const indexedCalls: Array = []; + let i = 0; + while (i < calls.length && indexedCalls.length < PRINT_LIMIT) { + if (isEqualCall(expected, calls[i])) { + indexedCalls.push([i, calls[i]]); + } + i += 1; + } + + return ( + matcherHint(matcherName, receivedName, expectedArgument, options) + + '\n\n' + + `Expected: not ${printExpectedArgs(expected)}\n` + + (calls.length === 1 && stringify(calls[0]) === stringify(expected) + ? '' + : printReceivedCalls( + 'Received: ', + indexedCalls, + calls.length === 1, + )) + + `\nNumber of calls: ${printReceived(calls.length)}` + ); + } : () => - matcherHint(matcherName, receivedName) + + matcherHint('.' + matcherName, receivedName) + '\n\n' + `Expected ${identifier} to have been called with:\n` + formatMismatchedCalls(fail, expected, CALL_PRINT_LIMIT); @@ -425,7 +487,7 @@ const createLastCalledWithMatcher = (matcherName: string) => isNot: this.isNot, promise: this.promise, }; - ensureMockOrSpy(received, matcherName.slice(1), expectedArgument, options); + ensureMockOrSpy(received, matcherName, expectedArgument, options); const receivedIsSpy = isSpy(received); const type = receivedIsSpy ? 'spy' : 'mock function'; @@ -434,19 +496,39 @@ const createLastCalledWithMatcher = (matcherName: string) => receivedIsSpy || receivedName === 'jest.fn()' ? type : `${type} "${receivedName}"`; + const calls = receivedIsSpy ? received.calls.all().map((x: any) => x.args) : received.mock.calls; - const pass = equals(calls[calls.length - 1], expected, [iterableEquality]); + const iLast = calls.length - 1; + + const pass = iLast >= 0 && isEqualCall(expected, calls[iLast]); const message = pass - ? () => - matcherHint('.not' + matcherName, receivedName) + - '\n\n' + - `Expected ${identifier} to not have been last called with:\n` + - ` ${printExpected(expected)}` + ? () => { + const indexedCalls: Array = []; + if (iLast > 0) { + // Display preceding call as context. + indexedCalls.push([iLast - 1, calls[iLast - 1]]); + } + indexedCalls.push([iLast, calls[iLast]]); + + return ( + matcherHint(matcherName, receivedName, expectedArgument, options) + + '\n\n' + + `Expected: not ${printExpectedArgs(expected)}\n` + + (calls.length === 1 && stringify(calls[0]) === stringify(expected) + ? '' + : printReceivedCalls( + 'Received: ', + indexedCalls, + calls.length === 1, + )) + + `\nNumber of calls: ${printReceived(calls.length)}` + ); + } : () => - matcherHint(matcherName, receivedName) + + matcherHint('.' + matcherName, receivedName) + '\n\n' + `Expected ${identifier} to have been last called with:\n` + formatMismatchedCalls(calls, expected, LAST_CALL_PRINT_LIMIT); @@ -549,17 +631,12 @@ const createNthCalledWithMatcher = (matcherName: string) => promise: this.promise, secondArgument: '...expected', }; - ensureMockOrSpy(received, matcherName.slice(1), expectedArgument, options); + ensureMockOrSpy(received, matcherName, expectedArgument, options); if (!Number.isSafeInteger(nth) || nth < 1) { throw new Error( matcherErrorMessage( - matcherHint( - matcherName.slice(1), - undefined, - expectedArgument, - options, - ), + matcherHint(matcherName, undefined, expectedArgument, options), `${EXPECTED_COLOR(expectedArgument)} must be a positive integer`, printWithType(expectedArgument, nth, printExpected), ), @@ -574,21 +651,46 @@ const createNthCalledWithMatcher = (matcherName: string) => receivedIsSpy || receivedName === 'jest.fn()' ? type : `${type} "${receivedName}"`; + const calls = receivedIsSpy ? received.calls.all().map((x: any) => x.args) : received.mock.calls; - const pass = equals(calls[nth - 1], expected, [iterableEquality]); + const length = calls.length; + const iNth = nth - 1; + + const pass = iNth < length && isEqualCall(expected, calls[iNth]); const message = pass - ? () => - matcherHint('.not' + matcherName, receivedName) + - '\n\n' + - `Expected ${identifier} ${nthToString( - nth, - )} call to not have been called with:\n` + - ` ${printExpected(expected)}` + ? () => { + // Display preceding and following calls, + // in case assertions fails because index is off by one. + const indexedCalls: Array = []; + if (iNth - 1 >= 0) { + indexedCalls.push([iNth - 1, calls[iNth - 1]]); + } + indexedCalls.push([iNth, calls[iNth]]); + if (iNth + 1 < length) { + indexedCalls.push([iNth + 1, calls[iNth + 1]]); + } + + return ( + matcherHint(matcherName, receivedName, expectedArgument, options) + + '\n\n' + + `n: ${nth}\n` + + `Expected: not ${printExpectedArgs(expected)}\n` + + (calls.length === 1 && stringify(calls[0]) === stringify(expected) + ? '' + : printReceivedCalls( + 'Received: ', + indexedCalls, + calls.length === 1, + iNth, + )) + + `\nNumber of calls: ${printReceived(calls.length)}` + ); + } : () => - matcherHint(matcherName, receivedName) + + matcherHint('.' + matcherName, receivedName) + '\n\n' + `Expected ${identifier} ${nthToString( nth, @@ -730,21 +832,21 @@ const createNthReturnedWithMatcher = (matcherName: string) => }; const spyMatchers: MatchersObject = { - lastCalledWith: createLastCalledWithMatcher('.lastCalledWith'), + lastCalledWith: createLastCalledWithMatcher('lastCalledWith'), lastReturnedWith: createLastReturnedMatcher('lastReturnedWith'), - nthCalledWith: createNthCalledWithMatcher('.nthCalledWith'), + nthCalledWith: createNthCalledWithMatcher('nthCalledWith'), nthReturnedWith: createNthReturnedWithMatcher('nthReturnedWith'), toBeCalled: createToBeCalledMatcher('toBeCalled'), toBeCalledTimes: createToBeCalledTimesMatcher('toBeCalledTimes'), - toBeCalledWith: createToBeCalledWithMatcher('.toBeCalledWith'), + toBeCalledWith: createToBeCalledWithMatcher('toBeCalledWith'), toHaveBeenCalled: createToBeCalledMatcher('toHaveBeenCalled'), toHaveBeenCalledTimes: createToBeCalledTimesMatcher('toHaveBeenCalledTimes'), - toHaveBeenCalledWith: createToBeCalledWithMatcher('.toHaveBeenCalledWith'), + toHaveBeenCalledWith: createToBeCalledWithMatcher('toHaveBeenCalledWith'), toHaveBeenLastCalledWith: createLastCalledWithMatcher( - '.toHaveBeenLastCalledWith', + 'toHaveBeenLastCalledWith', ), toHaveBeenNthCalledWith: createNthCalledWithMatcher( - '.toHaveBeenNthCalledWith', + 'toHaveBeenNthCalledWith', ), toHaveLastReturnedWith: createLastReturnedMatcher('toHaveLastReturnedWith'), toHaveNthReturnedWith: createNthReturnedWithMatcher('toHaveNthReturnedWith'), From 63d96fe4af43851b0efb56d134fd9e81c854f071 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Fri, 26 Jul 2019 16:19:48 -0400 Subject: [PATCH 2/5] Add missing argument in printReceivedCalls for LastCalled --- .../src/__tests__/__snapshots__/spyMatchers.test.js.snap | 4 ++-- packages/expect/src/spyMatchers.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap index a9aeb6eda19c..b2c17d4f8e60 100644 --- a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap @@ -114,7 +114,7 @@ exports[`lastCalledWith works with many arguments 1`] = ` Expected: not \\"foo\\", \\"bar\\" Received 2: \\"foo\\", \\"bar1\\" - 3: \\"foo\\", \\"bar\\" +-> 3: \\"foo\\", \\"bar\\" Number of calls: 3" `; @@ -1508,7 +1508,7 @@ exports[`toHaveBeenLastCalledWith works with many arguments 1`] = ` Expected: not \\"foo\\", \\"bar\\" Received 2: \\"foo\\", \\"bar1\\" - 3: \\"foo\\", \\"bar\\" +-> 3: \\"foo\\", \\"bar\\" Number of calls: 3" `; diff --git a/packages/expect/src/spyMatchers.ts b/packages/expect/src/spyMatchers.ts index 64b811c3c8e0..05918a679490 100644 --- a/packages/expect/src/spyMatchers.ts +++ b/packages/expect/src/spyMatchers.ts @@ -523,6 +523,7 @@ const createLastCalledWithMatcher = (matcherName: string) => 'Received: ', indexedCalls, calls.length === 1, + iLast, )) + `\nNumber of calls: ${printReceived(calls.length)}` ); From 7bb2177fb5aa933c13c98de33c4ea23f3ce036bb Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Fri, 26 Jul 2019 16:22:47 -0400 Subject: [PATCH 3/5] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 208b90faf636..875d67cc3678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `[expect]` Improve report when mock-spy matcher fails, part 3 ([#8697](https://github.com/facebook/jest/pull/8697)) - `[expect]` Improve report when mock-spy matcher fails, part 4 ([#8710](https://github.com/facebook/jest/pull/8710)) - `[expect]` Throw matcher error when received cannot be jasmine spy ([#8747](https://github.com/facebook/jest/pull/8747)) +- `[expect]` Improve report when negative CalledWith assertion fails ([#8755](https://github.com/facebook/jest/pull/8755)) - `[jest-snapshot]` Highlight substring differences when matcher fails, part 3 ([#8569](https://github.com/facebook/jest/pull/8569)) - `[jest-cli]` Improve chai support (with detailed output, to match jest exceptions) ([#8454](https://github.com/facebook/jest/pull/8454)) - `[*]` Manage the global timeout with `--testTimeout` command line argument. ([#8456](https://github.com/facebook/jest/pull/8456)) From 723cbd67a37f66b5a364c2c3bce8522d5c5799ce Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Mon, 29 Jul 2019 10:31:50 -0400 Subject: [PATCH 4/5] Remove label argument from printReceivedCalls --- packages/expect/src/spyMatchers.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/expect/src/spyMatchers.ts b/packages/expect/src/spyMatchers.ts index 05918a679490..7f61b30f2f7f 100644 --- a/packages/expect/src/spyMatchers.ts +++ b/packages/expect/src/spyMatchers.ts @@ -82,8 +82,7 @@ type IndexedCall = [number, Array]; // Return either empty string or one line per indexed result, // so additional empty line can separate from `Number of returns` which follows. -const printReceivedCalls = ( - label: string, +const printReceivedCallsNegative = ( indexedCalls: Array, isOnlyCall: boolean, iExpectedCall?: number, @@ -92,6 +91,7 @@ const printReceivedCalls = ( return ''; } + const label = 'Received: '; if (isOnlyCall) { return label + printReceivedArgs(indexedCalls[0]) + '\n'; } @@ -99,8 +99,7 @@ const printReceivedCalls = ( const printAligned = getRightAlignedPrinter(label); return ( - label.replace(':', '').trim() + - '\n' + + 'Received\n' + indexedCalls.reduce( (printed: string, [i, args]: IndexedCall) => printed + @@ -388,11 +387,7 @@ const createToBeCalledWithMatcher = (matcherName: string) => `Expected: not ${printExpectedArgs(expected)}\n` + (calls.length === 1 && stringify(calls[0]) === stringify(expected) ? '' - : printReceivedCalls( - 'Received: ', - indexedCalls, - calls.length === 1, - )) + + : printReceivedCallsNegative(indexedCalls, calls.length === 1)) + `\nNumber of calls: ${printReceived(calls.length)}` ); } @@ -519,8 +514,7 @@ const createLastCalledWithMatcher = (matcherName: string) => `Expected: not ${printExpectedArgs(expected)}\n` + (calls.length === 1 && stringify(calls[0]) === stringify(expected) ? '' - : printReceivedCalls( - 'Received: ', + : printReceivedCallsNegative( indexedCalls, calls.length === 1, iLast, @@ -681,8 +675,7 @@ const createNthCalledWithMatcher = (matcherName: string) => `Expected: not ${printExpectedArgs(expected)}\n` + (calls.length === 1 && stringify(calls[0]) === stringify(expected) ? '' - : printReceivedCalls( - 'Received: ', + : printReceivedCallsNegative( indexedCalls, calls.length === 1, iNth, From d8ec52fb13502fc2b7cce85606b6487a5f9377de Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Tue, 30 Jul 2019 16:14:28 -0400 Subject: [PATCH 5/5] Display equal args with dim color and delete redundant tests --- .../__snapshots__/spyMatchers.test.js.snap | 58 +++---------------- .../expect/src/__tests__/spyMatchers.test.js | 21 ------- packages/expect/src/spyMatchers.ts | 54 ++++++++++++----- 3 files changed, 49 insertions(+), 84 deletions(-) diff --git a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap index b2c17d4f8e60..73a8f011f7d1 100644 --- a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap @@ -113,8 +113,8 @@ exports[`lastCalledWith works with many arguments 1`] = ` Expected: not \\"foo\\", \\"bar\\" Received - 2: \\"foo\\", \\"bar1\\" --> 3: \\"foo\\", \\"bar\\" + 2: \\"foo\\", \\"bar1\\" +-> 3: \\"foo\\", \\"bar\\" Number of calls: 3" `; @@ -314,27 +314,6 @@ n has type: number n has value: 0" `; -exports[`nthCalledWith should replace 1st, 2nd, 3rd with first, second, third 1`] = ` -"expect(jest.fn()).nthCalledWith(expected) - -Expected mock function first call to have been called with: - \\"foo\\" -as argument 1, but it was called with - \\"foo1\\"." -`; - -exports[`nthCalledWith should replace 1st, 2nd, 3rd with first, second, third 2`] = ` -"expect(jest.fn()).not.nthCalledWith(n, ...expected) - -n: 1 -Expected: not \\"foo1\\", \\"bar\\" -Received --> 1: \\"foo1\\", \\"bar\\" - 2: \\"foo\\", \\"bar1\\" - -Number of calls: 3" -`; - exports[`nthCalledWith works only on spies or jest.fn 1`] = ` "expect(received).nthCalledWith(n, ...expected) @@ -445,7 +424,7 @@ exports[`nthCalledWith works with three calls 1`] = ` n: 1 Expected: not \\"foo1\\", \\"bar\\" Received --> 1: \\"foo1\\", \\"bar\\" +-> 1: \\"foo1\\", \\"bar\\" 2: \\"foo\\", \\"bar1\\" Number of calls: 3" @@ -1027,7 +1006,7 @@ exports[`toBeCalledWith works with many arguments 1`] = ` Expected: not \\"foo\\", \\"bar\\" Received - 3: \\"foo\\", \\"bar\\" + 3: \\"foo\\", \\"bar\\" Number of calls: 3" `; @@ -1365,7 +1344,7 @@ exports[`toHaveBeenCalledWith works with many arguments 1`] = ` Expected: not \\"foo\\", \\"bar\\" Received - 3: \\"foo\\", \\"bar\\" + 3: \\"foo\\", \\"bar\\" Number of calls: 3" `; @@ -1507,8 +1486,8 @@ exports[`toHaveBeenLastCalledWith works with many arguments 1`] = ` Expected: not \\"foo\\", \\"bar\\" Received - 2: \\"foo\\", \\"bar1\\" --> 3: \\"foo\\", \\"bar\\" + 2: \\"foo\\", \\"bar1\\" +-> 3: \\"foo\\", \\"bar\\" Number of calls: 3" `; @@ -1565,27 +1544,6 @@ n has type: number n has value: 0" `; -exports[`toHaveBeenNthCalledWith should replace 1st, 2nd, 3rd with first, second, third 1`] = ` -"expect(jest.fn()).toHaveBeenNthCalledWith(expected) - -Expected mock function first call to have been called with: - \\"foo\\" -as argument 1, but it was called with - \\"foo1\\"." -`; - -exports[`toHaveBeenNthCalledWith should replace 1st, 2nd, 3rd with first, second, third 2`] = ` -"expect(jest.fn()).not.toHaveBeenNthCalledWith(n, ...expected) - -n: 1 -Expected: not \\"foo1\\", \\"bar\\" -Received --> 1: \\"foo1\\", \\"bar\\" - 2: \\"foo\\", \\"bar1\\" - -Number of calls: 3" -`; - exports[`toHaveBeenNthCalledWith works only on spies or jest.fn 1`] = ` "expect(received).toHaveBeenNthCalledWith(n, ...expected) @@ -1696,7 +1654,7 @@ exports[`toHaveBeenNthCalledWith works with three calls 1`] = ` n: 1 Expected: not \\"foo1\\", \\"bar\\" Received --> 1: \\"foo1\\", \\"bar\\" +-> 1: \\"foo1\\", \\"bar\\" 2: \\"foo\\", \\"bar1\\" Number of calls: 3" diff --git a/packages/expect/src/__tests__/spyMatchers.test.js b/packages/expect/src/__tests__/spyMatchers.test.js index c6ce6366a124..db6322d736eb 100644 --- a/packages/expect/src/__tests__/spyMatchers.test.js +++ b/packages/expect/src/__tests__/spyMatchers.test.js @@ -347,27 +347,6 @@ const createSpy = fn => { expect(() => { jestExpect(fn).not[calledWith](1, 'foo1', 'bar'); - jestExpect(fn).not[calledWith](2, 'foo', 'bar1'); - jestExpect(fn).not[calledWith](3, 'foo', 'bar'); - }).toThrowErrorMatchingSnapshot(); - }); - - test('should replace 1st, 2nd, 3rd with first, second, third', async () => { - const fn = jest.fn(); - fn('foo1', 'bar'); - fn('foo', 'bar1'); - fn('foo', 'bar'); - - expect(() => { - jestExpect(fn)[calledWith](1, 'foo', 'bar'); - jestExpect(fn)[calledWith](2, 'foo', 'bar'); - jestExpect(fn)[calledWith](3, 'foo1', 'bar'); - }).toThrowErrorMatchingSnapshot(); - - expect(() => { - jestExpect(fn).not[calledWith](1, 'foo1', 'bar'); - jestExpect(fn).not[calledWith](2, 'foo', 'bar1'); - jestExpect(fn).not[calledWith](3, 'foo', 'bar'); }).toThrowErrorMatchingSnapshot(); }); diff --git a/packages/expect/src/spyMatchers.ts b/packages/expect/src/spyMatchers.ts index 7f61b30f2f7f..7be62b35d270 100644 --- a/packages/expect/src/spyMatchers.ts +++ b/packages/expect/src/spyMatchers.ts @@ -9,6 +9,7 @@ import { diff, ensureExpectedIsNumber, ensureNoExpected, + DIM_COLOR, EXPECTED_COLOR, matcherErrorMessage, matcherHint, @@ -29,18 +30,36 @@ const LAST_CALL_PRINT_LIMIT = 1; const NO_ARGUMENTS = 'called with 0 arguments'; -const printExpectedArgs = (args: Array): string => - args.length === 0 +const printExpectedArgs = (expected: Array): string => + expected.length === 0 ? NO_ARGUMENTS - : args.map(arg => printExpected(arg)).join(', '); + : expected.map(arg => printExpected(arg)).join(', '); -const printReceivedArgs = (args: Array): string => - args.length === 0 +const printReceivedArgs = ( + received: Array, + expected?: Array, +): string => + received.length === 0 ? NO_ARGUMENTS - : args.map(arg => printReceived(arg)).join(', '); - -const isEqualCall = (expected: unknown, args: any): boolean => - equals(expected, args, [iterableEquality]); + : received + .map((arg, i) => + Array.isArray(expected) && + i < expected.length && + isEqualValue(expected[i], arg) + ? printCommon(arg) + : printReceived(arg), + ) + .join(', '); + +const printCommon = (val: unknown) => DIM_COLOR(stringify(val)); + +const isEqualValue = (expected: unknown, received: unknown): boolean => + equals(expected, received, [iterableEquality]); + +const isEqualCall = ( + expected: Array, + received: Array, +): boolean => equals(expected, received, [iterableEquality]); const isEqualReturn = (expected: unknown, result: any): boolean => result.type === 'return' && @@ -83,6 +102,7 @@ type IndexedCall = [number, Array]; // Return either empty string or one line per indexed result, // so additional empty line can separate from `Number of returns` which follows. const printReceivedCallsNegative = ( + expected: Array, indexedCalls: Array, isOnlyCall: boolean, iExpectedCall?: number, @@ -93,7 +113,7 @@ const printReceivedCallsNegative = ( const label = 'Received: '; if (isOnlyCall) { - return label + printReceivedArgs(indexedCalls[0]) + '\n'; + return label + printReceivedArgs(indexedCalls[0], expected) + '\n'; } const printAligned = getRightAlignedPrinter(label); @@ -104,7 +124,7 @@ const printReceivedCallsNegative = ( (printed: string, [i, args]: IndexedCall) => printed + printAligned(String(i + 1), i === iExpectedCall) + - printReceivedArgs(args) + + printReceivedArgs(args, expected) + '\n', '', ) @@ -366,7 +386,9 @@ const createToBeCalledWithMatcher = (matcherName: string) => ? received.calls.all().map((x: any) => x.args) : received.mock.calls; - const [match, fail] = partition(calls, call => isEqualCall(expected, call)); + const [match, fail] = partition(calls, call => + isEqualCall(expected, call as Array), + ); const pass = match.length > 0; const message = pass @@ -387,7 +409,11 @@ const createToBeCalledWithMatcher = (matcherName: string) => `Expected: not ${printExpectedArgs(expected)}\n` + (calls.length === 1 && stringify(calls[0]) === stringify(expected) ? '' - : printReceivedCallsNegative(indexedCalls, calls.length === 1)) + + : printReceivedCallsNegative( + expected, + indexedCalls, + calls.length === 1, + )) + `\nNumber of calls: ${printReceived(calls.length)}` ); } @@ -515,6 +541,7 @@ const createLastCalledWithMatcher = (matcherName: string) => (calls.length === 1 && stringify(calls[0]) === stringify(expected) ? '' : printReceivedCallsNegative( + expected, indexedCalls, calls.length === 1, iLast, @@ -676,6 +703,7 @@ const createNthCalledWithMatcher = (matcherName: string) => (calls.length === 1 && stringify(calls[0]) === stringify(expected) ? '' : printReceivedCallsNegative( + expected, indexedCalls, calls.length === 1, iNth,