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)) diff --git a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap index c25156a340cd..73a8f011f7d1 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: not Map {1 => 2, 2 => 1} -Expected mock function to not have been last called with: - [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: not Set {1, 2} -Expected mock function to not have been last called with: - [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 mock function to not have been last called with: - [\\"foo\\", \\"bar\\"]" +Expected: not \\"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) + +n: 1 +Expected: not \\"foo\\", \\"bar\\" -Expected mock function \\"named-mock\\" first call to not have been called with: - [\\"foo\\", \\"bar\\"]" +Number of calls: 1" `; exports[`nthCalledWith negative throw matcher error for n that is not integer 1`] = ` @@ -303,22 +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(expected) - -Expected mock function first call to not have been called with: - [\\"foo1\\", \\"bar\\"]" -`; - exports[`nthCalledWith works only on spies or jest.fn 1`] = ` "expect(received).nthCalledWith(n, ...expected) @@ -337,17 +332,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) + +n: 1 +Expected: not Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}}, Immutable.Map {\\"a\\": {\\"b\\": \\"c\\"}} -Expected mock function first call to not have been called with: - [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 +371,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 +410,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) + +n: 1 +Expected: not \\"foo\\", \\"bar\\" -Expected mock function first call to not have been called with: - [\\"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) -Expected mock function first call to not have been called with: - [\\"foo1\\", \\"bar\\"]" +n: 1 +Expected: not \\"foo1\\", \\"bar\\" +Received +-> 1: \\"foo1\\", \\"bar\\" + 2: \\"foo\\", \\"bar1\\" + +Number of calls: 3" `; exports[`nthCalledWith works with trailing undefined arguments 1`] = ` @@ -720,7 +728,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 +894,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: not \\"foo\\", \\"bar\\" -Expected mock function \\"named-mock\\" not to have been called with: - [\\"foo\\", \\"bar\\"]" +Number of calls: 1" `; exports[`toBeCalledWith works only on spies or jest.fn 1`] = ` @@ -910,17 +919,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: not Map {1 => 2, 2 => 1} -Expected mock function not to have been called with: - [Map {1 => 2, 2 => 1}]" +Number of calls: 1" `; exports[`toBeCalledWith works with Map 2`] = ` @@ -945,10 +956,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 +994,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 +1066,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 +1232,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 +1257,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: not Map {1 => 2, 2 => 1} -Expected mock function not to have been called with: - [Map {1 => 2, 2 => 1}]" +Number of calls: 1" `; exports[`toHaveBeenCalledWith works with Map 2`] = ` @@ -1275,10 +1294,11 @@ Difference: `; exports[`toHaveBeenCalledWith works with Set 1`] = ` -"expect(jest.fn()).not.toHaveBeenCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenCalledWith(...expected) + +Expected: not Set {1, 2} -Expected mock function not to have been called with: - [Set {1, 2}]" +Number of calls: 1" `; exports[`toHaveBeenCalledWith works with Set 2`] = ` @@ -1312,17 +1332,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 mock function not to have been called with: - [\\"foo\\", \\"bar\\"]" +Expected: not \\"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 +1374,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 +1399,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 +1436,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 +1474,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 mock function to not have been last called with: - [\\"foo\\", \\"bar\\"]" +Expected: not \\"foo\\", \\"bar\\" +Received + 2: \\"foo\\", \\"bar1\\" +-> 3: \\"foo\\", \\"bar\\" + +Number of calls: 3" `; exports[`toHaveBeenLastCalledWith works with many arguments that don't match 1`] = ` @@ -1476,10 +1509,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`] = ` @@ -1509,22 +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(expected) - -Expected mock function first call to not have been called with: - [\\"foo1\\", \\"bar\\"]" -`; - exports[`toHaveBeenNthCalledWith works only on spies or jest.fn 1`] = ` "expect(received).toHaveBeenNthCalledWith(n, ...expected) @@ -1543,17 +1562,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) -Expected mock function first call to not have been called with: - [Map {1 => 2, 2 => 1}]" +n: 1 +Expected: not Map {1 => 2, 2 => 1} + +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with Map 2`] = ` @@ -1578,10 +1601,12 @@ Difference: `; exports[`toHaveBeenNthCalledWith works with Set 1`] = ` -"expect(jest.fn()).not.toHaveBeenNthCalledWith(expected) +"expect(jest.fn()).not.toHaveBeenNthCalledWith(n, ...expected) + +n: 1 +Expected: not Set {1, 2} -Expected mock function first call to not have been called with: - [Set {1, 2}]" +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with Set 2`] = ` @@ -1615,17 +1640,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/__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 3e6360332173..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, @@ -27,10 +28,38 @@ const PRINT_LIMIT = 3; const CALL_PRINT_LIMIT = 3; const LAST_CALL_PRINT_LIMIT = 1; -const printReceivedArgs = (args: Array): string => - args.length === 0 - ? 'called with no arguments' - : args.map(arg => printReceived(arg)).join(', '); +const NO_ARGUMENTS = 'called with 0 arguments'; + +const printExpectedArgs = (expected: Array): string => + expected.length === 0 + ? NO_ARGUMENTS + : expected.map(arg => printExpected(arg)).join(', '); + +const printReceivedArgs = ( + received: Array, + expected?: Array, +): string => + received.length === 0 + ? NO_ARGUMENTS + : 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' && @@ -68,6 +97,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 printReceivedCallsNegative = ( + expected: Array, + indexedCalls: Array, + isOnlyCall: boolean, + iExpectedCall?: number, +) => { + if (indexedCalls.length === 0) { + return ''; + } + + const label = 'Received: '; + if (isOnlyCall) { + return label + printReceivedArgs(indexedCalls[0], expected) + '\n'; + } + + const printAligned = getRightAlignedPrinter(label); + + return ( + 'Received\n' + + indexedCalls.reduce( + (printed: string, [i, args]: IndexedCall) => + printed + + printAligned(String(i + 1), i === iExpectedCall) + + printReceivedArgs(args, expected) + + '\n', + '', + ) + ); +}; + const printResult = (result: any) => result.type === 'throw' ? 'function call threw an error' @@ -309,7 +372,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'; @@ -324,18 +387,38 @@ const createToBeCalledWithMatcher = (matcherName: string) => : received.mock.calls; const [match, fail] = partition(calls, call => - equals(call, expected, [iterableEquality]), + isEqualCall(expected, call as Array), ); 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) + ? '' + : printReceivedCallsNegative( + expected, + 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 +508,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 +517,40 @@ 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) + ? '' + : printReceivedCallsNegative( + expected, + indexedCalls, + calls.length === 1, + iLast, + )) + + `\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 +653,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 +673,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) + ? '' + : printReceivedCallsNegative( + expected, + 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 +854,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'),