Skip to content

Commit

Permalink
Use iterableEquality in spy matchers
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
philipp-spiess committed May 25, 2017
1 parent 9b157c3 commit 8b79691
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ Received:
function: <red>[Function fn]</>"
`;

exports[`lastCalledWith works with jest.fn and Immutable.js objects 1`] = `
"<dim>expect(<red>jest.fn()</><dim>).not.lastCalledWith(<green>expected</><dim>)

Expected mock function to not have been last called with:
<green>[Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]</>"
`;

exports[`lastCalledWith works with jest.fn and arguments that don't match 1`] = `
"<dim>expect(<red>jest.fn()</><dim>).lastCalledWith(<green>expected</><dim>)

Expand Down Expand Up @@ -107,6 +114,13 @@ Received:
function: <red>[Function fn]</>"
`;

exports[`toBeCalledWith works with jest.fn and Immutable.js objects 1`] = `
"<dim>expect(<red>jest.fn()</><dim>).not.toBeCalledWith(<green>expected</><dim>)

Expected mock function not to have been called with:
<green>[Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]</>"
`;

exports[`toBeCalledWith works with jest.fn and arguments that don't match 1`] = `
"<dim>expect(<red>jest.fn()</><dim>).toBeCalledWith(<green>expected</><dim>)

Expand Down Expand Up @@ -279,6 +293,13 @@ Received:
function: <red>[Function fn]</>"
`;

exports[`toHaveBeenCalledWith works with jasmine.createSpy and Immutable.js objects 1`] = `
"<dim>expect(<red>spy</><dim>).not.toHaveBeenCalledWith(<green>expected</><dim>)

Expected spy not to have been called with:
<green>[Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]</>"
`;

exports[`toHaveBeenCalledWith works with jasmine.createSpy and arguments that don't match 1`] = `
"<dim>expect(<red>spy</><dim>).toHaveBeenCalledWith(<green>expected</><dim>)

Expand Down Expand Up @@ -319,6 +340,13 @@ Expected spy to have been called with:
But it was <red>not called</>."
`;

exports[`toHaveBeenCalledWith works with jest.fn and Immutable.js objects 1`] = `
"<dim>expect(<red>jest.fn()</><dim>).not.toHaveBeenCalledWith(<green>expected</><dim>)

Expected mock function not to have been called with:
<green>[Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]</>"
`;

exports[`toHaveBeenCalledWith works with jest.fn and arguments that don't match 1`] = `
"<dim>expect(<red>jest.fn()</><dim>).toHaveBeenCalledWith(<green>expected</><dim>)

Expand Down Expand Up @@ -367,6 +395,13 @@ Received:
function: <red>[Function fn]</>"
`;

exports[`toHaveBeenLastCalledWith works with jasmine.createSpy and Immutable.js objects 1`] = `
"<dim>expect(<red>spy</><dim>).not.toHaveBeenLastCalledWith(<green>expected</><dim>)

Expected spy to not have been last called with:
<green>[Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]</>"
`;

exports[`toHaveBeenLastCalledWith works with jasmine.createSpy and arguments that don't match 1`] = `
"<dim>expect(<red>spy</><dim>).toHaveBeenLastCalledWith(<green>expected</><dim>)

Expand Down Expand Up @@ -408,6 +443,13 @@ Expected spy to have been last called with:
But it was <red>not called</>."
`;

exports[`toHaveBeenLastCalledWith works with jest.fn and Immutable.js objects 1`] = `
"<dim>expect(<red>jest.fn()</><dim>).not.toHaveBeenLastCalledWith(<green>expected</><dim>)

Expected mock function to not have been last called with:
<green>[Immutable.Map {a: {\\"b\\": \\"c\\"}}, Immutable.Map {a: {\\"b\\": \\"c\\"}}]</>"
`;

exports[`toHaveBeenLastCalledWith works with jest.fn and arguments that don't match 1`] = `
"<dim>expect(<red>jest.fn()</><dim>).toHaveBeenLastCalledWith(<green>expected</><dim>)

Expand Down
14 changes: 14 additions & 0 deletions packages/jest-matchers/src/__tests__/spyMatchers-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @emails oncall+jsinfra
*/

const Immutable = require('immutable');
const jestExpect = require('../');

[
Expand Down Expand Up @@ -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();
});
});
37 changes: 6 additions & 31 deletions packages/jest-matchers/src/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -33,36 +38,6 @@ type ContainIterable =
| DOMTokenList
| HTMLCollection<any>;

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' &&
Expand Down
5 changes: 3 additions & 2 deletions packages/jest-matchers/src/spyMatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()',
Expand Down Expand Up @@ -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
? () =>
Expand Down Expand Up @@ -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
? () =>
Expand Down
34 changes: 34 additions & 0 deletions packages/jest-matchers/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* @flow
*/

const {equals} = require('./jasmine-utils');

type GetPath = {
hasEndProp?: boolean,
lastTraversedObject: ?Object,
Expand Down Expand Up @@ -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,
};

0 comments on commit 8b79691

Please sign in to comment.