Skip to content

Commit c872aa5

Browse files
Fix(expect-utils): Circular references not being caught when inside arrays (#14894)
1 parent 89d6b08 commit c872aa5

File tree

3 files changed

+96
-2
lines changed

3 files changed

+96
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
- `[@jest/expect-utils]` [**BREAKING**] exclude non-enumerable in object matching ([#14670](https://github.com/jestjs/jest/pull/14670))
4747
- `[@jest/expect-utils]` Fix comparison of `URL` ([#14672](https://github.com/jestjs/jest/pull/14672))
4848
- `[@jest/expect-utils]` Check `Symbol` properties in equality ([#14688](https://github.com/jestjs/jest/pull/14688))
49+
- `[@jest/expect-utils]` Catch circular references within arrays when matching objects ([#14894](https://github.com/jestjs/jest/pull/14894))
4950
- `[jest-leak-detector]` Make leak-detector more aggressive when running GC ([#14526](https://github.com/jestjs/jest/pull/14526))
5051
- `[jest-runtime]` Properly handle re-exported native modules in ESM via CJS ([#14589](https://github.com/jestjs/jest/pull/14589))
5152
- `[jest-util]` Make sure `isInteractive` works in a browser ([#14552](https://github.com/jestjs/jest/pull/14552))
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
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+
describe('matches circular references nested in:', () => {
9+
interface CircularObj {
10+
ref: unknown;
11+
[prop: string]: unknown;
12+
}
13+
14+
test('arrays', () => {
15+
type CircularArray = CircularObj & {ref: Array<unknown>};
16+
17+
const a: CircularArray = {c: 1, ref: [1]};
18+
const b: CircularArray = {c: 1, ref: [1]};
19+
20+
a.ref.push(a);
21+
b.ref.push(b);
22+
expect(a).toMatchObject(b);
23+
24+
b.ref = [];
25+
expect(a).not.toMatchObject(b);
26+
27+
b.ref = [1];
28+
expect(a).not.toMatchObject(b);
29+
});
30+
31+
test('deeply nested array properties', () => {
32+
type DeepCircularArray = CircularObj & {ref: {inner: Array<unknown>}};
33+
const a: DeepCircularArray = {
34+
c: 1,
35+
ref: {
36+
inner: [1],
37+
},
38+
};
39+
const b: DeepCircularArray = {
40+
c: 1,
41+
ref: {
42+
inner: [1],
43+
},
44+
};
45+
a.ref.inner.push(a);
46+
b.ref.inner.push(b);
47+
expect(a).toMatchObject(b);
48+
49+
b.ref.inner = [];
50+
expect(a).not.toMatchObject(b);
51+
52+
b.ref.inner = [1];
53+
expect(a).not.toMatchObject(b);
54+
});
55+
56+
test('sets', () => {
57+
type CircularSet = CircularObj & {ref: Set<unknown>};
58+
59+
const a: CircularSet = {c: 1, ref: new Set()};
60+
const b: CircularSet = {c: 1, ref: new Set()};
61+
62+
a.ref.add(a);
63+
b.ref.add(b);
64+
expect(a).toMatchObject(b);
65+
66+
b.ref.clear();
67+
expect(a).not.toMatchObject(b);
68+
69+
b.ref.add(1);
70+
expect(a).not.toMatchObject(b);
71+
});
72+
73+
test('maps', () => {
74+
type CircularMap = CircularObj & {ref: Map<string, unknown>};
75+
76+
const a: CircularMap = {c: 1, ref: new Map()};
77+
const b: CircularMap = {c: 1, ref: new Map()};
78+
79+
a.ref.set('innerRef', a);
80+
b.ref.set('innerRef', b);
81+
expect(a).toMatchObject(b);
82+
83+
b.ref.clear();
84+
expect(a).not.toMatchObject(b);
85+
86+
b.ref.set('innerRef', 1);
87+
expect(a).not.toMatchObject(b);
88+
});
89+
});

packages/expect-utils/src/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,14 @@ export const subsetEquality = (
355355
return undefined;
356356
}
357357

358-
return getObjectKeys(subset).every(key => {
358+
if (seenReferences.has(subset)) return undefined;
359+
seenReferences.set(subset, true);
360+
361+
const matchResult = getObjectKeys(subset).every(key => {
359362
if (isObjectWithKeys(subset[key])) {
360363
if (seenReferences.has(subset[key])) {
361364
return equals(object[key], subset[key], filteredCustomTesters);
362365
}
363-
seenReferences.set(subset[key], true);
364366
}
365367
const result =
366368
object != null &&
@@ -377,6 +379,8 @@ export const subsetEquality = (
377379
seenReferences.delete(subset[key]);
378380
return result;
379381
});
382+
seenReferences.delete(subset);
383+
return matchResult;
380384
};
381385

382386
return subsetEqualityWithContext()(object, subset);

0 commit comments

Comments
 (0)