diff --git a/CHANGELOG.md b/CHANGELOG.md index 807bd102a36e..b338681ef535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ - `[@jest/expect-utils]` [**BREAKING**] exclude non-enumerable in object matching ([#14670](https://github.com/jestjs/jest/pull/14670)) - `[@jest/expect-utils]` Fix comparison of `URL` ([#14672](https://github.com/jestjs/jest/pull/14672)) - `[@jest/expect-utils]` Check `Symbol` properties in equality ([#14688](https://github.com/jestjs/jest/pull/14688)) +- `[@jest/expect-utils]` Catch circular references within arrays when matching objects ([#14894](https://github.com/jestjs/jest/pull/14894)) - `[jest-leak-detector]` Make leak-detector more aggressive when running GC ([#14526](https://github.com/jestjs/jest/pull/14526)) - `[jest-runtime]` Properly handle re-exported native modules in ESM via CJS ([#14589](https://github.com/jestjs/jest/pull/14589)) - `[jest-util]` Make sure `isInteractive` works in a browser ([#14552](https://github.com/jestjs/jest/pull/14552)) diff --git a/e2e/__tests__/circularRefInBuiltInObj.test.ts b/e2e/__tests__/circularRefInBuiltInObj.test.ts new file mode 100644 index 000000000000..9a367ffdc1bf --- /dev/null +++ b/e2e/__tests__/circularRefInBuiltInObj.test.ts @@ -0,0 +1,89 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +describe('matches circular references nested in:', () => { + interface CircularObj { + ref: unknown; + [prop: string]: unknown; + } + + test('arrays', () => { + type CircularArray = CircularObj & {ref: Array}; + + const a: CircularArray = {c: 1, ref: [1]}; + const b: CircularArray = {c: 1, ref: [1]}; + + a.ref.push(a); + b.ref.push(b); + expect(a).toMatchObject(b); + + b.ref = []; + expect(a).not.toMatchObject(b); + + b.ref = [1]; + expect(a).not.toMatchObject(b); + }); + + test('deeply nested array properties', () => { + type DeepCircularArray = CircularObj & {ref: {inner: Array}}; + const a: DeepCircularArray = { + c: 1, + ref: { + inner: [1], + }, + }; + const b: DeepCircularArray = { + c: 1, + ref: { + inner: [1], + }, + }; + a.ref.inner.push(a); + b.ref.inner.push(b); + expect(a).toMatchObject(b); + + b.ref.inner = []; + expect(a).not.toMatchObject(b); + + b.ref.inner = [1]; + expect(a).not.toMatchObject(b); + }); + + test('sets', () => { + type CircularSet = CircularObj & {ref: Set}; + + const a: CircularSet = {c: 1, ref: new Set()}; + const b: CircularSet = {c: 1, ref: new Set()}; + + a.ref.add(a); + b.ref.add(b); + expect(a).toMatchObject(b); + + b.ref.clear(); + expect(a).not.toMatchObject(b); + + b.ref.add(1); + expect(a).not.toMatchObject(b); + }); + + test('maps', () => { + type CircularMap = CircularObj & {ref: Map}; + + const a: CircularMap = {c: 1, ref: new Map()}; + const b: CircularMap = {c: 1, ref: new Map()}; + + a.ref.set('innerRef', a); + b.ref.set('innerRef', b); + expect(a).toMatchObject(b); + + b.ref.clear(); + expect(a).not.toMatchObject(b); + + b.ref.set('innerRef', 1); + expect(a).not.toMatchObject(b); + }); +}); diff --git a/packages/expect-utils/src/utils.ts b/packages/expect-utils/src/utils.ts index 4872e4a5c7b5..2af87aa95dcd 100644 --- a/packages/expect-utils/src/utils.ts +++ b/packages/expect-utils/src/utils.ts @@ -355,12 +355,14 @@ export const subsetEquality = ( return undefined; } - return getObjectKeys(subset).every(key => { + if (seenReferences.has(subset)) return undefined; + seenReferences.set(subset, true); + + const matchResult = getObjectKeys(subset).every(key => { if (isObjectWithKeys(subset[key])) { if (seenReferences.has(subset[key])) { return equals(object[key], subset[key], filteredCustomTesters); } - seenReferences.set(subset[key], true); } const result = object != null && @@ -377,6 +379,8 @@ export const subsetEquality = ( seenReferences.delete(subset[key]); return result; }); + seenReferences.delete(subset); + return matchResult; }; return subsetEqualityWithContext()(object, subset);