Skip to content

Commit 2ece4f9

Browse files
WeiAnAnSimenB
authored andcommittedJan 19, 2020
Do not highlight matched asymmetricMatcher in diffs (#9257)
1 parent 2839036 commit 2ece4f9

File tree

9 files changed

+790
-10
lines changed

9 files changed

+790
-10
lines changed
 

‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- `[@jest/fake-timers]` Add Lolex as implementation of fake timers ([#8897](https://github.com/facebook/jest/pull/8897))
2929
- `[jest-get-type]` Add `BigInt` support. ([#8382](https://github.com/facebook/jest/pull/8382))
3030
- `[jest-matcher-utils]` Add `BigInt` support to `ensureNumbers` `ensureActualIsNumber`, `ensureExpectedIsNumber` ([#8382](https://github.com/facebook/jest/pull/8382))
31+
- `[jest-matcher-utils]` Ignore highlighting matched asymmetricMatcher in diffs ([#9257](https://github.com/facebook/jest/pull/9257))
3132
- `[jest-reporters]` Export utils for path formatting ([#9162](https://github.com/facebook/jest/pull/9162))
3233
- `[jest-reporters]` Provides global coverage thresholds as watermarks for istanbul ([#9416](https://github.com/facebook/jest/pull/9416))
3334
- `[jest-runner]` Warn if a worker had to be force exited ([#8206](https://github.com/facebook/jest/pull/8206))

‎packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap

+2-8
Original file line numberDiff line numberDiff line change
@@ -364,14 +364,8 @@ exports[`.toBe() fails for: {"a": [Function a], "b": 2} and {"a": Any<Function>,
364364

365365
<d>If it should pass with deep equality, replace "toBe" with "toStrictEqual"</>
366366

367-
<g>- Expected - 1</>
368-
<r>+ Received + 1</>
369-
370-
<d> Object {</>
371-
<g>- "a": Any<Function>,</>
372-
<r>+ "a": [Function a],</>
373-
<d> "b": 2,</>
374-
<d> }</>
367+
Expected: <g>{"a": Any<Function>, "b": 2}</>
368+
Received: <r>{"a": [Function a], "b": 2}</>
375369
`;
376370

377371
exports[`.toBe() fails for: {"a": 1} and {"a": 1} 1`] = `
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import getType = require('jest-get-type');
9+
10+
const supportTypes = ['map', 'array', 'object'];
11+
12+
type ReplaceableForEachCallBack = (value: any, key: any, object: any) => void;
13+
14+
export default class Replaceable {
15+
object: any;
16+
type: string;
17+
18+
constructor(object: any) {
19+
this.object = object;
20+
this.type = getType(object);
21+
if (!supportTypes.includes(this.type)) {
22+
throw new Error(`Type ${this.type} is not support in Replaceable!`);
23+
}
24+
}
25+
26+
static isReplaceable(obj1: any, obj2: any): boolean {
27+
const obj1Type = getType(obj1);
28+
const obj2Type = getType(obj2);
29+
return obj1Type === obj2Type && supportTypes.includes(obj1Type);
30+
}
31+
32+
forEach(cb: ReplaceableForEachCallBack): void {
33+
if (this.type === 'object') {
34+
Object.entries(this.object).forEach(([key, value]) => {
35+
cb(value, key, this.object);
36+
});
37+
} else {
38+
this.object.forEach(cb);
39+
}
40+
}
41+
42+
get(key: any): any {
43+
if (this.type === 'map') {
44+
return this.object.get(key);
45+
}
46+
return this.object[key];
47+
}
48+
49+
set(key: any, value: any): void {
50+
if (this.type === 'map') {
51+
this.object.set(key, value);
52+
} else {
53+
this.object[key] = value;
54+
}
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import Replaceable from '../Replaceable';
9+
10+
describe('Replaceable', () => {
11+
describe('constructor', () => {
12+
test('init with object', () => {
13+
const replaceable = new Replaceable({a: 1, b: 2});
14+
expect(replaceable.object).toEqual({a: 1, b: 2});
15+
expect(replaceable.type).toBe('object');
16+
});
17+
18+
test('init with array', () => {
19+
const replaceable = new Replaceable([1, 2, 3]);
20+
expect(replaceable.object).toEqual([1, 2, 3]);
21+
expect(replaceable.type).toBe('array');
22+
});
23+
24+
test('init with Map', () => {
25+
const replaceable = new Replaceable(
26+
new Map([
27+
['a', 1],
28+
['b', 2],
29+
]),
30+
);
31+
expect(replaceable.object).toEqual(
32+
new Map([
33+
['a', 1],
34+
['b', 2],
35+
]),
36+
);
37+
expect(replaceable.type).toBe('map');
38+
});
39+
40+
test('init with other type should throw error', () => {
41+
expect(() => {
42+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
43+
const replaceable = new Replaceable(new Date());
44+
}).toThrow('Type date is not support in Replaceable!');
45+
});
46+
});
47+
48+
describe('get', () => {
49+
test('get object item', () => {
50+
const replaceable = new Replaceable({a: 1, b: 2});
51+
expect(replaceable.get('b')).toBe(2);
52+
});
53+
54+
test('get array item', () => {
55+
const replaceable = new Replaceable([1, 2, 3]);
56+
expect(replaceable.get(1)).toBe(2);
57+
});
58+
59+
test('get Map item', () => {
60+
const replaceable = new Replaceable(
61+
new Map([
62+
['a', 1],
63+
['b', 2],
64+
]),
65+
);
66+
expect(replaceable.get('b')).toBe(2);
67+
});
68+
});
69+
70+
describe('set', () => {
71+
test('set object item', () => {
72+
const replaceable = new Replaceable({a: 1, b: 2});
73+
replaceable.set('b', 3);
74+
expect(replaceable.object).toEqual({a: 1, b: 3});
75+
});
76+
77+
test('set array item', () => {
78+
const replaceable = new Replaceable([1, 2, 3]);
79+
replaceable.set(1, 3);
80+
expect(replaceable.object).toEqual([1, 3, 3]);
81+
});
82+
83+
test('set Map item', () => {
84+
const replaceable = new Replaceable(
85+
new Map([
86+
['a', 1],
87+
['b', 2],
88+
]),
89+
);
90+
replaceable.set('b', 3);
91+
expect(replaceable.object).toEqual(
92+
new Map([
93+
['a', 1],
94+
['b', 3],
95+
]),
96+
);
97+
});
98+
});
99+
100+
describe('forEach', () => {
101+
test('object forEach', () => {
102+
const replaceable = new Replaceable({a: 1, b: 2});
103+
const cb = jest.fn();
104+
replaceable.forEach(cb);
105+
expect(cb.mock.calls[0]).toEqual([1, 'a', {a: 1, b: 2}]);
106+
expect(cb.mock.calls[1]).toEqual([2, 'b', {a: 1, b: 2}]);
107+
});
108+
109+
test('array forEach', () => {
110+
const replaceable = new Replaceable([1, 2, 3]);
111+
const cb = jest.fn();
112+
replaceable.forEach(cb);
113+
expect(cb.mock.calls[0]).toEqual([1, 0, [1, 2, 3]]);
114+
expect(cb.mock.calls[1]).toEqual([2, 1, [1, 2, 3]]);
115+
expect(cb.mock.calls[2]).toEqual([3, 2, [1, 2, 3]]);
116+
});
117+
118+
test('map forEach', () => {
119+
const map = new Map([
120+
['a', 1],
121+
['b', 2],
122+
]);
123+
const replaceable = new Replaceable(map);
124+
const cb = jest.fn();
125+
replaceable.forEach(cb);
126+
expect(cb.mock.calls[0]).toEqual([1, 'a', map]);
127+
expect(cb.mock.calls[1]).toEqual([2, 'b', map]);
128+
});
129+
});
130+
131+
describe('isReplaceable', () => {
132+
test('should return true if two object types equal and support', () => {
133+
expect(Replaceable.isReplaceable({a: 1}, {b: 2})).toBe(true);
134+
expect(Replaceable.isReplaceable([], [1, 2, 3])).toBe(true);
135+
expect(
136+
Replaceable.isReplaceable(
137+
new Map(),
138+
new Map([
139+
['a', 1],
140+
['b', 2],
141+
]),
142+
),
143+
).toBe(true);
144+
});
145+
146+
test('should return false if two object types not equal', () => {
147+
expect(Replaceable.isReplaceable({a: 1}, [1, 2, 3])).toBe(false);
148+
});
149+
150+
test('should return false if object types not support', () => {
151+
expect(Replaceable.isReplaceable('foo', 'bar')).toBe(false);
152+
});
153+
});
154+
});

‎packages/jest-matcher-utils/src/__tests__/__snapshots__/printDiffOrStringify.test.ts.snap

+155
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,160 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`printDiffOrStringify asymmetricMatcher array 1`] = `
4+
<g>- Expected - 1</>
5+
<r>+ Received + 1</>
6+
7+
<d> Array [</>
8+
<d> 1,</>
9+
<d> Any<Number>,</>
10+
<g>- 3,</>
11+
<r>+ 2,</>
12+
<d> ]</>
13+
`;
14+
15+
exports[`printDiffOrStringify asymmetricMatcher circular array 1`] = `
16+
<g>- Expected - 1</>
17+
<r>+ Received + 1</>
18+
19+
<d> Array [</>
20+
<d> 1,</>
21+
<d> Any<Number>,</>
22+
<g>- 3,</>
23+
<r>+ 2,</>
24+
<d> [Circular],</>
25+
<d> ]</>
26+
`;
27+
28+
exports[`printDiffOrStringify asymmetricMatcher circular map 1`] = `
29+
<g>- Expected - 2</>
30+
<r>+ Received + 2</>
31+
32+
<d> Map {</>
33+
<d> "a" => 1,</>
34+
<d> "b" => Any<Number>,</>
35+
<g>- "c" => 3,</>
36+
<r>+ "c" => 2,</>
37+
<d> "circular" => Map {</>
38+
<d> "a" => 1,</>
39+
<d> "b" => Any<Number>,</>
40+
<g>- "c" => 3,</>
41+
<r>+ "c" => 2,</>
42+
<d> "circular" => [Circular],</>
43+
<d> },</>
44+
<d> }</>
45+
`;
46+
47+
exports[`printDiffOrStringify asymmetricMatcher circular object 1`] = `
48+
<g>- Expected - 1</>
49+
<r>+ Received + 1</>
50+
51+
<d> Object {</>
52+
<d> "a": [Circular],</>
53+
<d> "b": Any<Number>,</>
54+
<g>- "c": 3,</>
55+
<r>+ "c": 2,</>
56+
<d> }</>
57+
`;
58+
59+
exports[`printDiffOrStringify asymmetricMatcher custom asymmetricMatcher 1`] = `
60+
<g>- Expected - 1</>
61+
<r>+ Received + 1</>
62+
63+
<d> Object {</>
64+
<d> "a": equal5<>,</>
65+
<g>- "b": false,</>
66+
<r>+ "b": true,</>
67+
<d> }</>
68+
`;
69+
70+
exports[`printDiffOrStringify asymmetricMatcher jest asymmetricMatcher 1`] = `
71+
<g>- Expected - 1</>
72+
<r>+ Received + 1</>
73+
74+
<d> Object {</>
75+
<d> "a": Any<Number>,</>
76+
<d> "b": Anything,</>
77+
<d> "c": ArrayContaining [</>
78+
<d> 1,</>
79+
<d> 3,</>
80+
<d> ],</>
81+
<d> "d": StringContaining "jest",</>
82+
<d> "e": StringMatching /^jest/,</>
83+
<d> "f": ObjectContaining {</>
84+
<d> "a": Any<Date>,</>
85+
<d> },</>
86+
<g>- "g": true,</>
87+
<r>+ "g": false,</>
88+
<d> }</>
89+
`;
90+
91+
exports[`printDiffOrStringify asymmetricMatcher map 1`] = `
92+
<g>- Expected - 1</>
93+
<r>+ Received + 1</>
94+
95+
<d> Map {</>
96+
<d> "a" => 1,</>
97+
<d> "b" => Any<Number>,</>
98+
<g>- "c" => 3,</>
99+
<r>+ "c" => 2,</>
100+
<d> }</>
101+
`;
102+
103+
exports[`printDiffOrStringify asymmetricMatcher minimal test 1`] = `
104+
<g>- Expected - 1</>
105+
<r>+ Received + 1</>
106+
107+
<d> Object {</>
108+
<d> "a": Any<Number>,</>
109+
<g>- "b": 2,</>
110+
<r>+ "b": 1,</>
111+
<d> }</>
112+
`;
113+
114+
exports[`printDiffOrStringify asymmetricMatcher nested object 1`] = `
115+
<g>- Expected - 1</>
116+
<r>+ Received + 1</>
117+
118+
<d> Object {</>
119+
<d> "a": Any<Number>,</>
120+
<d> "b": Object {</>
121+
<d> "a": 1,</>
122+
<d> "b": Any<Number>,</>
123+
<d> },</>
124+
<g>- "c": 2,</>
125+
<r>+ "c": 1,</>
126+
<d> }</>
127+
`;
128+
129+
exports[`printDiffOrStringify asymmetricMatcher object in array 1`] = `
130+
<g>- Expected - 1</>
131+
<r>+ Received + 1</>
132+
133+
<d> Array [</>
134+
<d> 1,</>
135+
<d> Object {</>
136+
<d> "a": 1,</>
137+
<d> "b": Any<Number>,</>
138+
<d> },</>
139+
<g>- 3,</>
140+
<r>+ 2,</>
141+
<d> ]</>
142+
`;
143+
144+
exports[`printDiffOrStringify asymmetricMatcher transitive circular 1`] = `
145+
<g>- Expected - 1</>
146+
<r>+ Received + 1</>
147+
148+
<d> Object {</>
149+
<g>- "a": 3,</>
150+
<r>+ "a": 2,</>
151+
<d> "nested": Object {</>
152+
<d> "b": Any<Number>,</>
153+
<d> "parent": [Circular],</>
154+
<d> },</>
155+
<d> }</>
156+
`;
157+
3158
exports[`printDiffOrStringify expected and received are multi line with trailing spaces 1`] = `
4159
<g>- Expected - 3</>
5160
<r>+ Received + 3</>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import deepCyclicCopyReplaceable from '../deepCyclicCopyReplaceable';
10+
11+
test('returns the same value for primitive or function values', () => {
12+
const fn = () => {};
13+
14+
expect(deepCyclicCopyReplaceable(undefined)).toBe(undefined);
15+
expect(deepCyclicCopyReplaceable(null)).toBe(null);
16+
expect(deepCyclicCopyReplaceable(true)).toBe(true);
17+
expect(deepCyclicCopyReplaceable(42)).toBe(42);
18+
expect(Number.isNaN(deepCyclicCopyReplaceable(NaN))).toBe(true);
19+
expect(deepCyclicCopyReplaceable('foo')).toBe('foo');
20+
expect(deepCyclicCopyReplaceable(fn)).toBe(fn);
21+
});
22+
23+
test('does not execute getters/setters, but copies them', () => {
24+
const fn = jest.fn();
25+
const obj = {
26+
// @ts-ignore
27+
get foo() {
28+
fn();
29+
},
30+
};
31+
const copy = deepCyclicCopyReplaceable(obj);
32+
33+
expect(Object.getOwnPropertyDescriptor(copy, 'foo')).toBeDefined();
34+
expect(fn).not.toBeCalled();
35+
});
36+
37+
test('copies symbols', () => {
38+
const symbol = Symbol('foo');
39+
const obj = {[symbol]: 42};
40+
41+
expect(deepCyclicCopyReplaceable(obj)[symbol]).toBe(42);
42+
});
43+
44+
test('copies arrays as array objects', () => {
45+
const array = [null, 42, 'foo', 'bar', [], {}];
46+
47+
expect(deepCyclicCopyReplaceable(array)).toEqual(array);
48+
expect(Array.isArray(deepCyclicCopyReplaceable(array))).toBe(true);
49+
});
50+
51+
test('handles cyclic dependencies', () => {
52+
const cyclic: any = {a: 42, subcycle: {}};
53+
54+
cyclic.subcycle.baz = cyclic;
55+
cyclic.bar = cyclic;
56+
57+
expect(() => deepCyclicCopyReplaceable(cyclic)).not.toThrow();
58+
59+
const copy = deepCyclicCopyReplaceable(cyclic);
60+
61+
expect(copy.a).toBe(42);
62+
expect(copy.bar).toEqual(copy);
63+
expect(copy.subcycle.baz).toEqual(copy);
64+
});
65+
66+
test('Copy Map', () => {
67+
const map = new Map([
68+
['a', 1],
69+
['b', 2],
70+
]);
71+
const copy = deepCyclicCopyReplaceable(map);
72+
expect(copy).toEqual(map);
73+
expect(copy.constructor).toBe(Map);
74+
});
75+
76+
test('Copy cyclic Map', () => {
77+
const map: Map<any, any> = new Map([
78+
['a', 1],
79+
['b', 2],
80+
]);
81+
map.set('map', map);
82+
expect(deepCyclicCopyReplaceable(map)).toEqual(map);
83+
});
84+
85+
test('return same value for built-in object type except array, map and object', () => {
86+
const date = new Date();
87+
const buffer = Buffer.from('jest');
88+
const numberArray = new Uint8Array([1, 2, 3]);
89+
const regexp = /jest/;
90+
const set = new Set(['foo', 'bar']);
91+
92+
expect(deepCyclicCopyReplaceable(date)).toBe(date);
93+
expect(deepCyclicCopyReplaceable(buffer)).toBe(buffer);
94+
expect(deepCyclicCopyReplaceable(numberArray)).toBe(numberArray);
95+
expect(deepCyclicCopyReplaceable(regexp)).toBe(regexp);
96+
expect(deepCyclicCopyReplaceable(set)).toBe(set);
97+
});

‎packages/jest-matcher-utils/src/__tests__/printDiffOrStringify.test.ts

+159-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {INVERTED_COLOR, printDiffOrStringify} from '../index';
1111
expect.addSnapshotSerializer(alignedAnsiStyleSerializer);
1212

1313
describe('printDiffOrStringify', () => {
14-
const testDiffOrStringify = (expected: string, received: string): string =>
14+
const testDiffOrStringify = (expected: unknown, received: unknown): string =>
1515
printDiffOrStringify(expected, received, 'Expected', 'Received', true);
1616

1717
test('expected is empty and received is single line', () => {
@@ -86,4 +86,162 @@ describe('printDiffOrStringify', () => {
8686
expect(difference).not.toMatch(lessChange);
8787
});
8888
});
89+
90+
describe('asymmetricMatcher', () => {
91+
test('minimal test', () => {
92+
const expected = {a: expect.any(Number), b: 2};
93+
const received = {a: 1, b: 1};
94+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
95+
});
96+
97+
test('jest asymmetricMatcher', () => {
98+
const expected = {
99+
a: expect.any(Number),
100+
b: expect.anything(),
101+
c: expect.arrayContaining([1, 3]),
102+
d: 'jest is awesome',
103+
e: 'jest is awesome',
104+
f: {
105+
a: new Date(),
106+
b: 'jest is awesome',
107+
},
108+
g: true,
109+
};
110+
const received = {
111+
a: 1,
112+
b: 'anything',
113+
c: [1, 2, 3],
114+
d: expect.stringContaining('jest'),
115+
e: expect.stringMatching(/^jest/),
116+
f: expect.objectContaining({
117+
a: expect.any(Date),
118+
}),
119+
g: false,
120+
};
121+
122+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
123+
});
124+
125+
test('custom asymmetricMatcher', () => {
126+
expect.extend({
127+
equal5(received: any) {
128+
if (received === 5)
129+
return {
130+
message: () => `expected ${received} not to be 5`,
131+
pass: true,
132+
};
133+
return {
134+
message: () => `expected ${received} to be 5`,
135+
pass: false,
136+
};
137+
},
138+
});
139+
const expected = {
140+
a: expect.equal5(),
141+
b: false,
142+
};
143+
const received = {
144+
a: 5,
145+
b: true,
146+
};
147+
148+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
149+
});
150+
151+
test('nested object', () => {
152+
const expected = {
153+
a: 1,
154+
b: {
155+
a: 1,
156+
b: expect.any(Number),
157+
},
158+
c: 2,
159+
};
160+
const received = {
161+
a: expect.any(Number),
162+
b: {
163+
a: 1,
164+
b: 2,
165+
},
166+
c: 1,
167+
};
168+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
169+
});
170+
171+
test('array', () => {
172+
const expected: Array<any> = [1, expect.any(Number), 3];
173+
const received: Array<any> = [1, 2, 2];
174+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
175+
});
176+
177+
test('object in array', () => {
178+
const expected: Array<any> = [1, {a: 1, b: expect.any(Number)}, 3];
179+
const received: Array<any> = [1, {a: 1, b: 2}, 2];
180+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
181+
});
182+
183+
test('map', () => {
184+
const expected: Map<any, any> = new Map([
185+
['a', 1],
186+
['b', expect.any(Number)],
187+
['c', 3],
188+
]);
189+
const received: Map<any, any> = new Map([
190+
['a', 1],
191+
['b', 2],
192+
['c', 2],
193+
]);
194+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
195+
});
196+
197+
test('circular object', () => {
198+
const expected: any = {
199+
b: expect.any(Number),
200+
c: 3,
201+
};
202+
expected.a = expected;
203+
const received: any = {
204+
b: 2,
205+
c: 2,
206+
};
207+
received.a = received;
208+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
209+
});
210+
211+
test('transitive circular', () => {
212+
const expected: any = {
213+
a: 3,
214+
};
215+
expected.nested = {b: expect.any(Number), parent: expected};
216+
const received: any = {
217+
a: 2,
218+
};
219+
received.nested = {b: 2, parent: received};
220+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
221+
});
222+
223+
test('circular array', () => {
224+
const expected: Array<any> = [1, expect.any(Number), 3];
225+
expected.push(expected);
226+
const received: Array<any> = [1, 2, 2];
227+
received.push(received);
228+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
229+
});
230+
231+
test('circular map', () => {
232+
const expected: Map<any, any> = new Map([
233+
['a', 1],
234+
['b', expect.any(Number)],
235+
['c', 3],
236+
]);
237+
expected.set('circular', expected);
238+
const received: Map<any, any> = new Map([
239+
['a', 1],
240+
['b', 2],
241+
['c', 2],
242+
]);
243+
received.set('circular', received);
244+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
245+
});
246+
});
89247
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
const builtInObject = [
9+
Array,
10+
Buffer,
11+
Date,
12+
Float32Array,
13+
Float64Array,
14+
Int16Array,
15+
Int32Array,
16+
Int8Array,
17+
Map,
18+
Set,
19+
RegExp,
20+
Uint16Array,
21+
Uint32Array,
22+
Uint8Array,
23+
Uint8ClampedArray,
24+
];
25+
26+
const isBuiltInObject = (object: any) =>
27+
builtInObject.includes(object.constructor);
28+
29+
const isMap = (value: any): value is Map<any, any> => value.constructor === Map;
30+
31+
export default function deepCyclicCopyReplaceable<T>(
32+
value: T,
33+
cycles: WeakMap<any, any> = new WeakMap(),
34+
): T {
35+
if (typeof value !== 'object' || value === null) {
36+
return value;
37+
} else if (cycles.has(value)) {
38+
return cycles.get(value);
39+
} else if (Array.isArray(value)) {
40+
return deepCyclicCopyArray(value, cycles);
41+
} else if (isMap(value)) {
42+
return deepCyclicCopyMap(value, cycles);
43+
} else if (isBuiltInObject(value)) {
44+
return value;
45+
} else {
46+
return deepCyclicCopyObject(value, cycles);
47+
}
48+
}
49+
50+
function deepCyclicCopyObject<T>(object: T, cycles: WeakMap<any, any>): T {
51+
const newObject = Object.create(Object.getPrototypeOf(object));
52+
const descriptors = Object.getOwnPropertyDescriptors(object);
53+
54+
cycles.set(object, newObject);
55+
56+
Object.keys(descriptors).forEach(key => {
57+
const descriptor = descriptors[key];
58+
if (typeof descriptor.value !== 'undefined') {
59+
descriptor.value = deepCyclicCopyReplaceable(descriptor.value, cycles);
60+
}
61+
62+
descriptor.configurable = true;
63+
});
64+
65+
return Object.defineProperties(newObject, descriptors);
66+
}
67+
68+
function deepCyclicCopyArray<T>(array: Array<T>, cycles: WeakMap<any, any>): T {
69+
const newArray = new (Object.getPrototypeOf(array).constructor)(array.length);
70+
const length = array.length;
71+
72+
cycles.set(array, newArray);
73+
74+
for (let i = 0; i < length; i++) {
75+
newArray[i] = deepCyclicCopyReplaceable(array[i], cycles);
76+
}
77+
78+
return newArray;
79+
}
80+
81+
function deepCyclicCopyMap<T>(
82+
map: Map<any, any>,
83+
cycles: WeakMap<any, any>,
84+
): T {
85+
const newMap = new Map();
86+
87+
cycles.set(map, newMap);
88+
89+
map.forEach((value, key) => {
90+
newMap.set(key, deepCyclicCopyReplaceable(value, cycles));
91+
});
92+
93+
return newMap as any;
94+
}

‎packages/jest-matcher-utils/src/index.ts

+72-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import diffDefault, {
1717
} from 'jest-diff';
1818
import getType = require('jest-get-type');
1919
import prettyFormat = require('pretty-format');
20+
import Replaceable from './Replaceable';
21+
import deepCyclicCopyReplaceable from './deepCyclicCopyReplaceable';
2022

2123
const {
2224
AsymmetricMatcher,
@@ -351,7 +353,16 @@ export const printDiffOrStringify = (
351353
}
352354

353355
if (isLineDiffable(expected, received)) {
354-
const difference = diffDefault(expected, received, {
356+
const {
357+
replacedExpected,
358+
replacedReceived,
359+
} = replaceMatchedToAsymmetricMatcher(
360+
deepCyclicCopyReplaceable(expected),
361+
deepCyclicCopyReplaceable(received),
362+
[],
363+
[],
364+
);
365+
const difference = diffDefault(replacedExpected, replacedReceived, {
355366
aAnnotation: expectedLabel,
356367
bAnnotation: receivedLabel,
357368
expand,
@@ -394,6 +405,66 @@ const shouldPrintDiff = (actual: unknown, expected: unknown) => {
394405
return true;
395406
};
396407

408+
function replaceMatchedToAsymmetricMatcher(
409+
replacedExpected: unknown,
410+
replacedReceived: unknown,
411+
expectedCycles: Array<any>,
412+
receivedCycles: Array<any>,
413+
) {
414+
if (!Replaceable.isReplaceable(replacedExpected, replacedReceived)) {
415+
return {replacedExpected, replacedReceived};
416+
}
417+
418+
if (
419+
expectedCycles.includes(replacedExpected) ||
420+
receivedCycles.includes(replacedReceived)
421+
) {
422+
return {replacedExpected, replacedReceived};
423+
}
424+
425+
expectedCycles.push(replacedExpected);
426+
receivedCycles.push(replacedReceived);
427+
428+
const expectedReplaceable = new Replaceable(replacedExpected);
429+
const receivedReplaceable = new Replaceable(replacedReceived);
430+
431+
expectedReplaceable.forEach((expectedValue: unknown, key: unknown) => {
432+
const receivedValue = receivedReplaceable.get(key);
433+
if (isAsymmetricMatcher(expectedValue)) {
434+
if (expectedValue.asymmetricMatch(receivedValue)) {
435+
receivedReplaceable.set(key, expectedValue);
436+
}
437+
} else if (isAsymmetricMatcher(receivedValue)) {
438+
if (receivedValue.asymmetricMatch(expectedValue)) {
439+
expectedReplaceable.set(key, receivedValue);
440+
}
441+
} else if (Replaceable.isReplaceable(expectedValue, receivedValue)) {
442+
const replaced = replaceMatchedToAsymmetricMatcher(
443+
expectedValue,
444+
receivedValue,
445+
expectedCycles,
446+
receivedCycles,
447+
);
448+
expectedReplaceable.set(key, replaced.replacedExpected);
449+
receivedReplaceable.set(key, replaced.replacedReceived);
450+
}
451+
});
452+
453+
return {
454+
replacedExpected: expectedReplaceable.object,
455+
replacedReceived: receivedReplaceable.object,
456+
};
457+
}
458+
459+
type AsymmetricMatcher = {
460+
asymmetricMatch: Function;
461+
};
462+
463+
function isAsymmetricMatcher(data: any): data is AsymmetricMatcher {
464+
const type = getType(data);
465+
return type === 'object' && typeof data.asymmetricMatch === 'function';
466+
}
467+
397468
export const diff = (a: any, b: any, options?: DiffOptions): string | null =>
398469
shouldPrintDiff(a, b) ? diffDefault(a, b, options) : null;
399470

0 commit comments

Comments
 (0)
Please sign in to comment.