Skip to content

Commit

Permalink
Integrate and simplify toMatchObject; validate "special cases" (#2798)
Browse files Browse the repository at this point in the history
- Hoist methods from `toMatchObject` outside the matchers object to
better resemble other matchers
  - Simplify “subset matching” while leveraging the expected equality
and iterable equality tests by creating a customMatcher for `eq`
  - Add test cases to validate jasmine’s special handling of equality
like `0 == -0`
  - Add test cases to validate `iterableEquality` when used as a custom
matcher for `toEqual` and `toMatchObject`
  - Cleanup style near changed code.
  • Loading branch information
nfarina authored and cpojer committed Feb 8, 2017
1 parent ccd80c2 commit 9bdb555
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1901,6 +1901,19 @@ Received:
{\\"a\\": 99}"
`;

exports[`.toEqual() expect(0).toEqual(-0) 1`] = `
"expect(received).toEqual(expected)

Expected value to equal:
-0
Received:
0

Difference:

Compared values have no visual difference."
`;

exports[`.toEqual() expect(1).not.toEqual(1) 1`] = `
"expect(received).not.toEqual(expected)

Expand All @@ -1919,6 +1932,28 @@ Received:
1"
`;

exports[`.toEqual() expect(Set {1, 2}).not.toEqual(Set {1, 2}) 1`] = `
"expect(received).not.toEqual(expected)

Expected value to not equal:
Set {1, 2}
Received:
Set {1, 2}"
`;

exports[`.toEqual() expect(Set {1, 2}).toEqual(Set {2, 1}) 1`] = `
"expect(received).toEqual(expected)

Expected value to equal:
Set {2, 1}
Received:
Set {1, 2}

Difference:

Compared values have no visual difference."
`;

exports[`.toEqual() expect(null).toEqual(undefined) 1`] = `
"expect(received).toEqual(expected)

Expand Down Expand Up @@ -2549,6 +2584,23 @@ Received:
\\"bar\\""
`;

exports[`toMatchObject() {pass: false} expect([0]).toMatchObject([-0]) 1`] = `
"expect(received).toMatchObject(expected)

Expected value to match object:
[-0]
Received:
[0]
Difference:
- Expected
+ Received

 Array [
- -0,
+ 0,
 ]"
`;

exports[`toMatchObject() {pass: false} expect([1, 2, 3]).toMatchObject([1, 2, 2]) 1`] = `
"expect(received).toMatchObject(expected)

Expand Down Expand Up @@ -2951,6 +3003,17 @@ Difference:
+2015-11-30T00:00:00.000Z"
`;

exports[`toMatchObject() {pass: false} expect(Set {1, 2}).toMatchObject(Set {2, 1}) 1`] = `
"expect(received).toMatchObject(expected)

Expected value to match object:
Set {2, 1}
Received:
Set {1, 2}
Difference:
Compared values have no visual difference."
`;

exports[`toMatchObject() {pass: true} expect([1, 2]).toMatchObject([1, 2]) 1`] = `
"expect(received).not.toMatchObject(expected)

Expand Down Expand Up @@ -3095,6 +3158,15 @@ Received:
Array []"
`;

exports[`toMatchObject() {pass: true} expect(Set {1, 2}).toMatchObject(Set {1, 2}) 1`] = `
"expect(received).not.toMatchObject(expected)

Expected value not to match object:
Set {1, 2}
Received:
Set {1, 2}"
`;

exports[`toMatchObject() throws expect("44").toMatchObject({}) 1`] = `
"expect(object)[.not].toMatchObject(expected)

Expand Down
6 changes: 6 additions & 0 deletions packages/jest-matchers/src/__tests__/matchers-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,11 @@ describe('.toEqual()', () => {
[
[true, false],
[1, 2],
[0, -0],
[{a: 5}, {b: 6}],
['banana', 'apple'],
[null, undefined],
[new Set([1, 2]), new Set([2, 1])],
[{a: 1, b: 2}, jestExpect.objectContaining({a: 2})],
[[1, 3], jestExpect.arrayContaining([1, 2])],
['abd', jestExpect.stringContaining('bc')],
Expand All @@ -96,6 +98,7 @@ describe('.toEqual()', () => {
[1, 1],
['abc', 'abc'],
[{a: 99}, {a: 99}],
[new Set([1, 2]), new Set([1, 2])],
[{a: 1, b: 2}, jestExpect.objectContaining({a: 1})],
[[1, 2, 3], jestExpect.arrayContaining([2, 3])],
['abcd', jestExpect.stringContaining('bc')],
Expand Down Expand Up @@ -681,6 +684,7 @@ describe('toMatchObject()', () => {
[{a: [3, 4, 5, 'v'], b: 'b'}, {a: [3, 4, 5, 'v']}],
[{a: 1, c: 2}, {a: jestExpect.any(Number)}],
[{a: {x: 'x', y: 'y'}}, {a: {x: jestExpect.any(String)}}],
[new Set([1, 2]), new Set([1, 2])],
[new Date('2015-11-30'), new Date('2015-11-30')],
[{a: new Date('2015-11-30'), b: 'b'}, {a: new Date('2015-11-30')}],
[{a: null, b: 'b'}, {a: null}],
Expand Down Expand Up @@ -709,6 +713,8 @@ describe('toMatchObject()', () => {
[{a: [3, 4, 5], b: 'b'}, {a: {b: 4}}],
[{a: [3, 4, 5], b: 'b'}, {a: {b: jestExpect.any(String)}}],
[[1, 2], [1, 3]],
[[0], [-0]],
[new Set([1, 2]), new Set([2, 1])],
[new Date('2015-11-30'), new Date('2015-10-10')],
[{a: new Date('2015-11-30'), b: 'b'}, {a: new Date('2015-10-10')}],
[{a: null, b: 'b'}, {a: '4'}],
Expand Down
21 changes: 20 additions & 1 deletion packages/jest-matchers/src/__tests__/utils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@

'use strict';

const {getPath} = require('../utils');
const {stringify} = require('jest-matcher-utils');
const {
getObjectSubset,
getPath,
} = require('../utils');

describe('getPath()', () => {
test('property exists', () => {
Expand Down Expand Up @@ -65,3 +69,18 @@ describe('getPath()', () => {
});
});
});

describe('getObjectSubset()', () => {
[
[{a: 'b', c: 'd'}, {a: 'd'}, {a: 'b'}],
[{a: [1, 2], b: 'b'}, {a: [3, 4]}, {a: [1, 2]}],
[[{a: 'b', c: 'd'}], [{a: 'z'}], [{a: 'b'}]],
[[1, 2], [1, 2, 3], [1, 2]],
[{a: [1]}, {a: [1, 2]}, {a: [1]}],
[new Date('2015-11-30'), new Date('2016-12-30'), new Date('2015-11-30')],
].forEach(([object, subset, expected]) => {
test(`expect(getObjectSubset(${stringify(object)}, ${stringify(subset)})).toEqual(${stringify(expected)})`, () => {
expect(getObjectSubset(object, subset)).toEqual(expected);
});
});
});
1 change: 0 additions & 1 deletion packages/jest-matchers/src/jasmine-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,5 +297,4 @@ module.exports = {
hasProperty,
isA,
isUndefined,
asymmetricMatch,
};
139 changes: 31 additions & 108 deletions packages/jest-matchers/src/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import type {

const diff = require('jest-diff');
const {escapeStrForRegex} = require('jest-regex-util');
const {getPath} = require('./utils');
const {
getObjectSubset,
getPath,
} = require('./utils');
const {
EXPECTED_COLOR,
RECEIVED_COLOR,
Expand All @@ -31,8 +34,6 @@ const {
} = require('jest-matcher-utils');
const {
equals,
asymmetricMatch,
isUndefined,
} = require('./jasmine-utils');

type ContainIterable = (
Expand Down Expand Up @@ -80,6 +81,17 @@ const iterableEquality = (a, b) => {
}
return true;
};
const isObjectWithKeys = a => a !== null && typeof a === 'object' &&
!(a instanceof Array) && !(a instanceof Date);
const subsetEquality = (object, subset) => {
if (!isObjectWithKeys(object) || !isObjectWithKeys(subset)) {
return undefined;
}
return Object.keys(subset).every(key =>
object.hasOwnProperty(key) &&
equals(object[key], subset[key], [iterableEquality, subsetEquality])
);
};

const matchers: MatchersObject = {
toBe(received: any, expected: number) {
Expand Down Expand Up @@ -561,114 +573,25 @@ const matchers: MatchersObject = {
);
}

const compare = (expected: any, received: any): boolean => {

const asymmetricResult = asymmetricMatch(expected, received);
if (!isUndefined(asymmetricResult)) {
return asymmetricResult;
}

if (typeof received !== typeof expected) {
return false;
}
if (typeof expected !== 'object' || expected === null) {
return expected === received;
}

if (Array.isArray(expected)) {
if (!Array.isArray(received)) {
return false;
}

if (expected.length !== received.length) {
return false;
}

return expected.every((exp, i) => compare(exp, received[i]));
}

if (expected instanceof Date && received instanceof Date) {
return expected.getTime() === received.getTime();
}

return Object.keys(expected).every(key => {
if (!received.hasOwnProperty(key)) {
return false;
}
const exp = expected[key];
const act = received[key];
if (typeof exp === 'object' && exp !== null && act !== null) {
return compare(exp, act);
}
return act === exp;
});
};

// Strip properties form received object that are not present in the expected
// object. We need it to print the diff without adding a lot of unrelated noise.
const findMatchObject = (expected: Object, received: Object) => {
if (Array.isArray(received)) {
if (!Array.isArray(expected)) {
return received;
}

if (expected.length !== received.length) {
return received;
}

return expected.map((exp, i) => findMatchObject(exp, received[i]));

} else if (received instanceof Date) {
return received;
} else if (typeof received === 'object' && received !== null && typeof expected === 'object' && expected !== null) {

const matchedObject = {};

let match = false;
Object.keys(expected).forEach(key => {
if (received.hasOwnProperty(key)) {
match = true;

const exp = expected[key];
const act = received[key];

if (typeof exp === 'object' && exp !== null) {
matchedObject[key] = findMatchObject(exp, act);
} else {
matchedObject[key] = act;
}
}
});

if (match) {
return matchedObject;
} else {
return received;
}
} else {
return received;
}
};

const pass = compare(expectedObject, receivedObject);
const pass = equals(receivedObject, expectedObject, [iterableEquality, subsetEquality]);

const message = pass
? () => matcherHint('.not.toMatchObject') +
`\n\nExpected value not to match object:\n` +
` ${printExpected(expectedObject)}` +
`\nReceived:\n` +
` ${printReceived(receivedObject)}`
: () => {
const diffString = diff(expectedObject, findMatchObject(expectedObject, receivedObject), {
expand: this.expand,
});
return matcherHint('.toMatchObject') +
`\n\nExpected value to match object:\n` +
` ${printExpected(expectedObject)}` +
`\nReceived:\n` +
` ${printReceived(receivedObject)}` +
(diffString ? `\nDifference:\n${diffString}` : '');
};
`\n\nExpected value not to match object:\n` +
` ${printExpected(expectedObject)}` +
`\nReceived:\n` +
` ${printReceived(receivedObject)}`
: () => {
const diffString = diff(expectedObject, getObjectSubset(receivedObject, expectedObject), {
expand: this.expand,
});
return matcherHint('.toMatchObject') +
`\n\nExpected value to match object:\n` +
` ${printExpected(expectedObject)}` +
`\nReceived:\n` +
` ${printReceived(receivedObject)}` +
(diffString ? `\nDifference:\n${diffString}` : '');
};

return {message, pass};
},
Expand Down
Loading

0 comments on commit 9bdb555

Please sign in to comment.