diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index 38ef42eebc21..f37d58124c86 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -4092,6 +4092,30 @@ Expected: not Set {2, 1} Received: Set {1, 2}" `; +exports[`toMatchObject() circular references simple circular references 1`] = ` +"expect(received).not.toMatchObject(expected) + +Expected: not {\\"ref\\": [Circular]}" +`; + +exports[`toMatchObject() circular references simple circular references 2`] = ` +"expect(received).not.toMatchObject(expected) + +Expected: not [[Circular]]" +`; + +exports[`toMatchObject() circular references transitive circular references 1`] = ` +"expect(received).not.toMatchObject(expected) + +Expected: not {\\"nestedObj\\": {\\"parentObj\\": [Circular]}}" +`; + +exports[`toMatchObject() circular references transitive circular references 2`] = ` +"expect(received).not.toMatchObject(expected) + +Expected: not [[[Circular]]]" +`; + exports[`toMatchObject() throws expect("44").toMatchObject({}) 1`] = ` "expect(received).toMatchObject(expected) diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index 9b5661339c0f..31c9595eb846 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -1537,6 +1537,49 @@ describe('toMatchObject()', () => { } } + describe('circular references', () => { + test('simple circular references', () => { + const circularObj = {}; + circularObj.ref = circularObj; + + jestExpect(circularObj).toMatchObject(circularObj); + expect(() => { + jestExpect(circularObj).not.toMatchObject(circularObj); + }).toThrowErrorMatchingSnapshot(); + + const circularArray = []; + circularArray.push(circularArray); + jestExpect(circularArray).toMatchObject(circularArray); + expect(() => { + jestExpect(circularArray).not.toMatchObject(circularArray); + }).toThrowErrorMatchingSnapshot(); + }); + + test('transitive circular references', () => { + const transitiveCircularObj = {}; + transitiveCircularObj.nestedObj = {parentObj: transitiveCircularObj}; + + jestExpect(transitiveCircularObj).toMatchObject(transitiveCircularObj); + expect(() => { + jestExpect(transitiveCircularObj).not.toMatchObject( + transitiveCircularObj, + ); + }).toThrowErrorMatchingSnapshot(); + + const transitiveCircularArray = []; + const refArray = [transitiveCircularArray]; + transitiveCircularArray.push(refArray); + jestExpect(transitiveCircularArray).toMatchObject( + transitiveCircularArray, + ); + expect(() => { + jestExpect(transitiveCircularArray).not.toMatchObject( + transitiveCircularArray, + ); + }).toThrowErrorMatchingSnapshot(); + }); + }); + [ [{a: 'b', c: 'd'}, {a: 'b'}], [{a: 'b', c: 'd'}, {a: 'b', c: 'd'}], diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index f8aa8aec01d3..b0e165663093 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -257,9 +257,10 @@ export const iterableEquality = ( return true; }; +const isObject = (a: any) => a !== null && typeof a === 'object'; + const isObjectWithKeys = (a: any) => - a !== null && - typeof a === 'object' && + isObject(a) && !(a instanceof Error) && !(a instanceof Array) && !(a instanceof Date); @@ -268,16 +269,38 @@ export const subsetEquality = ( object: any, subset: any, ): undefined | boolean => { - if (!isObjectWithKeys(subset)) { - return undefined; - } + const seenReferences = new WeakMap(); + + // subsetEquality needs to keep track of the references + // it has already visited to avoid infinite loops in case + // there are circular references in the subset passed to it. + const subsetEqualityWithContext = ( + seenReferences: WeakMap, + ) => (object: any, subset: any): undefined | boolean => { + if (!isObjectWithKeys(subset)) { + return undefined; + } - return Object.keys(subset).every( - key => - object != null && - hasOwnProperty(object, key) && - equals(object[key], subset[key], [iterableEquality, subsetEquality]), - ); + return Object.keys(subset).every(key => { + const target = subset[key]; + if (isObject(target)) { + console.log(typeof target, target); + if (seenReferences.get(target)) return true; + seenReferences.set(target, true); + } + + return ( + object != null && + hasOwnProperty(object, key) && + equals(object[key], target, [ + iterableEquality, + subsetEqualityWithContext(seenReferences), + ]) + ); + }); + }; + + return subsetEqualityWithContext(seenReferences)(object, subset); }; export const typeEquality = (a: any, b: any) => {