From 2a004bfdb17f12b1cc0b2a6d7e3620aecd095260 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Sun, 4 Aug 2019 13:57:18 -0400 Subject: [PATCH] expect: Improve report when positive CalledWith assertion fails (#8771) * expect: Improve report when positive CalledWith assertion fails * Replace isOnlyCallLineDiffable variable with inline call * Exchange operands in or condition * Update CHANGELOG.md --- CHANGELOG.md | 1 + .../__snapshots__/spyMatchers.test.js.snap | 384 +++++++--------- packages/expect/src/spyMatchers.ts | 420 +++++++++++++----- 3 files changed, 472 insertions(+), 333 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3985721a9662..696eeb23d138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - `[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)) +- `[expect]` Improve report when positive CalledWith assertion fails ([#8771](https://github.com/facebook/jest/pull/8771)) - `[jest-snapshot]` Highlight substring differences when matcher fails, part 3 ([#8569](https://github.com/facebook/jest/pull/8569)) - `[jest-core]` Improve report when snapshots are obsolete ([#8448](https://github.com/facebook/jest/pull/8665)) - `[jest-cli]` Improve chai support (with detailed output, to match jest exceptions) ([#8454](https://github.com/facebook/jest/pull/8454)) diff --git a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap index 73a8f011f7d1..c22e8bdf327e 100644 --- a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.js.snap @@ -18,11 +18,11 @@ Received has value: [Function fn]" `; exports[`lastCalledWith works when not called 1`] = ` -"expect(jest.fn()).lastCalledWith(expected) +"expect(jest.fn()).lastCalledWith(...expected) -Expected mock function to have been last called with: - [\\"foo\\", \\"bar\\"] -But it was not called." +Expected: \\"foo\\", \\"bar\\" + +Number of calls: 0" `; exports[`lastCalledWith works with Immutable.js objects 1`] = ` @@ -42,14 +42,7 @@ Number of calls: 1" `; exports[`lastCalledWith works with Map 2`] = ` -"expect(jest.fn()).lastCalledWith(expected) - -Expected mock function to have been last called with: - Map {\\"a\\" => \\"b\\", \\"b\\" => \\"a\\"} -as argument 1, but it was called with - Map {1 => 2, 2 => 1}. - -Difference: +"expect(jest.fn()).lastCalledWith(...expected) - Expected + Received @@ -59,7 +52,9 @@ Difference: - \\"b\\" => \\"a\\", + 1 => 2, + 2 => 1, - }" + }, + +Number of calls: 1" `; exports[`lastCalledWith works with Set 1`] = ` @@ -71,14 +66,7 @@ Number of calls: 1" `; exports[`lastCalledWith works with Set 2`] = ` -"expect(jest.fn()).lastCalledWith(expected) - -Expected mock function to have been last called with: - Set {3, 4} -as argument 1, but it was called with - Set {1, 2}. - -Difference: +"expect(jest.fn()).lastCalledWith(...expected) - Expected + Received @@ -88,16 +76,18 @@ Difference: - 4, + 1, + 2, - }" + }, + +Number of calls: 1" `; exports[`lastCalledWith works with arguments that don't match 1`] = ` -"expect(jest.fn()).lastCalledWith(expected) +"expect(jest.fn()).lastCalledWith(...expected) -Expected mock function to have been last called with: - \\"bar\\" -as argument 2, but it was called with - \\"bar1\\"." +Expected: \\"foo\\", \\"bar\\" +Received: \\"foo\\", \\"bar1\\" + +Number of calls: 1" `; exports[`lastCalledWith works with arguments that match 1`] = ` @@ -120,19 +110,23 @@ Number of calls: 3" `; exports[`lastCalledWith works with many arguments that don't match 1`] = ` -"expect(jest.fn()).lastCalledWith(expected) +"expect(jest.fn()).lastCalledWith(...expected) -Expected mock function to have been last called with: - \\"bar\\" -as argument 2, but it was called with - \\"bar3\\"." +Expected: \\"foo\\", \\"bar\\" +Received + 2: \\"foo\\", \\"bar2\\" +-> 3: \\"foo\\", \\"bar3\\" + +Number of calls: 3" `; exports[`lastCalledWith works with trailing undefined arguments 1`] = ` -"expect(jest.fn()).lastCalledWith(expected) +"expect(jest.fn()).lastCalledWith(...expected) -Expected mock function to have been last called with: - Did not expect argument 2 but it was called with undefined." +Expected: \\"foo\\" +Received: \\"foo\\", undefined + +Number of calls: 1" `; exports[`lastReturnedWith a call that throws is not considered to have returned 1`] = ` @@ -324,11 +318,12 @@ Received has value: [Function fn]" `; exports[`nthCalledWith works when not called 1`] = ` -"expect(jest.fn()).nthCalledWith(expected) +"expect(jest.fn()).nthCalledWith(n, ...expected) -Expected mock function first call to have been called with: - [\\"foo\\", \\"bar\\"] -But it was not called." +n: 1 +Expected: \\"foo\\", \\"bar\\" + +Number of calls: 0" `; exports[`nthCalledWith works with Immutable.js objects 1`] = ` @@ -350,15 +345,9 @@ Number of calls: 1" `; exports[`nthCalledWith works with Map 2`] = ` -"expect(jest.fn()).nthCalledWith(expected) - -Expected mock function first call to have been called with: - Map {\\"a\\" => \\"b\\", \\"b\\" => \\"a\\"} -as argument 1, but it was called with - Map {1 => 2, 2 => 1}. - -Difference: +"expect(jest.fn()).nthCalledWith(n, ...expected) +n: 1 - Expected + Received @@ -367,7 +356,9 @@ Difference: - \\"b\\" => \\"a\\", + 1 => 2, + 2 => 1, - }" + }, + +Number of calls: 1" `; exports[`nthCalledWith works with Set 1`] = ` @@ -380,15 +371,9 @@ Number of calls: 1" `; exports[`nthCalledWith works with Set 2`] = ` -"expect(jest.fn()).nthCalledWith(expected) - -Expected mock function first call to have been called with: - Set {3, 4} -as argument 1, but it was called with - Set {1, 2}. - -Difference: +"expect(jest.fn()).nthCalledWith(n, ...expected) +n: 1 - Expected + Received @@ -397,16 +382,19 @@ Difference: - 4, + 1, + 2, - }" + }, + +Number of calls: 1" `; exports[`nthCalledWith works with arguments that don't match 1`] = ` -"expect(jest.fn()).nthCalledWith(expected) +"expect(jest.fn()).nthCalledWith(n, ...expected) + +n: 1 +Expected: \\"foo\\", \\"bar\\" +Received: \\"foo\\", \\"bar1\\" -Expected mock function first call to have been called with: - \\"bar\\" -as argument 2, but it was called with - \\"bar1\\"." +Number of calls: 1" `; exports[`nthCalledWith works with arguments that match 1`] = ` @@ -431,10 +419,13 @@ Number of calls: 3" `; exports[`nthCalledWith works with trailing undefined arguments 1`] = ` -"expect(jest.fn()).nthCalledWith(expected) +"expect(jest.fn()).nthCalledWith(n, ...expected) -Expected mock function first call to have been called with: - Did not expect argument 2 but it was called with undefined." +n: 1 +Expected: \\"foo\\" +Received: \\"foo\\", undefined + +Number of calls: 1" `; exports[`nthReturnedWith a call that throws is not considered to have returned 1`] = ` @@ -911,11 +902,11 @@ Received has value: [Function fn]" `; exports[`toBeCalledWith works when not called 1`] = ` -"expect(jest.fn()).toBeCalledWith(expected) +"expect(jest.fn()).toBeCalledWith(...expected) + +Expected: \\"foo\\", \\"bar\\" -Expected mock function to have been called with: - [\\"foo\\", \\"bar\\"] -But it was not called." +Number of calls: 0" `; exports[`toBeCalledWith works with Immutable.js objects 1`] = ` @@ -935,14 +926,7 @@ Number of calls: 1" `; exports[`toBeCalledWith works with Map 2`] = ` -"expect(jest.fn()).toBeCalledWith(expected) - -Expected mock function to have been called with: - Map {\\"a\\" => \\"b\\", \\"b\\" => \\"a\\"} -as argument 1, but it was called with - Map {1 => 2, 2 => 1}. - -Difference: +"expect(jest.fn()).toBeCalledWith(...expected) - Expected + Received @@ -952,7 +936,9 @@ Difference: - \\"b\\" => \\"a\\", + 1 => 2, + 2 => 1, - }" + }, + +Number of calls: 1" `; exports[`toBeCalledWith works with Set 1`] = ` @@ -964,14 +950,7 @@ Number of calls: 1" `; exports[`toBeCalledWith works with Set 2`] = ` -"expect(jest.fn()).toBeCalledWith(expected) - -Expected mock function to have been called with: - Set {3, 4} -as argument 1, but it was called with - Set {1, 2}. - -Difference: +"expect(jest.fn()).toBeCalledWith(...expected) - Expected + Received @@ -981,16 +960,18 @@ Difference: - 4, + 1, + 2, - }" + }, + +Number of calls: 1" `; exports[`toBeCalledWith works with arguments that don't match 1`] = ` -"expect(jest.fn()).toBeCalledWith(expected) +"expect(jest.fn()).toBeCalledWith(...expected) + +Expected: \\"foo\\", \\"bar\\" +Received: \\"foo\\", \\"bar1\\" -Expected mock function to have been called with: - \\"bar\\" -as argument 2, but it was called with - \\"bar1\\"." +Number of calls: 1" `; exports[`toBeCalledWith works with arguments that match 1`] = ` @@ -1012,27 +993,24 @@ Number of calls: 3" `; exports[`toBeCalledWith works with many arguments that don't match 1`] = ` -"expect(jest.fn()).toBeCalledWith(expected) +"expect(jest.fn()).toBeCalledWith(...expected) -Expected mock function to have been called with: - \\"bar\\" -as argument 2, but it was called with - \\"bar3\\". - - \\"bar\\" -as argument 2, but it was called with - \\"bar2\\". +Expected: \\"foo\\", \\"bar\\" +Received + 1: \\"foo\\", \\"bar1\\" + 2: \\"foo\\", \\"bar2\\" + 3: \\"foo\\", \\"bar3\\" - \\"bar\\" -as argument 2, but it was called with - \\"bar1\\"." +Number of calls: 3" `; exports[`toBeCalledWith works with trailing undefined arguments 1`] = ` -"expect(jest.fn()).toBeCalledWith(expected) +"expect(jest.fn()).toBeCalledWith(...expected) -Expected mock function to have been called with: - Did not expect argument 2 but it was called with undefined." +Expected: \\"foo\\" +Received: \\"foo\\", undefined + +Number of calls: 1" `; exports[`toHaveBeenCalled .not fails with any argument passed 1`] = ` @@ -1249,11 +1227,11 @@ Received has value: [Function fn]" `; exports[`toHaveBeenCalledWith works when not called 1`] = ` -"expect(jest.fn()).toHaveBeenCalledWith(expected) +"expect(jest.fn()).toHaveBeenCalledWith(...expected) -Expected mock function to have been called with: - [\\"foo\\", \\"bar\\"] -But it was not called." +Expected: \\"foo\\", \\"bar\\" + +Number of calls: 0" `; exports[`toHaveBeenCalledWith works with Immutable.js objects 1`] = ` @@ -1273,14 +1251,7 @@ Number of calls: 1" `; exports[`toHaveBeenCalledWith works with Map 2`] = ` -"expect(jest.fn()).toHaveBeenCalledWith(expected) - -Expected mock function to have been called with: - Map {\\"a\\" => \\"b\\", \\"b\\" => \\"a\\"} -as argument 1, but it was called with - Map {1 => 2, 2 => 1}. - -Difference: +"expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received @@ -1290,7 +1261,9 @@ Difference: - \\"b\\" => \\"a\\", + 1 => 2, + 2 => 1, - }" + }, + +Number of calls: 1" `; exports[`toHaveBeenCalledWith works with Set 1`] = ` @@ -1302,14 +1275,7 @@ Number of calls: 1" `; exports[`toHaveBeenCalledWith works with Set 2`] = ` -"expect(jest.fn()).toHaveBeenCalledWith(expected) - -Expected mock function to have been called with: - Set {3, 4} -as argument 1, but it was called with - Set {1, 2}. - -Difference: +"expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received @@ -1319,16 +1285,18 @@ Difference: - 4, + 1, + 2, - }" + }, + +Number of calls: 1" `; exports[`toHaveBeenCalledWith works with arguments that don't match 1`] = ` -"expect(jest.fn()).toHaveBeenCalledWith(expected) +"expect(jest.fn()).toHaveBeenCalledWith(...expected) -Expected mock function to have been called with: - \\"bar\\" -as argument 2, but it was called with - \\"bar1\\"." +Expected: \\"foo\\", \\"bar\\" +Received: \\"foo\\", \\"bar1\\" + +Number of calls: 1" `; exports[`toHaveBeenCalledWith works with arguments that match 1`] = ` @@ -1350,27 +1318,24 @@ Number of calls: 3" `; exports[`toHaveBeenCalledWith works with many arguments that don't match 1`] = ` -"expect(jest.fn()).toHaveBeenCalledWith(expected) +"expect(jest.fn()).toHaveBeenCalledWith(...expected) -Expected mock function to have been called with: - \\"bar\\" -as argument 2, but it was called with - \\"bar3\\". - - \\"bar\\" -as argument 2, but it was called with - \\"bar2\\". +Expected: \\"foo\\", \\"bar\\" +Received + 1: \\"foo\\", \\"bar1\\" + 2: \\"foo\\", \\"bar2\\" + 3: \\"foo\\", \\"bar3\\" - \\"bar\\" -as argument 2, but it was called with - \\"bar1\\"." +Number of calls: 3" `; exports[`toHaveBeenCalledWith works with trailing undefined arguments 1`] = ` -"expect(jest.fn()).toHaveBeenCalledWith(expected) +"expect(jest.fn()).toHaveBeenCalledWith(...expected) -Expected mock function to have been called with: - Did not expect argument 2 but it was called with undefined." +Expected: \\"foo\\" +Received: \\"foo\\", undefined + +Number of calls: 1" `; exports[`toHaveBeenLastCalledWith includes the custom mock name in the error message 1`] = ` @@ -1391,11 +1356,11 @@ Received has value: [Function fn]" `; exports[`toHaveBeenLastCalledWith works when not called 1`] = ` -"expect(jest.fn()).toHaveBeenLastCalledWith(expected) +"expect(jest.fn()).toHaveBeenLastCalledWith(...expected) + +Expected: \\"foo\\", \\"bar\\" -Expected mock function to have been last called with: - [\\"foo\\", \\"bar\\"] -But it was not called." +Number of calls: 0" `; exports[`toHaveBeenLastCalledWith works with Immutable.js objects 1`] = ` @@ -1415,14 +1380,7 @@ Number of calls: 1" `; exports[`toHaveBeenLastCalledWith works with Map 2`] = ` -"expect(jest.fn()).toHaveBeenLastCalledWith(expected) - -Expected mock function to have been last called with: - Map {\\"a\\" => \\"b\\", \\"b\\" => \\"a\\"} -as argument 1, but it was called with - Map {1 => 2, 2 => 1}. - -Difference: +"expect(jest.fn()).toHaveBeenLastCalledWith(...expected) - Expected + Received @@ -1432,7 +1390,9 @@ Difference: - \\"b\\" => \\"a\\", + 1 => 2, + 2 => 1, - }" + }, + +Number of calls: 1" `; exports[`toHaveBeenLastCalledWith works with Set 1`] = ` @@ -1444,14 +1404,7 @@ Number of calls: 1" `; exports[`toHaveBeenLastCalledWith works with Set 2`] = ` -"expect(jest.fn()).toHaveBeenLastCalledWith(expected) - -Expected mock function to have been last called with: - Set {3, 4} -as argument 1, but it was called with - Set {1, 2}. - -Difference: +"expect(jest.fn()).toHaveBeenLastCalledWith(...expected) - Expected + Received @@ -1461,16 +1414,18 @@ Difference: - 4, + 1, + 2, - }" + }, + +Number of calls: 1" `; exports[`toHaveBeenLastCalledWith works with arguments that don't match 1`] = ` -"expect(jest.fn()).toHaveBeenLastCalledWith(expected) +"expect(jest.fn()).toHaveBeenLastCalledWith(...expected) + +Expected: \\"foo\\", \\"bar\\" +Received: \\"foo\\", \\"bar1\\" -Expected mock function to have been last called with: - \\"bar\\" -as argument 2, but it was called with - \\"bar1\\"." +Number of calls: 1" `; exports[`toHaveBeenLastCalledWith works with arguments that match 1`] = ` @@ -1493,19 +1448,23 @@ Number of calls: 3" `; exports[`toHaveBeenLastCalledWith works with many arguments that don't match 1`] = ` -"expect(jest.fn()).toHaveBeenLastCalledWith(expected) +"expect(jest.fn()).toHaveBeenLastCalledWith(...expected) + +Expected: \\"foo\\", \\"bar\\" +Received + 2: \\"foo\\", \\"bar2\\" +-> 3: \\"foo\\", \\"bar3\\" -Expected mock function to have been last called with: - \\"bar\\" -as argument 2, but it was called with - \\"bar3\\"." +Number of calls: 3" `; exports[`toHaveBeenLastCalledWith works with trailing undefined arguments 1`] = ` -"expect(jest.fn()).toHaveBeenLastCalledWith(expected) +"expect(jest.fn()).toHaveBeenLastCalledWith(...expected) -Expected mock function to have been last called with: - Did not expect argument 2 but it was called with undefined." +Expected: \\"foo\\" +Received: \\"foo\\", undefined + +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith includes the custom mock name in the error message 1`] = ` @@ -1554,11 +1513,12 @@ Received has value: [Function fn]" `; exports[`toHaveBeenNthCalledWith works when not called 1`] = ` -"expect(jest.fn()).toHaveBeenNthCalledWith(expected) +"expect(jest.fn()).toHaveBeenNthCalledWith(n, ...expected) -Expected mock function first call to have been called with: - [\\"foo\\", \\"bar\\"] -But it was not called." +n: 1 +Expected: \\"foo\\", \\"bar\\" + +Number of calls: 0" `; exports[`toHaveBeenNthCalledWith works with Immutable.js objects 1`] = ` @@ -1580,15 +1540,9 @@ Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with Map 2`] = ` -"expect(jest.fn()).toHaveBeenNthCalledWith(expected) - -Expected mock function first call to have been called with: - Map {\\"a\\" => \\"b\\", \\"b\\" => \\"a\\"} -as argument 1, but it was called with - Map {1 => 2, 2 => 1}. - -Difference: +"expect(jest.fn()).toHaveBeenNthCalledWith(n, ...expected) +n: 1 - Expected + Received @@ -1597,7 +1551,9 @@ Difference: - \\"b\\" => \\"a\\", + 1 => 2, + 2 => 1, - }" + }, + +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with Set 1`] = ` @@ -1610,15 +1566,9 @@ Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with Set 2`] = ` -"expect(jest.fn()).toHaveBeenNthCalledWith(expected) - -Expected mock function first call to have been called with: - Set {3, 4} -as argument 1, but it was called with - Set {1, 2}. - -Difference: +"expect(jest.fn()).toHaveBeenNthCalledWith(n, ...expected) +n: 1 - Expected + Received @@ -1627,16 +1577,19 @@ Difference: - 4, + 1, + 2, - }" + }, + +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with arguments that don't match 1`] = ` -"expect(jest.fn()).toHaveBeenNthCalledWith(expected) +"expect(jest.fn()).toHaveBeenNthCalledWith(n, ...expected) -Expected mock function first call to have been called with: - \\"bar\\" -as argument 2, but it was called with - \\"bar1\\"." +n: 1 +Expected: \\"foo\\", \\"bar\\" +Received: \\"foo\\", \\"bar1\\" + +Number of calls: 1" `; exports[`toHaveBeenNthCalledWith works with arguments that match 1`] = ` @@ -1661,10 +1614,13 @@ Number of calls: 3" `; exports[`toHaveBeenNthCalledWith works with trailing undefined arguments 1`] = ` -"expect(jest.fn()).toHaveBeenNthCalledWith(expected) +"expect(jest.fn()).toHaveBeenNthCalledWith(n, ...expected) + +n: 1 +Expected: \\"foo\\" +Received: \\"foo\\", undefined -Expected mock function first call to have been called with: - Did not expect argument 2 but it was called with undefined." +Number of calls: 1" `; exports[`toHaveLastReturnedWith a call that throws is not considered to have returned 1`] = ` diff --git a/packages/expect/src/spyMatchers.ts b/packages/expect/src/spyMatchers.ts index 7be62b35d270..890dfce1f18b 100644 --- a/packages/expect/src/spyMatchers.ts +++ b/packages/expect/src/spyMatchers.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import getType, {isPrimitive} from 'jest-get-type'; import { diff, ensureExpectedIsNumber, @@ -22,11 +23,12 @@ import { } from 'jest-matcher-utils'; import {MatchersObject, MatcherState, SyncExpectationResult} from './types'; import {equals} from './jasmineUtils'; -import {iterableEquality, partition, isOneline} from './utils'; +import {iterableEquality} from './utils'; + +// The optional property of matcher context is true if undefined. +const isExpand = (expand?: boolean): boolean => expand !== false; const PRINT_LIMIT = 3; -const CALL_PRINT_LIMIT = 3; -const LAST_CALL_PRINT_LIMIT = 1; const NO_ARGUMENTS = 'called with 0 arguments'; @@ -99,8 +101,6 @@ const getRightAlignedPrinter = (label: string): PrintLabel => { 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, @@ -131,6 +131,188 @@ const printReceivedCallsNegative = ( ); }; +const printExpectedReceivedCallsPositive = ( + expected: Array, + indexedCalls: Array, + expand: boolean, + isOnlyCall: boolean, + iExpectedCall?: number, +) => { + const expectedLine = `Expected: ${printExpectedArgs(expected)}\n`; + if (indexedCalls.length === 0) { + return expectedLine; + } + + const label = 'Received: '; + if (isOnlyCall && (iExpectedCall === 0 || iExpectedCall === undefined)) { + const received = indexedCalls[0][1]; + + if (isLineDiffableCall(expected, received)) { + // Display diff without indentation. + const lines = [ + EXPECTED_COLOR('- Expected'), + RECEIVED_COLOR('+ Received'), + '', + ]; + + const length = Math.max(expected.length, received.length); + for (let i = 0; i < length; i += 1) { + if (i < expected.length && i < received.length) { + if (isEqualValue(expected[i], received[i])) { + lines.push(` ${printCommon(received[i])},`); + continue; + } + + if (isLineDiffableArg(expected[i], received[i])) { + const difference = diff(expected[i], received[i], {expand}); + if ( + typeof difference === 'string' && + difference.includes('- Expected') && + difference.includes('+ Received') + ) { + // Omit annotation in case multiple args have diff. + lines.push( + difference + .split('\n') + .slice(3) + .join('\n') + ',', + ); + continue; + } + } + } + + if (i < expected.length) { + lines.push(EXPECTED_COLOR('- ' + stringify(expected[i])) + ','); + } + if (i < received.length) { + lines.push(RECEIVED_COLOR('+ ' + stringify(received[i])) + ','); + } + } + + return lines.join('\n') + '\n'; + } + + return expectedLine + label + printReceivedArgs(received, expected) + '\n'; + } + + const printAligned = getRightAlignedPrinter(label); + + return ( + expectedLine + + 'Received\n' + + indexedCalls.reduce((printed: string, [i, received]: IndexedCall) => { + const aligned = printAligned(String(i + 1), i === iExpectedCall); + return ( + printed + + ((i === iExpectedCall || iExpectedCall === undefined) && + isLineDiffableCall(expected, received) + ? aligned.replace(': ', '\n') + + printDiffCall(expected, received, expand) + : aligned + printReceivedArgs(received, expected)) + + '\n' + ); + }, '') + ); +}; + +const indentation = 'Received'.replace(/\w/g, ' '); + +const printDiffCall = ( + expected: Array, + received: Array, + expand: boolean, +) => + received + .map((arg, i) => { + if (i < expected.length) { + if (isEqualValue(expected[i], arg)) { + return indentation + ' ' + printCommon(arg) + ','; + } + + if (isLineDiffableArg(expected[i], arg)) { + const difference = diff(expected[i], arg, {expand}); + + if ( + typeof difference === 'string' && + difference.includes('- Expected') && + difference.includes('+ Received') + ) { + // Display diff with indentation. + // Omit annotation in case multiple args have diff. + return ( + difference + .split('\n') + .slice(3) + .map(line => indentation + line) + .join('\n') + ',' + ); + } + } + } + + // Display + only if received arg has no corresponding expected arg. + return ( + indentation + + (i < expected.length + ? ' ' + printReceived(arg) + : RECEIVED_COLOR('+ ' + stringify(arg))) + + ',' + ); + }) + .join('\n'); + +const isLineDiffableCall = ( + expected: Array, + received: Array, +): boolean => + expected.some( + (arg, i) => i < received.length && isLineDiffableArg(arg, received[i]), + ); + +// Almost redundant with function in jest-matcher-utils, +// except no line diff for any strings. +const isLineDiffableArg = (expected: unknown, received: unknown): boolean => { + const expectedType = getType(expected); + const receivedType = getType(received); + + if (expectedType !== receivedType) { + return false; + } + + if (isPrimitive(expected)) { + return false; + } + + if ( + expectedType === 'date' || + expectedType === 'function' || + expectedType === 'regexp' + ) { + return false; + } + + if (expected instanceof Error && received instanceof Error) { + return false; + } + + if ( + expectedType === 'object' && + typeof (expected as any).asymmetricMatch === 'function' + ) { + return false; + } + + if ( + receivedType === 'object' && + typeof (received as any).asymmetricMatch === 'function' + ) { + return false; + } + + return true; +}; + const printResult = (result: any) => result.type === 'throw' ? 'function call threw an error' @@ -152,7 +334,7 @@ const printReceivedResults = ( return ''; } - if (isOnlyCall) { + if (isOnlyCall && (iExpectedCall === 0 || iExpectedCall === undefined)) { return label + printResult(indexedResults[0][1]) + '\n'; } @@ -375,21 +557,13 @@ const createToBeCalledWithMatcher = (matcherName: string) => ensureMockOrSpy(received, matcherName, expectedArgument, options); const receivedIsSpy = isSpy(received); - const type = receivedIsSpy ? 'spy' : 'mock function'; const receivedName = receivedIsSpy ? 'spy' : received.getMockName(); - const identifier = - receivedIsSpy || receivedName === 'jest.fn()' - ? type - : `${type} "${receivedName}"`; const calls = receivedIsSpy ? received.calls.all().map((x: any) => x.args) : received.mock.calls; - const [match, fail] = partition(calls, call => - isEqualCall(expected, call as Array), - ); - const pass = match.length > 0; + const pass = calls.some((call: any) => isEqualCall(expected, call)); const message = pass ? () => { @@ -417,11 +591,27 @@ const createToBeCalledWithMatcher = (matcherName: string) => `\nNumber of calls: ${printReceived(calls.length)}` ); } - : () => - matcherHint('.' + matcherName, receivedName) + - '\n\n' + - `Expected ${identifier} to have been called with:\n` + - formatMismatchedCalls(fail, expected, CALL_PRINT_LIMIT); + : () => { + // Some examples of calls that are not equal to expected value. + const indexedCalls: Array = []; + let i = 0; + while (i < calls.length && indexedCalls.length < PRINT_LIMIT) { + indexedCalls.push([i, calls[i]]); + i += 1; + } + + return ( + matcherHint(matcherName, receivedName, expectedArgument, options) + + '\n\n' + + printExpectedReceivedCallsPositive( + expected, + indexedCalls, + isExpand(this.expand), + calls.length === 1, + ) + + `\nNumber of calls: ${printReceived(calls.length)}` + ); + }; return {message, pass}; }; @@ -511,12 +701,7 @@ const createLastCalledWithMatcher = (matcherName: string) => ensureMockOrSpy(received, matcherName, expectedArgument, options); const receivedIsSpy = isSpy(received); - const type = receivedIsSpy ? 'spy' : 'mock function'; const receivedName = receivedIsSpy ? 'spy' : received.getMockName(); - const identifier = - receivedIsSpy || receivedName === 'jest.fn()' - ? type - : `${type} "${receivedName}"`; const calls = receivedIsSpy ? received.calls.all().map((x: any) => x.args) @@ -549,11 +734,38 @@ const createLastCalledWithMatcher = (matcherName: string) => `\nNumber of calls: ${printReceived(calls.length)}` ); } - : () => - matcherHint('.' + matcherName, receivedName) + - '\n\n' + - `Expected ${identifier} to have been last called with:\n` + - formatMismatchedCalls(calls, expected, LAST_CALL_PRINT_LIMIT); + : () => { + const indexedCalls: Array = []; + if (iLast >= 0) { + if (iLast > 0) { + let i = iLast - 1; + // Is there a preceding call that is equal to expected args? + while (i >= 0 && !isEqualCall(expected, calls[i])) { + i -= 1; + } + if (i < 0) { + i = iLast - 1; // otherwise, preceding call + } + + indexedCalls.push([i, calls[i]]); + } + + indexedCalls.push([iLast, calls[iLast]]); + } + + return ( + matcherHint(matcherName, receivedName, expectedArgument, options) + + '\n\n' + + printExpectedReceivedCallsPositive( + expected, + indexedCalls, + isExpand(this.expand), + calls.length === 1, + iLast, + ) + + `\nNumber of calls: ${printReceived(calls.length)}` + ); + }; return {message, pass}; }; @@ -666,13 +878,7 @@ const createNthCalledWithMatcher = (matcherName: string) => } const receivedIsSpy = isSpy(received); - const type = receivedIsSpy ? 'spy' : 'mock function'; - const receivedName = receivedIsSpy ? 'spy' : received.getMockName(); - const identifier = - receivedIsSpy || receivedName === 'jest.fn()' - ? type - : `${type} "${receivedName}"`; const calls = receivedIsSpy ? received.calls.all().map((x: any) => x.args) @@ -711,17 +917,66 @@ const createNthCalledWithMatcher = (matcherName: string) => `\nNumber of calls: ${printReceived(calls.length)}` ); } - : () => - matcherHint('.' + matcherName, receivedName) + - '\n\n' + - `Expected ${identifier} ${nthToString( - nth, - )} call to have been called with:\n` + - formatMismatchedCalls( - calls[nth - 1] ? [calls[nth - 1]] : [], - expected, - LAST_CALL_PRINT_LIMIT, + : () => { + // Display preceding and following calls: + // * nearest call that is equal to expected args + // * otherwise, adjacent call + // in case assertions fails because of index, especially off by one. + const indexedCalls: Array = []; + if (iNth < length) { + if (iNth - 1 >= 0) { + let i = iNth - 1; + // Is there a preceding call that is equal to expected args? + while (i >= 0 && !isEqualCall(expected, calls[i])) { + i -= 1; + } + if (i < 0) { + i = iNth - 1; // otherwise, adjacent call + } + + indexedCalls.push([i, calls[i]]); + } + indexedCalls.push([iNth, calls[iNth]]); + if (iNth + 1 < length) { + let i = iNth + 1; + // Is there a following call that is equal to expected args? + while (i < length && !isEqualCall(expected, calls[i])) { + i += 1; + } + if (i >= length) { + i = iNth + 1; // otherwise, adjacent call + } + + indexedCalls.push([i, calls[i]]); + } + } else if (length > 0) { + // The number of received calls is fewer than the expected number. + let i = length - 1; + // Is there a call that is equal to expected args? + while (i >= 0 && !isEqualCall(expected, calls[i])) { + i -= 1; + } + if (i < 0) { + i = length - 1; // otherwise, last call + } + + indexedCalls.push([i, calls[i]]); + } + + return ( + matcherHint(matcherName, receivedName, expectedArgument, options) + + '\n\n' + + `n: ${nth}\n` + + printExpectedReceivedCallsPositive( + expected, + indexedCalls, + isExpand(this.expand), + calls.length === 1, + iNth, + ) + + `\nNumber of calls: ${printReceived(calls.length)}` ); + }; return {message, pass}; }; @@ -923,77 +1178,4 @@ const ensureMock = ( } }; -const getPrintedCalls = ( - calls: Array, - limit: number, - sep: string, - fn: Function, -): string => { - const result = []; - let i = calls.length; - - while (--i >= 0 && --limit >= 0) { - result.push(fn(calls[i])); - } - - return result.join(sep); -}; - -const formatMismatchedCalls = ( - calls: Array, - expected: any, - limit: number, -): string => { - if (calls.length) { - return getPrintedCalls( - calls, - limit, - '\n\n', - formatMismatchedArgs.bind(null, expected), - ); - } else { - return ( - ` ${printExpected(expected)}\n` + - `But it was ${RECEIVED_COLOR('not called')}.` - ); - } -}; - -const formatMismatchedArgs = (expected: any, received: any): string => { - const length = Math.max(expected.length, received.length); - - const printedArgs = []; - for (let i = 0; i < length; i++) { - if (!equals(expected[i], received[i], [iterableEquality])) { - const oneline = isOneline(expected[i], received[i]); - const diffString = diff(expected[i], received[i]); - printedArgs.push( - ` ${printExpected(expected[i])}\n` + - `as argument ${i + 1}, but it was called with\n` + - ` ${printReceived(received[i])}.` + - (diffString && !oneline ? `\n\nDifference:\n\n${diffString}` : ''), - ); - } else if (i >= expected.length) { - printedArgs.push( - ` Did not expect argument ${i + 1} ` + - `but it was called with ${printReceived(received[i])}.`, - ); - } - } - - return printedArgs.join('\n'); -}; - -const nthToString = (nth: number): string => { - switch (nth) { - case 1: - return 'first'; - case 2: - return 'second'; - case 3: - return 'third'; - } - return `${nth}th`; -}; - export default spyMatchers;