Skip to content

Commit 2e30f52

Browse files
authored
Fix diffing object contain readonly symbol key object (#10414)
1 parent 200adc0 commit 2e30f52

12 files changed

+241
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

7+
- `[jest-matcher-utils]` Fix diffing object contain readonly symbol key object ([#10414](https://github.com/facebook/jest/pull/10414))
78
- `[jest-reporters]` Fixes notify reporter on Linux (using notify-send) ([#10393](https://github.com/facebook/jest/pull/10400))
89
- `[jest-snapshot]` Correctly handles arrays and property matchers in snapshots ([#10404](https://github.com/facebook/jest/pull/10404))
910

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`should work without error 1`] = `
4+
"FAIL __tests__/dom.test.js
5+
✕ use toBe compare two div (<<REPLACED>>)
6+
✕ compare span and div (<<REPLACED>>)
7+
8+
● use toBe compare two div
9+
10+
expect(received).toBe(expected) // Object.is equality
11+
12+
If it should pass with deep equality, replace \\"toBe\\" with \\"toStrictEqual\\"
13+
14+
Expected: <div />
15+
Received: serializes to the same string
16+
17+
12 | const div1 = document.createElement('div');
18+
13 | const div2 = document.createElement('div');
19+
> 14 | expect(div1).toBe(div2);
20+
| ^
21+
15 | });
22+
16 |
23+
17 | test('compare span and div', () => {
24+
25+
at Object.toBe (__tests__/dom.test.js:14:16)
26+
27+
compare span and div
28+
29+
expect(received).toBe(expected) // Object.is equality
30+
31+
- Expected - 1
32+
+ Received + 1
33+
34+
- <span />
35+
+ <div />
36+
37+
16 |
38+
17 | test('compare span and div', () => {
39+
> 18 | expect(document.createElement('div')).toBe(document.createElement('span'));
40+
| ^
41+
19 | });
42+
20 |
43+
44+
at Object.toBe (__tests__/dom.test.js:18:41)
45+
46+
Test Suites: 1 failed, 1 total
47+
Tests: 2 failed, 2 total
48+
Snapshots: 0 total
49+
Time: <<REPLACED>>
50+
Ran all test suites."
51+
`;

e2e/__tests__/domDiffing.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 runJest from '../runJest';
10+
import {replaceTime} from '../Utils';
11+
12+
test('should work without error', () => {
13+
const output = runJest('dom-diffing');
14+
expect(output.failed).toBe(true);
15+
expect(replaceTime(output.stderr)).toMatchSnapshot();
16+
});

e2e/dom-diffing/__tests__/dom.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
/* eslint-env browser */
10+
11+
test('use toBe compare two div', () => {
12+
const div1 = document.createElement('div');
13+
const div2 = document.createElement('div');
14+
expect(div1).toBe(div2);
15+
});
16+
17+
test('compare span and div', () => {
18+
expect(document.createElement('div')).toBe(document.createElement('span'));
19+
});

e2e/dom-diffing/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"jest": {}
3+
}

packages/jest-matcher-utils/src/Replaceable.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,17 @@ export default class Replaceable {
3232

3333
forEach(cb: ReplaceableForEachCallBack): void {
3434
if (this.type === 'object') {
35-
Object.entries(this.object).forEach(([key, value]) => {
36-
cb(value, key, this.object);
37-
});
38-
Object.getOwnPropertySymbols(this.object).forEach(key => {
39-
cb(this.object[key], key, this.object);
40-
});
35+
const descriptors = Object.getOwnPropertyDescriptors(this.object);
36+
[
37+
...Object.keys(descriptors),
38+
...Object.getOwnPropertySymbols(descriptors),
39+
]
40+
//@ts-expect-error because typescript do not support symbol key in object
41+
//https://github.com/microsoft/TypeScript/issues/1863
42+
.filter(key => descriptors[key].enumerable)
43+
.forEach(key => {
44+
cb(this.object[key], key, this.object);
45+
});
4146
} else {
4247
this.object.forEach(cb);
4348
}

packages/jest-matcher-utils/src/__tests__/Replaceable.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,30 @@ describe('Replaceable', () => {
132132
expect(cb.mock.calls[0]).toEqual([1, 'a', map]);
133133
expect(cb.mock.calls[1]).toEqual([2, 'b', map]);
134134
});
135+
136+
test('forEach should ignore nonenumerable property', () => {
137+
const symbolKey = Symbol('jest');
138+
const symbolKey2 = Symbol('awesome');
139+
const object = {a: 1, [symbolKey]: 3};
140+
Object.defineProperty(object, 'b', {
141+
configurable: true,
142+
enumerable: false,
143+
value: 2,
144+
writable: true,
145+
});
146+
Object.defineProperty(object, symbolKey2, {
147+
configurable: true,
148+
enumerable: false,
149+
value: 4,
150+
writable: true,
151+
});
152+
const replaceable = new Replaceable(object);
153+
const cb = jest.fn();
154+
replaceable.forEach(cb);
155+
expect(cb).toHaveBeenCalledTimes(2);
156+
expect(cb.mock.calls[0]).toEqual([1, 'a', object]);
157+
expect(cb.mock.calls[1]).toEqual([3, symbolKey, object]);
158+
});
135159
});
136160

137161
describe('isReplaceable', () => {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,16 @@ exports[`printDiffOrStringify has no common after clean up chaff one-line 1`] =
198198
Expected: <g>"delete"</>
199199
Received: <r>"insert"</>
200200
`;
201+
202+
exports[`printDiffOrStringify object contain readonly symbol key object 1`] = `
203+
<g>- Expected - 1</>
204+
<r>+ Received + 1</>
205+
206+
<d> Object {</>
207+
<g>- "b": 2,</>
208+
<r>+ "b": 1,</>
209+
<d> Symbol(key): Object {</>
210+
<d> "a": 1,</>
211+
<d> },</>
212+
<d> }</>
213+
`;

packages/jest-matcher-utils/src/__tests__/deepCyclicCopyReplaceable.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,20 @@ test('convert accessor descriptor into value descriptor', () => {
4343
});
4444
});
4545

46-
test('skips non-enumerables', () => {
46+
test('shuold not skips non-enumerables', () => {
4747
const obj = {};
4848
Object.defineProperty(obj, 'foo', {enumerable: false, value: 'bar'});
4949

5050
const copy = deepCyclicCopyReplaceable(obj);
5151

52-
expect(Object.getOwnPropertyDescriptors(copy)).toEqual({});
52+
expect(Object.getOwnPropertyDescriptors(copy)).toEqual({
53+
foo: {
54+
configurable: true,
55+
enumerable: false,
56+
value: 'bar',
57+
writable: true,
58+
},
59+
});
5360
});
5461

5562
test('copies symbols', () => {
@@ -113,3 +120,22 @@ test('return same value for built-in object type except array, map and object',
113120
expect(deepCyclicCopyReplaceable(regexp)).toBe(regexp);
114121
expect(deepCyclicCopyReplaceable(set)).toBe(set);
115122
});
123+
124+
test('should copy object symbol key property', () => {
125+
const symbolKey = Symbol.for('key');
126+
expect(deepCyclicCopyReplaceable({[symbolKey]: 1})).toEqual({[symbolKey]: 1});
127+
});
128+
129+
test('should set writable, configurable to true', () => {
130+
const a = {};
131+
Object.defineProperty(a, 'key', {
132+
configurable: false,
133+
enumerable: true,
134+
value: 1,
135+
writable: false,
136+
});
137+
const copied = deepCyclicCopyReplaceable(a);
138+
expect(Object.getOwnPropertyDescriptors(copied)).toEqual({
139+
key: {configurable: true, enumerable: true, value: 1, writable: true},
140+
});
141+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
* @jest-environment jsdom
8+
*/
9+
/* eslint-env browser*/
10+
import deepCyclicCopyReplaceable from '../deepCyclicCopyReplaceable';
11+
12+
test('should copy dom element', () => {
13+
const div = document.createElement('div');
14+
const copied = deepCyclicCopyReplaceable(div);
15+
expect(copied).toEqual(div);
16+
expect(div === copied).toBe(false); //assert reference is not the same
17+
});
18+
19+
test('should copy complex element', () => {
20+
const div = document.createElement('div');
21+
const span = document.createElement('span');
22+
div.setAttribute('id', 'div');
23+
div.innerText = 'this is div';
24+
div.appendChild(span);
25+
const copied = deepCyclicCopyReplaceable(div);
26+
expect(copied).toEqual(div);
27+
expect(div === copied).toBe(false); //assert reference is not the same
28+
expect(div.children[0] === copied.children[0]).toBe(false); //assert reference is not the same
29+
});

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ describe('printDiffOrStringify', () => {
5050
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
5151
});
5252

53+
test('object contain readonly symbol key object', () => {
54+
const expected = {b: 2};
55+
const received = {b: 1};
56+
const symbolKey = Symbol.for('key');
57+
Object.defineProperty(expected, symbolKey, {
58+
configurable: true,
59+
enumerable: true,
60+
value: {
61+
a: 1,
62+
},
63+
writable: false,
64+
});
65+
Object.defineProperty(received, symbolKey, {
66+
configurable: true,
67+
enumerable: true,
68+
value: {
69+
a: 1,
70+
},
71+
writable: false,
72+
});
73+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
74+
});
75+
5376
describe('MAX_DIFF_STRING_LENGTH', () => {
5477
const lessChange = INVERTED_COLOR('single ');
5578
const less = 'single line';

packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import {plugins} from 'pretty-format';
9+
810
const builtInObject = [
911
Array,
1012
Buffer,
@@ -42,6 +44,8 @@ export default function deepCyclicCopyReplaceable<T>(
4244
return deepCyclicCopyMap(value, cycles);
4345
} else if (isBuiltInObject(value)) {
4446
return value;
47+
} else if (plugins.DOMElement.test(value)) {
48+
return (((value as unknown) as Element).cloneNode(true) as unknown) as T;
4549
} else {
4650
return deepCyclicCopyObject(value, cycles);
4751
}
@@ -55,25 +59,33 @@ function deepCyclicCopyObject<T>(object: T, cycles: WeakMap<any, any>): T {
5559

5660
cycles.set(object, newObject);
5761

58-
Object.keys(descriptors).forEach(key => {
59-
if (descriptors[key].enumerable) {
60-
descriptors[key] = {
62+
const newDescriptors = [
63+
...Object.keys(descriptors),
64+
...Object.getOwnPropertySymbols(descriptors),
65+
].reduce(
66+
//@ts-expect-error because typescript do not support symbol key in object
67+
//https://github.com/microsoft/TypeScript/issues/1863
68+
(newDescriptors: {[x: string]: PropertyDescriptor}, key: string) => {
69+
const enumerable = descriptors[key].enumerable;
70+
71+
newDescriptors[key] = {
6172
configurable: true,
62-
enumerable: true,
73+
enumerable,
6374
value: deepCyclicCopyReplaceable(
6475
// this accesses the value or getter, depending. We just care about the value anyways, and this allows us to not mess with accessors
6576
// it has the side effect of invoking the getter here though, rather than copying it over
66-
(object as Record<string, unknown>)[key],
77+
(object as Record<string | symbol, unknown>)[key],
6778
cycles,
6879
),
6980
writable: true,
7081
};
71-
} else {
72-
delete descriptors[key];
73-
}
74-
});
75-
76-
return Object.defineProperties(newObject, descriptors);
82+
return newDescriptors;
83+
},
84+
{},
85+
);
86+
//@ts-expect-error because typescript do not support symbol key in object
87+
//https://github.com/microsoft/TypeScript/issues/1863
88+
return Object.defineProperties(newObject, newDescriptors);
7789
}
7890

7991
function deepCyclicCopyArray<T>(array: Array<T>, cycles: WeakMap<any, any>): T {

0 commit comments

Comments
 (0)