From a6d790bfce06093174a5992373a5a9f14811575a Mon Sep 17 00:00:00 2001 From: KhaledElmorsy Date: Sun, 11 Feb 2024 18:34:42 +0200 Subject: [PATCH 1/4] Fix circular references not being caught when inside arrays --- e2e/__tests__/circularRefInBuiltInObj.test.ts | 89 +++++++++++++++++++ packages/expect-utils/src/utils.ts | 8 +- 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 e2e/__tests__/circularRefInBuiltInObj.test.ts 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 62c3085a56fa..ad88675754c3 100644 --- a/packages/expect-utils/src/utils.ts +++ b/packages/expect-utils/src/utils.ts @@ -341,12 +341,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 && @@ -363,6 +365,8 @@ export const subsetEquality = ( seenReferences.delete(subset[key]); return result; }); + seenReferences.delete(subset); + return matchResult; }; return subsetEqualityWithContext()(object, subset); From 3b6a991c75311f002a87f40efa6f35097e890e49 Mon Sep 17 00:00:00 2001 From: KhaledElmorsy Date: Sun, 11 Feb 2024 19:08:34 +0200 Subject: [PATCH 2/4] Add changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc492183d40..2cc355ffdcc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ - `[jest-core, jest-circus, jest-reporter, jest-runner]` Added support for reporting about start individual test cases using jest-circus ([#14174](https://github.com/jestjs/jest/pull/14174)) ### Fixes - +- `[expect-utils]` Catch circular references within arrays when matching objects ([#14894](https://github.com/jestjs/jest/pull/14894)) - `[jest-circus]` Prevent false test failures caused by promise rejections handled asynchronously ([#14110](https://github.com/jestjs/jest/pull/14110)) - `[jest-config]` Handle frozen config object ([#14054](https://github.com/facebook/jest/pull/14054)) - `[jest-config]` Allow `coverageDirectory` and `collectCoverageFrom` in project config ([#14180](https://github.com/jestjs/jest/pull/14180)) From cc17320d7a102e309f118281ccf74dd8e27a1dcf Mon Sep 17 00:00:00 2001 From: KhaledElmorsy Date: Sun, 11 Feb 2024 19:45:09 +0200 Subject: [PATCH 3/4] lint --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc355ffdcc0..58ed0ae08bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - `[jest-core, jest-circus, jest-reporter, jest-runner]` Added support for reporting about start individual test cases using jest-circus ([#14174](https://github.com/jestjs/jest/pull/14174)) ### Fixes + - `[expect-utils]` Catch circular references within arrays when matching objects ([#14894](https://github.com/jestjs/jest/pull/14894)) - `[jest-circus]` Prevent false test failures caused by promise rejections handled asynchronously ([#14110](https://github.com/jestjs/jest/pull/14110)) - `[jest-config]` Handle frozen config object ([#14054](https://github.com/facebook/jest/pull/14054)) From 054dbc6e567fad8dc8aefe9e5f1c6cb084a3f3ee Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 16 Mar 2024 09:28:48 +0100 Subject: [PATCH 4/4] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ac9d6233ed..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)) @@ -156,7 +157,6 @@ ### Fixes -- `[expect-utils]` Catch circular references within arrays when matching objects ([#14894](https://github.com/jestjs/jest/pull/14894)) - `[jest-circus]` Prevent false test failures caused by promise rejections handled asynchronously ([#14110](https://github.com/jestjs/jest/pull/14110)) - `[jest-config]` Handle frozen config object ([#14054](https://github.com/facebook/jest/pull/14054)) - `[jest-config]` Allow `coverageDirectory` and `collectCoverageFrom` in project config ([#14180](https://github.com/jestjs/jest/pull/14180))