diff --git a/CHANGELOG.md b/CHANGELOG.md index 053ebb3bcbe0..497660acc406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ - `[jest-utils]` Allow querying process.domain ([#9136](https://github.com/facebook/jest/pull/9136)) - `[pretty-format]` Correctly detect memoized elements ([#9196](https://github.com/facebook/jest/pull/9196)) - `[jest-fake-timers]` Support `util.promisify` on `setTimeout` ([#9180](https://github.com/facebook/jest/pull/9180)) +- `[expect]` Fix subsetEquality: fix circular reference handling logic ([#9322](https://github.com/facebook/jest/pull/9322)) ### Chore & Maintenance diff --git a/packages/expect/src/__tests__/utils.test.js b/packages/expect/src/__tests__/utils.test.js index a8c539eb90dc..0eb4bdca4659 100644 --- a/packages/expect/src/__tests__/utils.test.js +++ b/packages/expect/src/__tests__/utils.test.js @@ -333,6 +333,19 @@ describe('subsetEquality()', () => { expect(subsetEquality(primitiveInsteadOfRef, circularObjA1)).toBe(false); }); + test('referenced object on same level should not regarded as circular reference', () => { + const referencedObj = {abc: 'def'}; + const object = { + a: {abc: 'def'}, + b: {abc: 'def', zzz: 'zzz'}, + }; + const thisIsNotCircular = { + a: referencedObj, + b: referencedObj, + }; + expect(subsetEquality(object, thisIsNotCircular)).toBeTruthy(); + }); + test('transitive circular references', () => { const transitiveCircularObjA1 = {a: 'hello'}; transitiveCircularObjA1.nestedObj = {parentObj: transitiveCircularObjA1}; diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index a642e56b22ee..d4763e9f4056 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -166,7 +166,6 @@ export const iterableEquality = ( if (a.constructor !== b.constructor) { return false; } - let length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of @@ -290,20 +289,25 @@ export const subsetEquality = ( return Object.keys(subset).every(key => { if (isObjectWithKeys(subset[key])) { - if (seenReferences.get(subset[key])) { + if (seenReferences.has(subset[key])) { return equals(object[key], subset[key], [iterableEquality]); } seenReferences.set(subset[key], true); } - - return ( + const result = object != null && hasOwnProperty(object, key) && equals(object[key], subset[key], [ iterableEquality, subsetEqualityWithContext(seenReferences), - ]) - ); + ]); + // The main goal of using seenReference is to avoid circular node on tree. + // It will only happen within a parent and its child, not a node and nodes next to it (same level) + // We should keep the reference for a parent and its child only + // Thus we should delete the reference immediately so that it doesn't interfere + // other nodes within the same level on tree. + seenReferences.delete(subset[key]); + return result; }); };