From dac2fdb6228acee55ef7180da5697358c83a8a3a Mon Sep 17 00:00:00 2001 From: Lucas Fernandes da Costa Date: Wed, 17 Jul 2019 21:20:10 +0100 Subject: [PATCH] fix: handling circular references properly in getObjectSubset (#8663) --- packages/expect/src/__tests__/utils.test.js | 68 +++++++++++++++++++++ packages/expect/src/utils.ts | 32 +++++----- 2 files changed, 86 insertions(+), 14 deletions(-) diff --git a/packages/expect/src/__tests__/utils.test.js b/packages/expect/src/__tests__/utils.test.js index f12eb0e667f6..b20b3db68700 100644 --- a/packages/expect/src/__tests__/utils.test.js +++ b/packages/expect/src/__tests__/utils.test.js @@ -164,6 +164,74 @@ describe('getObjectSubset()', () => { }, ); }); + + describe('calculating subsets of objects with circular references', () => { + test('simple circular references', () => { + const nonCircularObj = {a: 'world', b: 'something'}; + + const circularObjA = {a: 'hello'}; + circularObjA.ref = circularObjA; + + const circularObjB = {a: 'world'}; + circularObjB.ref = circularObjB; + + const primitiveInsteadOfRef = {b: 'something'}; + primitiveInsteadOfRef.ref = 'not a ref'; + + const nonCircularRef = {b: 'something'}; + nonCircularRef.ref = {}; + + expect(getObjectSubset(circularObjA, nonCircularObj)).toEqual({ + a: 'hello', + }); + expect(getObjectSubset(nonCircularObj, circularObjA)).toEqual({ + a: 'world', + }); + + expect(getObjectSubset(circularObjB, circularObjA)).toEqual(circularObjB); + + expect(getObjectSubset(primitiveInsteadOfRef, circularObjA)).toEqual({ + ref: 'not a ref', + }); + expect(getObjectSubset(nonCircularRef, circularObjA)).toEqual({ + ref: {}, + }); + }); + + test('transitive circular references', () => { + const nonCircularObj = {a: 'world', b: 'something'}; + + const transitiveCircularObjA = {a: 'hello'}; + transitiveCircularObjA.nestedObj = {parentObj: transitiveCircularObjA}; + + const transitiveCircularObjB = {a: 'world'}; + transitiveCircularObjB.nestedObj = {parentObj: transitiveCircularObjB}; + + const primitiveInsteadOfRef = {}; + primitiveInsteadOfRef.nestedObj = {otherProp: 'not the parent ref'}; + + const nonCircularRef = {}; + nonCircularRef.nestedObj = {otherProp: {}}; + + expect(getObjectSubset(transitiveCircularObjA, nonCircularObj)).toEqual({ + a: 'hello', + }); + expect(getObjectSubset(nonCircularObj, transitiveCircularObjA)).toEqual({ + a: 'world', + }); + + expect( + getObjectSubset(transitiveCircularObjB, transitiveCircularObjA), + ).toEqual(transitiveCircularObjB); + + expect( + getObjectSubset(primitiveInsteadOfRef, transitiveCircularObjA), + ).toEqual({nestedObj: {otherProp: 'not the parent ref'}}); + expect(getObjectSubset(nonCircularRef, transitiveCircularObjA)).toEqual({ + nestedObj: {otherProp: {}}, + }); + }); + }); }); describe('emptyObject()', () => { diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 8b76bde631d3..6e77197bf219 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -104,7 +104,11 @@ export const getPath = ( // Strip properties from object that are not present in the subset. Useful for // printing the diff for toMatchObject() without adding unrelated noise. -export const getObjectSubset = (object: any, subset: any): any => { +export const getObjectSubset = ( + object: any, + subset: any, + seenReferences: WeakMap = new WeakMap(), +): any => { if (Array.isArray(object)) { if (Array.isArray(subset) && subset.length === object.length) { return subset.map((sub: any, i: number) => @@ -113,18 +117,17 @@ export const getObjectSubset = (object: any, subset: any): any => { } } else if (object instanceof Date) { return object; - } else if ( - typeof object === 'object' && - object !== null && - typeof subset === 'object' && - subset !== null - ) { + } else if (isObject(object) && isObject(subset)) { const trimmed: any = {}; - Object.keys(subset) - .filter(key => hasOwnProperty(object, key)) - .forEach( - key => (trimmed[key] = getObjectSubset(object[key], subset[key])), - ); + seenReferences.set(object, trimmed); + + Object.keys(object) + .filter(key => hasOwnProperty(subset, key)) + .forEach(key => { + trimmed[key] = seenReferences.has(object[key]) + ? seenReferences.get(object[key]) + : getObjectSubset(object[key], subset[key], seenReferences); + }); if (Object.keys(trimmed).length > 0) { return trimmed; @@ -257,9 +260,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);