Skip to content

Commit 3554623

Browse files
authored
fix: drop getters and setters when diffing objects for error (#9757)
1 parent 26951bd commit 3554623

File tree

7 files changed

+187
-33
lines changed

7 files changed

+187
-33
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
### Features
44

55
### Fixes
6+
7+
- `[jest-matcher-utils]` Replace accessors with values to avoid calling setters in object descriptors when computing diffs for error reporting ([#9757](https://github.com/facebook/jest/pull/9757))
68
- `[@jest/watcher]` Correct return type of `shouldRunTestSuite` for `JestHookEmitter` ([#9753](https://github.com/facebook/jest/pull/9753))
79

810
### Chore & Maintenance

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,6 +2127,76 @@ exports[`.toEqual() {pass: false} expect({"foo": {"bar": 1}}).toEqual({"foo": {}
21272127
<d> }</>
21282128
`;
21292129

2130+
exports[`.toEqual() {pass: false} expect({"frozenGetter": {}}).toEqual({"frozenGetter": {"foo": "bar"}}) 1`] = `
2131+
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>
2132+
2133+
<g>- Expected - 3</>
2134+
<r>+ Received + 1</>
2135+
2136+
<d> Object {</>
2137+
<g>- "frozenGetter": Object {</>
2138+
<g>- "foo": "bar",</>
2139+
<g>- },</>
2140+
<r>+ "frozenGetter": Object {},</>
2141+
<d> }</>
2142+
`;
2143+
2144+
exports[`.toEqual() {pass: false} expect({"frozenGetterAndSetter": {}}).toEqual({"frozenGetterAndSetter": {"foo": "bar"}}) 1`] = `
2145+
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>
2146+
2147+
<g>- Expected - 3</>
2148+
<r>+ Received + 1</>
2149+
2150+
<d> Object {</>
2151+
<g>- "frozenGetterAndSetter": Object {</>
2152+
<g>- "foo": "bar",</>
2153+
<g>- },</>
2154+
<r>+ "frozenGetterAndSetter": Object {},</>
2155+
<d> }</>
2156+
`;
2157+
2158+
exports[`.toEqual() {pass: false} expect({"frozenSetter": undefined}).toEqual({"frozenSetter": {"foo": "bar"}}) 1`] = `
2159+
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>
2160+
2161+
<g>- Expected - 3</>
2162+
<r>+ Received + 1</>
2163+
2164+
<d> Object {</>
2165+
<g>- "frozenSetter": Object {</>
2166+
<g>- "foo": "bar",</>
2167+
<g>- },</>
2168+
<r>+ "frozenSetter": undefined,</>
2169+
<d> }</>
2170+
`;
2171+
2172+
exports[`.toEqual() {pass: false} expect({"getter": {}}).toEqual({"getter": {"foo": "bar"}}) 1`] = `
2173+
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>
2174+
2175+
<g>- Expected - 3</>
2176+
<r>+ Received + 1</>
2177+
2178+
<d> Object {</>
2179+
<g>- "getter": Object {</>
2180+
<g>- "foo": "bar",</>
2181+
<g>- },</>
2182+
<r>+ "getter": Object {},</>
2183+
<d> }</>
2184+
`;
2185+
2186+
exports[`.toEqual() {pass: false} expect({"getterAndSetter": {}}).toEqual({"getterAndSetter": {"foo": "bar"}}) 1`] = `
2187+
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>
2188+
2189+
<g>- Expected - 3</>
2190+
<r>+ Received + 1</>
2191+
2192+
<d> Object {</>
2193+
<g>- "getterAndSetter": Object {</>
2194+
<g>- "foo": "bar",</>
2195+
<g>- },</>
2196+
<r>+ "getterAndSetter": Object {},</>
2197+
<d> }</>
2198+
`;
2199+
21302200
exports[`.toEqual() {pass: false} expect({"nodeName": "div", "nodeType": 1}).toEqual({"nodeName": "p", "nodeType": 1}) 1`] = `
21312201
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>
21322202

@@ -2140,6 +2210,20 @@ exports[`.toEqual() {pass: false} expect({"nodeName": "div", "nodeType": 1}).toE
21402210
<d> }</>
21412211
`;
21422212

2213+
exports[`.toEqual() {pass: false} expect({"setter": undefined}).toEqual({"setter": {"foo": "bar"}}) 1`] = `
2214+
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>
2215+
2216+
<g>- Expected - 3</>
2217+
<r>+ Received + 1</>
2218+
2219+
<d> Object {</>
2220+
<g>- "setter": Object {</>
2221+
<g>- "foo": "bar",</>
2222+
<g>- },</>
2223+
<r>+ "setter": undefined,</>
2224+
<d> }</>
2225+
`;
2226+
21432227
exports[`.toEqual() {pass: false} expect({"target": {"nodeType": 1, "value": "a"}}).toEqual({"target": {"nodeType": 1, "value": "b"}}) 1`] = `
21442228
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>
21452229

packages/expect/src/__tests__/matchers.test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,62 @@ describe('.toEqual()', () => {
435435
[{a: 1}, {a: 2}],
436436
[{a: 5}, {b: 6}],
437437
[Object.freeze({foo: {bar: 1}}), {foo: {}}],
438+
[
439+
{
440+
get getterAndSetter() {
441+
return {};
442+
},
443+
set getterAndSetter(value) {
444+
throw new Error('noo');
445+
},
446+
},
447+
{getterAndSetter: {foo: 'bar'}},
448+
],
449+
[
450+
Object.freeze({
451+
get frozenGetterAndSetter() {
452+
return {};
453+
},
454+
set frozenGetterAndSetter(value) {
455+
throw new Error('noo');
456+
},
457+
}),
458+
{frozenGetterAndSetter: {foo: 'bar'}},
459+
],
460+
[
461+
{
462+
get getter() {
463+
return {};
464+
},
465+
},
466+
{getter: {foo: 'bar'}},
467+
],
468+
[
469+
Object.freeze({
470+
get frozenGetter() {
471+
return {};
472+
},
473+
}),
474+
{frozenGetter: {foo: 'bar'}},
475+
],
476+
[
477+
{
478+
// eslint-disable-next-line accessor-pairs
479+
set setter(value) {
480+
throw new Error('noo');
481+
},
482+
},
483+
{setter: {foo: 'bar'}},
484+
],
485+
[
486+
Object.freeze({
487+
// eslint-disable-next-line accessor-pairs
488+
set frozenSetter(value) {
489+
throw new Error('noo');
490+
},
491+
}),
492+
{frozenSetter: {foo: 'bar'}},
493+
],
438494
['banana', 'apple'],
439495
['1\u{00A0}234,57\u{00A0}$', '1 234,57 $'], // issues/6881
440496
[

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@ export default class Replaceable {
3636
cb(value, key, this.object);
3737
});
3838
Object.getOwnPropertySymbols(this.object).forEach(key => {
39-
const descriptor = Object.getOwnPropertyDescriptor(this.object, key);
40-
if ((descriptor as PropertyDescriptor).enumerable) {
41-
cb(this.object[key], key, this.object);
42-
}
39+
cb(this.object[key], key, this.object);
4340
});
4441
} else {
4542
this.object.forEach(cb);

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

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -104,28 +104,17 @@ describe('Replaceable', () => {
104104
const replaceable = new Replaceable(object);
105105
const cb = jest.fn();
106106
replaceable.forEach(cb);
107-
expect(cb.mock.calls.length).toBe(3);
107+
expect(cb).toHaveBeenCalledTimes(3);
108108
expect(cb.mock.calls[0]).toEqual([1, 'a', object]);
109109
expect(cb.mock.calls[1]).toEqual([2, 'b', object]);
110110
expect(cb.mock.calls[2]).toEqual([3, symbolKey, object]);
111111
});
112112

113-
test('object forEach do not iterate none enumerable symbol key', () => {
114-
const symbolKey = Symbol('jest');
115-
const object = {a: 1, b: 2};
116-
Object.defineProperty(object, symbolKey, {enumerable: false});
117-
const replaceable = new Replaceable(object);
118-
const cb = jest.fn();
119-
replaceable.forEach(cb);
120-
expect(cb.mock.calls.length).toBe(2);
121-
expect(cb.mock.calls[0]).toEqual([1, 'a', object]);
122-
expect(cb.mock.calls[1]).toEqual([2, 'b', object]);
123-
});
124-
125113
test('array forEach', () => {
126114
const replaceable = new Replaceable([1, 2, 3]);
127115
const cb = jest.fn();
128116
replaceable.forEach(cb);
117+
expect(cb).toHaveBeenCalledTimes(3);
129118
expect(cb.mock.calls[0]).toEqual([1, 0, [1, 2, 3]]);
130119
expect(cb.mock.calls[1]).toEqual([2, 1, [1, 2, 3]]);
131120
expect(cb.mock.calls[2]).toEqual([3, 2, [1, 2, 3]]);
@@ -139,6 +128,7 @@ describe('Replaceable', () => {
139128
const replaceable = new Replaceable(map);
140129
const cb = jest.fn();
141130
replaceable.forEach(cb);
131+
expect(cb).toHaveBeenCalledTimes(2);
142132
expect(cb.mock.calls[0]).toEqual([1, 'a', map]);
143133
expect(cb.mock.calls[1]).toEqual([2, 'b', map]);
144134
});

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,36 @@ test('returns the same value for primitive or function values', () => {
2020
expect(deepCyclicCopyReplaceable(fn)).toBe(fn);
2121
});
2222

23-
test('does not execute getters/setters, but copies them', () => {
24-
const fn = jest.fn();
23+
test('convert accessor descriptor into value descriptor', () => {
2524
const obj = {
26-
// @ts-ignore
25+
set foo(_) {},
2726
get foo() {
28-
fn();
27+
return 'bar';
2928
},
3029
};
30+
expect(Object.getOwnPropertyDescriptor(obj, 'foo')).toEqual({
31+
configurable: true,
32+
enumerable: true,
33+
get: expect.any(Function),
34+
set: expect.any(Function),
35+
});
3136
const copy = deepCyclicCopyReplaceable(obj);
3237

33-
expect(Object.getOwnPropertyDescriptor(copy, 'foo')).toBeDefined();
34-
expect(fn).not.toBeCalled();
38+
expect(Object.getOwnPropertyDescriptor(copy, 'foo')).toEqual({
39+
configurable: true,
40+
enumerable: true,
41+
value: 'bar',
42+
writable: true,
43+
});
44+
});
45+
46+
test('skips non-enumerables', () => {
47+
const obj = {};
48+
Object.defineProperty(obj, 'foo', {enumerable: false, value: 'bar'});
49+
50+
const copy = deepCyclicCopyReplaceable(obj);
51+
52+
expect(Object.getOwnPropertyDescriptors(copy)).toEqual({});
3553
});
3654

3755
test('copies symbols', () => {

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,28 @@ export default function deepCyclicCopyReplaceable<T>(
4949

5050
function deepCyclicCopyObject<T>(object: T, cycles: WeakMap<any, any>): T {
5151
const newObject = Object.create(Object.getPrototypeOf(object));
52-
const descriptors = Object.getOwnPropertyDescriptors(object);
52+
const descriptors: {
53+
[x: string]: PropertyDescriptor;
54+
} = Object.getOwnPropertyDescriptors(object);
5355

5456
cycles.set(object, newObject);
5557

5658
Object.keys(descriptors).forEach(key => {
57-
const descriptor = descriptors[key];
58-
if (typeof descriptor.value !== 'undefined') {
59-
descriptor.value = deepCyclicCopyReplaceable(descriptor.value, cycles);
59+
if (descriptors[key].enumerable) {
60+
descriptors[key] = {
61+
configurable: true,
62+
enumerable: true,
63+
value: deepCyclicCopyReplaceable(
64+
// this accesses the value or getter, depending. We just care about the value anyways, and this allows us to not mess with accessors
65+
// it has the side effect of invoking the getter here though, rather than copying it over
66+
(object as Record<string, unknown>)[key],
67+
cycles,
68+
),
69+
writable: true,
70+
};
71+
} else {
72+
delete descriptors[key];
6073
}
61-
62-
if (!('set' in descriptor)) {
63-
descriptor.writable = true;
64-
}
65-
66-
descriptor.configurable = true;
6774
});
6875

6976
return Object.defineProperties(newObject, descriptors);

0 commit comments

Comments
 (0)