Skip to content

Commit

Permalink
fix: handling circular references properly in getObjectSubset (#8663)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfcosta committed Jul 18, 2019
1 parent 2c6eb48 commit dac2fdb
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 14 deletions.
68 changes: 68 additions & 0 deletions packages/expect/src/__tests__/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down
32 changes: 18 additions & 14 deletions packages/expect/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<object, boolean> = new WeakMap(),
): any => {
if (Array.isArray(object)) {
if (Array.isArray(subset) && subset.length === object.length) {
return subset.map((sub: any, i: number) =>
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit dac2fdb

Please sign in to comment.