From 1f7bf0a3c08779e6edc20d27bcb52614f866a738 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) --- .../__snapshots__/matchers.test.js.snap | 28 +++++++- packages/expect/src/__tests__/utils.test.js | 68 +++++++++++++++++++ packages/expect/src/utils.ts | 32 +++++---- 3 files changed, 112 insertions(+), 16 deletions(-) diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index 3bebdcd75fa7..e50e796cb05f 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -4092,7 +4092,18 @@ Expected: not Set {2, 1} Received: Set {1, 2}" `; -exports[`toMatchObject() circular references simple circular references {pass: false} expect({"a": "hello", "ref": [Circular]}).toMatchObject({"a": "world", "ref": [Circular]}) 1`] = `"Maximum call stack size exceeded"`; +exports[`toMatchObject() circular references simple circular references {pass: false} expect({"a": "hello", "ref": [Circular]}).toMatchObject({"a": "world", "ref": [Circular]}) 1`] = ` +"expect(received).toMatchObject(expected) + +- Expected ++ Received + + Object { +- \\"a\\": \\"world\\", ++ \\"a\\": \\"hello\\", + \\"ref\\": [Circular], + }" +`; exports[`toMatchObject() circular references simple circular references {pass: false} expect({"ref": "not a ref"}).toMatchObject({"a": "hello", "ref": [Circular]}) 1`] = ` "expect(received).toMatchObject(expected) @@ -4133,7 +4144,20 @@ Expected: not {} Received: {\\"a\\": \\"hello\\", \\"ref\\": [Circular]}" `; -exports[`toMatchObject() circular references transitive circular references {pass: false} expect({"a": "world", "nestedObj": {"parentObj": [Circular]}}).toMatchObject({"a": "hello", "nestedObj": {"parentObj": [Circular]}}) 1`] = `"Maximum call stack size exceeded"`; +exports[`toMatchObject() circular references transitive circular references {pass: false} expect({"a": "world", "nestedObj": {"parentObj": [Circular]}}).toMatchObject({"a": "hello", "nestedObj": {"parentObj": [Circular]}}) 1`] = ` +"expect(received).toMatchObject(expected) + +- Expected ++ Received + + Object { +- \\"a\\": \\"hello\\", ++ \\"a\\": \\"world\\", + \\"nestedObj\\": Object { + \\"parentObj\\": [Circular], + }, + }" +`; exports[`toMatchObject() circular references transitive circular references {pass: false} expect({"nestedObj": {"parentObj": "not the parent ref"}}).toMatchObject({"a": "hello", "nestedObj": {"parentObj": [Circular]}}) 1`] = ` "expect(received).toMatchObject(expected) 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);