From 82bd3c33a924aa015c7aeaf4595008e7e0f5d7a1 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 25 May 2017 03:14:54 +0200 Subject: [PATCH] Use iterableEquality in spy matchers While working on custom matchers to solve #3574, I found out that the cause for not seeing this issue in `expect().toEqual()` comes from the fact that this matcher passes the `iterableEquality` to the `equals()` function. When I added this to the equal calls for our spy matchers as well, Immutable.js types were properly suppored. I'm considering this is a bug since the `toBeCalledWith()` matchers should behave the same as the `equals()` matcher. --- .../__snapshots__/spyMatchers-test.js.snap | 42 +++++++++++++++++++ .../src/__tests__/spyMatchers-test.js | 14 +++++++ packages/jest-matchers/src/matchers.js | 37 +++------------- packages/jest-matchers/src/spyMatchers.js | 5 ++- packages/jest-matchers/src/utils.js | 34 +++++++++++++++ 5 files changed, 99 insertions(+), 33 deletions(-) diff --git a/packages/jest-matchers/src/__tests__/__snapshots__/spyMatchers-test.js.snap b/packages/jest-matchers/src/__tests__/__snapshots__/spyMatchers-test.js.snap index 4a246ee70a63..5dd794604ce4 100644 --- a/packages/jest-matchers/src/__tests__/__snapshots__/spyMatchers-test.js.snap +++ b/packages/jest-matchers/src/__tests__/__snapshots__/spyMatchers-test.js.snap @@ -8,6 +8,13 @@ Received: function: [Function fn]" `; +exports[`lastCalledWith works with jest.fn and Immutable.js objects 1`] = ` +"expect(jest.fn()).not.lastCalledWith(expected) + +Expected mock function to not have been last called with: + [Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]" +`; + exports[`lastCalledWith works with jest.fn and arguments that don't match 1`] = ` "expect(jest.fn()).lastCalledWith(expected) @@ -107,6 +114,13 @@ Received: function: [Function fn]" `; +exports[`toBeCalledWith works with jest.fn and Immutable.js objects 1`] = ` +"expect(jest.fn()).not.toBeCalledWith(expected) + +Expected mock function not to have been called with: + [Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]" +`; + exports[`toBeCalledWith works with jest.fn and arguments that don't match 1`] = ` "expect(jest.fn()).toBeCalledWith(expected) @@ -279,6 +293,13 @@ Received: function: [Function fn]" `; +exports[`toHaveBeenCalledWith works with jasmine.createSpy and Immutable.js objects 1`] = ` +"expect(spy).not.toHaveBeenCalledWith(expected) + +Expected spy not to have been called with: + [Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]" +`; + exports[`toHaveBeenCalledWith works with jasmine.createSpy and arguments that don't match 1`] = ` "expect(spy).toHaveBeenCalledWith(expected) @@ -319,6 +340,13 @@ Expected spy to have been called with: But it was not called." `; +exports[`toHaveBeenCalledWith works with jest.fn and Immutable.js objects 1`] = ` +"expect(jest.fn()).not.toHaveBeenCalledWith(expected) + +Expected mock function not to have been called with: + [Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]" +`; + exports[`toHaveBeenCalledWith works with jest.fn and arguments that don't match 1`] = ` "expect(jest.fn()).toHaveBeenCalledWith(expected) @@ -367,6 +395,13 @@ Received: function: [Function fn]" `; +exports[`toHaveBeenLastCalledWith works with jasmine.createSpy and Immutable.js objects 1`] = ` +"expect(spy).not.toHaveBeenLastCalledWith(expected) + +Expected spy to not have been last called with: + [Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]" +`; + exports[`toHaveBeenLastCalledWith works with jasmine.createSpy and arguments that don't match 1`] = ` "expect(spy).toHaveBeenLastCalledWith(expected) @@ -408,6 +443,13 @@ Expected spy to have been last called with: But it was not called." `; +exports[`toHaveBeenLastCalledWith works with jest.fn and Immutable.js objects 1`] = ` +"expect(jest.fn()).not.toHaveBeenLastCalledWith(expected) + +Expected mock function to not have been last called with: + [Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]" +`; + exports[`toHaveBeenLastCalledWith works with jest.fn and arguments that don't match 1`] = ` "expect(jest.fn()).toHaveBeenLastCalledWith(expected) diff --git a/packages/jest-matchers/src/__tests__/spyMatchers-test.js b/packages/jest-matchers/src/__tests__/spyMatchers-test.js index 1f39ae5abd48..c4e4ff6dabb4 100644 --- a/packages/jest-matchers/src/__tests__/spyMatchers-test.js +++ b/packages/jest-matchers/src/__tests__/spyMatchers-test.js @@ -8,6 +8,7 @@ * @emails oncall+jsinfra */ +const Immutable = require('Immutable'); const jestExpect = require('../'); [ @@ -187,4 +188,17 @@ describe('toHaveBeenCalledTimes', () => { jestExpect(fn).not[calledWith]('foo', 'bar'), ).toThrowErrorMatchingSnapshot(); }); + + test(`${calledWith} works with ${mockName} and Immutable.js objects`, () => { + const fn = getFunction(); + const directlyCreated = new Immutable.Map([['a', {b: 'c'}]]); + const indirectlyCreated = new Immutable.Map().set('a', {b: 'c'}); + fn(directlyCreated, indirectlyCreated); + + jestExpect(fn)[calledWith](indirectlyCreated, directlyCreated); + + expect(() => + jestExpect(fn).not[calledWith](indirectlyCreated, directlyCreated), + ).toThrowErrorMatchingSnapshot(); + }); }); diff --git a/packages/jest-matchers/src/matchers.js b/packages/jest-matchers/src/matchers.js index a6b43bf3f40a..6d0c1b1eb975 100644 --- a/packages/jest-matchers/src/matchers.js +++ b/packages/jest-matchers/src/matchers.js @@ -23,7 +23,12 @@ const { printExpected, printWithType, } = require('jest-matcher-utils'); -const {getObjectSubset, getPath, hasOwnProperty} = require('./utils'); +const { + getObjectSubset, + getPath, + hasOwnProperty, + iterableEquality, +} = require('./utils'); const {equals} = require('./jasmine-utils'); type ContainIterable = @@ -33,36 +38,6 @@ type ContainIterable = | DOMTokenList | HTMLCollection; -const IteratorSymbol = Symbol.iterator; - -const hasIterator = object => !!(object != null && object[IteratorSymbol]); -const iterableEquality = (a, b) => { - if ( - typeof a !== 'object' || - typeof b !== 'object' || - Array.isArray(a) || - Array.isArray(b) || - !hasIterator(a) || - !hasIterator(b) - ) { - return undefined; - } - if (a.constructor !== b.constructor) { - return false; - } - const bIterator = b[IteratorSymbol](); - - for (const aValue of a) { - const nextB = bIterator.next(); - if (nextB.done || !equals(aValue, nextB.value, [iterableEquality])) { - return false; - } - } - if (!bIterator.next().done) { - return false; - } - return true; -}; const isObjectWithKeys = a => a !== null && typeof a === 'object' && diff --git a/packages/jest-matchers/src/spyMatchers.js b/packages/jest-matchers/src/spyMatchers.js index 0b38721f703b..076af084b040 100644 --- a/packages/jest-matchers/src/spyMatchers.js +++ b/packages/jest-matchers/src/spyMatchers.js @@ -24,6 +24,7 @@ const { RECEIVED_COLOR, } = require('jest-matcher-utils'); const {equals} = require('./jasmine-utils'); +const {iterableEquality} = require('./utils'); const RECEIVED_NAME = { 'mock function': 'jest.fn()', @@ -68,7 +69,7 @@ const createToBeCalledWithMatcher = matcherName => ( const calls = receivedIsSpy ? received.calls.all().map(x => x.args) : received.mock.calls; - const pass = calls.some(call => equals(call, expected)); + const pass = calls.some(call => equals(call, expected, [iterableEquality])); const message = pass ? () => @@ -97,7 +98,7 @@ const createLastCalledWithMatcher = matcherName => ( const calls = receivedIsSpy ? received.calls.all().map(x => x.args) : received.mock.calls; - const pass = equals(calls[calls.length - 1], expected); + const pass = equals(calls[calls.length - 1], expected, [iterableEquality]); const message = pass ? () => diff --git a/packages/jest-matchers/src/utils.js b/packages/jest-matchers/src/utils.js index 3becb17b52a5..12e62771d966 100644 --- a/packages/jest-matchers/src/utils.js +++ b/packages/jest-matchers/src/utils.js @@ -8,6 +8,8 @@ * @flow */ +const {equals} = require('./jasmine-utils'); + type GetPath = { hasEndProp?: boolean, lastTraversedObject: ?Object, @@ -91,8 +93,40 @@ const getObjectSubset = (object: Object, subset: Object) => { return object; }; +const IteratorSymbol = Symbol.iterator; + +const hasIterator = object => !!(object != null && object[IteratorSymbol]); +const iterableEquality = (a: any, b: any) => { + if ( + typeof a !== 'object' || + typeof b !== 'object' || + Array.isArray(a) || + Array.isArray(b) || + !hasIterator(a) || + !hasIterator(b) + ) { + return undefined; + } + if (a.constructor !== b.constructor) { + return false; + } + const bIterator = b[IteratorSymbol](); + + for (const aValue of a) { + const nextB = bIterator.next(); + if (nextB.done || !equals(aValue, nextB.value, [iterableEquality])) { + return false; + } + } + if (!bIterator.next().done) { + return false; + } + return true; +}; + module.exports = { getObjectSubset, getPath, hasOwnProperty, + iterableEquality, };