Skip to content

Commit 292e9d0

Browse files
kentaromiuraFacebook Github Bot 1
authored andcommitted
Add custom equality matcher to support ES 2015 iterables
Summary: Allows jest to check for equalities of Sets/Maps and Array-like by adding a [custom equality tester](http://jasmine.github.io/2.0/custom_equality.html) for iterators. Jasmine run custom equality before the normal equality checker, so I also add a test to ensure it didn't break normal Arrays equality (as they implement `Symbol.iterator`). Closes #923 Reviewed By: cpojer Differential Revision: D3207226 fb-gh-sync-id: 56c074462a7e49b983c80e10cadeeb951db45118 fbshipit-source-id: 56c074462a7e49b983c80e10cadeeb951db45118
1 parent a3d30de commit 292e9d0

File tree

4 files changed

+201
-12
lines changed

4 files changed

+201
-12
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc. All rights reserved.
3+
*
4+
* This source code is licensed under the BSD-style license found in the
5+
* LICENSE file in the root directory of this source tree. An additional grant
6+
* of patent rights can be found in the PATENTS file in the same directory.
7+
*
8+
* @emails oncall+jsinfra
9+
*/
10+
'use strict';
11+
12+
describe('iterators', () => {
13+
14+
it('works for arrays', () => {
15+
const mixedArray = [1, {}, []];
16+
17+
expect(mixedArray).toEqual(mixedArray);
18+
expect([1, 2, 3]).toEqual([1, 2, 3]);
19+
expect([1]).not.toEqual([2]);
20+
expect([1, 2, 3]).not.toEqual([1, 2]);
21+
expect([1, 2, 3]).not.toEqual([1, 2, 3, 4]);
22+
});
23+
24+
it('works for custom iterables', () => {
25+
const iterable = {
26+
0: 'a',
27+
1: 'b',
28+
2: 'c',
29+
length: 3,
30+
[Symbol.iterator]: Array.prototype[Symbol.iterator],
31+
};
32+
const expectedIterable = {
33+
0: 'a',
34+
1: 'b',
35+
2: 'c',
36+
length: 3,
37+
[Symbol.iterator]: Array.prototype[Symbol.iterator],
38+
};
39+
expect(iterable).toEqual(expectedIterable);
40+
expect(iterable).not.toEqual(['a', 'b']);
41+
expect(iterable).not.toEqual(['a', 'b', 'c']);
42+
expect(iterable).not.toEqual(['a', 'b', 'c', 'd']);
43+
});
44+
45+
it('works for Sets', () => {
46+
const numbers = [1, 2, 3, 4];
47+
const setOfNumbers = new Set(numbers);
48+
expect(setOfNumbers).not.toEqual(new Set());
49+
expect(setOfNumbers).not.toBe(numbers);
50+
expect(setOfNumbers).not.toEqual([1, 2]);
51+
expect(setOfNumbers).not.toEqual([1, 2, 3]);
52+
expect(setOfNumbers).toEqual(new Set(numbers));
53+
54+
const nestedSets = new Set([new Set([1, 2])]);
55+
expect(nestedSets).not.toEqual(new Set([new Set([1, 4])]));
56+
expect(nestedSets).toEqual(new Set([new Set([1, 2])]));
57+
});
58+
59+
it('works for Maps', () => {
60+
const keyValuePairs = [['key1', 'value1'], ['key2', 'value2']];
61+
const smallerKeyValuePairs = [['key1', 'value1']];
62+
const biggerKeyValuePairs = [
63+
['key1', 'value1'], ['key2', 'value2'], ['key3', 'value3'],
64+
];
65+
const map = new Map(keyValuePairs);
66+
expect(map).not.toEqual(smallerKeyValuePairs);
67+
expect(map).not.toEqual(new Map(smallerKeyValuePairs));
68+
expect(map).not.toEqual(biggerKeyValuePairs);
69+
expect(map).not.toEqual(new Map(biggerKeyValuePairs));
70+
expect(map).not.toEqual(keyValuePairs);
71+
expect(map).not.toBe(keyValuePairs);
72+
expect(map).toEqual(new Map(keyValuePairs));
73+
});
74+
75+
});

packages/jest-jasmine2/src/index.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,46 @@ function jasmine2(config, environment, moduleLoader, testPath) {
6868
moduleLoader.requireModule(config.setupTestFrameworkScriptFile);
6969
}
7070
});
71+
72+
const hasIterator = object => !!(object != null && object[Symbol.iterator]);
73+
const iterableEquality = (a, b) => {
74+
if (
75+
typeof a !== 'object' ||
76+
typeof b !== 'object' ||
77+
Array.isArray(a) ||
78+
Array.isArray(b) ||
79+
!hasIterator(a) ||
80+
!hasIterator(b)
81+
) {
82+
return undefined;
83+
}
84+
if (a.constructor !== b.constructor) {
85+
return false;
86+
}
87+
const bIterator = b[Symbol.iterator]();
88+
89+
for (const aValue of a) {
90+
const nextB = bIterator.next();
91+
if (
92+
nextB.done ||
93+
!jasmine.matchersUtil.equals(
94+
aValue,
95+
nextB.value,
96+
[iterableEquality]
97+
)
98+
) {
99+
return false;
100+
}
101+
}
102+
if (!bIterator.next().done) {
103+
return false;
104+
}
105+
return true;
106+
};
107+
71108
env.beforeEach(() => {
109+
jasmine.addCustomEqualityTester(iterableEquality);
110+
72111
jasmine.addMatchers({
73112
toBeCalled: () => ({
74113
compare: (actual, expected) => {

packages/jest-util/lib/JasmineFormatter.js

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,47 +101,76 @@ class JasmineFormatter {
101101
return ret;
102102
}
103103

104-
prettyPrint(obj, indent, cycleWeakMap) {
104+
prettyPrint(object, indent, cycleWeakMap) {
105105
if (!indent) {
106106
indent = '';
107107
}
108108

109-
if (typeof obj === 'object' && obj !== null) {
110-
if (this._jasmine.isDomNode(obj)) {
109+
if (typeof object === 'object' && object !== null) {
110+
111+
if (this._jasmine.isDomNode(object)) {
111112
let attrStr = '';
112-
Array.prototype.forEach.call(obj.attributes, attr => {
113+
Array.prototype.forEach.call(object.attributes, attr => {
113114
const attrName = attr.name.trim();
114115
const attrValue = attr.value.trim();
115116
attrStr += ' ' + attrName + '="' + attrValue + '"';
116117
});
117118
return (
118-
'HTMLNode(<' + obj.tagName + attrStr + '>{...}</' + obj.tagName + '>)'
119+
`HTMLNode(<${object.tagName + attrStr}>{...}</${object.tagName}>)`
119120
);
120121
}
121122

122123
if (!cycleWeakMap) {
123124
cycleWeakMap = new WeakMap();
124125
}
125126

126-
if (cycleWeakMap.get(obj) === true) {
127+
if (cycleWeakMap.get(object) === true) {
127128
return '<circular reference>';
128129
}
129-
cycleWeakMap.set(obj, true);
130+
cycleWeakMap.set(object, true);
131+
132+
const type = Object.prototype.toString.call(object);
133+
const output = [];
134+
if (type === '[object Map]') {
135+
indent = chalk.gray('|') + ' ' + indent;
136+
for (const value of object) {
137+
output.push(
138+
indent + value[0] + ': ' + this.prettyPrint(
139+
value[1],
140+
indent,
141+
cycleWeakMap
142+
)
143+
);
144+
}
145+
return `Map {\n${output.join(',')}\n}`;
146+
}
147+
if (type === '[object Set]') {
148+
for (const value of object) {
149+
output.push(
150+
this.prettyPrint(
151+
value,
152+
chalk.gray('|') + ' ' + indent,
153+
cycleWeakMap
154+
)
155+
);
156+
}
157+
return `Set [\n${indent}${output.join(', ')}\n${indent}]`;
158+
}
130159

131-
const orderedKeys = Object.keys(obj).sort();
160+
const orderedKeys = Object.keys(object).sort();
132161
let value;
133162
const keysOutput = [];
134163
const keyIndent = chalk.gray('|') + ' ';
135164
for (let i = 0; i < orderedKeys.length; i++) {
136-
value = obj[orderedKeys[i]];
165+
value = object[orderedKeys[i]];
137166
keysOutput.push(
138167
indent + keyIndent + orderedKeys[i] + ': ' +
139168
this.prettyPrint(value, indent + keyIndent, cycleWeakMap)
140169
);
141170
}
142171
return '{\n' + keysOutput.join(',\n') + '\n' + indent + '}';
143172
} else {
144-
return this._jasmine.pp(obj);
173+
return this._jasmine.pp(object);
145174
}
146175
}
147176

packages/jest-util/lib/__tests__/JasmineFormatter-test.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
jest.disableAutomock();
1313

14+
const chalk = require('chalk');
1415
const jasmine = require('jest-jasmine1/vendor/jasmine-1.3.0').jasmine;
1516
const jasmine2Require = require('jest-jasmine2/vendor/jasmine-2.4.1.js');
1617
const jasmine2 = jasmine2Require.core(jasmine2Require);
@@ -24,16 +25,61 @@ let formatter;
2425

2526
describe('JasmineFormatter', () => {
2627
describe('pretty printer', () => {
28+
beforeEach(() => {
29+
formatter = new JasmineFormatter(jasmine2);
30+
});
31+
2732
it('should handle JSDOM nodes with Jasmine 1.x', () => {
2833
formatter = new JasmineFormatter(jasmine);
2934

3035
expect(() => formatter.prettyPrint(jsdom(fixture).body)).not.toThrow();
3136
});
3237

3338
it('should handle JSDOM nodes with Jasmine 2.x', () => {
34-
formatter = new JasmineFormatter(jasmine2);
35-
3639
expect(() => formatter.prettyPrint(jsdom(fixture).body)).not.toThrow();
3740
});
41+
42+
it('should pretty print Maps', () => {
43+
const map = new Map([['foo', {
44+
bar: 'baz',
45+
}]]);
46+
47+
const printed = formatter.prettyPrint(map);
48+
const expected = 'Map {\n' +
49+
'| foo: {\n' +
50+
'| | bar: \'baz\'\n' +
51+
'| }\n' +
52+
'}';
53+
expect(printed).toBe(expected.replace(/\|/g, chalk.gray('|')));
54+
});
55+
56+
it('should pretty print Sets', () => {
57+
const set = new Set(['a', 'b', 'c']);
58+
const printed = formatter.prettyPrint(set);
59+
expect(printed).toBe(`Set [\n'a', 'b', 'c'\n]`);
60+
});
61+
62+
it('should pretty print mix of Sets, Maps and plain objects', () => {
63+
const map = new Map([['foo', {
64+
bar: 'baz',
65+
set: new Set(['a', 'b', {
66+
foo: 'bar',
67+
baz: 'foo',
68+
}]),
69+
}]]);
70+
const printed = formatter.prettyPrint(map);
71+
const expected = 'Map {\n' +
72+
'| foo: {\n' +
73+
'| | bar: \'baz\',\n' +
74+
'| | set: Set [\n' +
75+
'| | \'a\', \'b\', {\n' +
76+
'| | | | baz: \'foo\',\n' +
77+
'| | | | foo: \'bar\'\n' +
78+
'| | | }\n' +
79+
'| | ]\n' +
80+
'| }\n' +
81+
'}';
82+
expect(printed).toBe(expected.replace(/\|/g, chalk.gray('|')));
83+
});
3884
});
3985
});

0 commit comments

Comments
 (0)