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 a97950f
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 16 deletions.
28 changes: 26 additions & 2 deletions packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap
Expand Up @@ -4092,7 +4092,18 @@ Expected: not <green>Set {2, 1}</>
Received: <red>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`] = `
"<dim>expect(</><red>received</><dim>).</>toMatchObject<dim>(</><green>expected</><dim>)</>

<green>- Expected</>
<red>+ Received</>

<dim> Object {</>
<green>- \\"a\\": \\"world\\",</>
<red>+ \\"a\\": \\"hello\\",</>
<dim> \\"ref\\": [Circular],</>
<dim> }</>"
`;

exports[`toMatchObject() circular references simple circular references {pass: false} expect({"ref": "not a ref"}).toMatchObject({"a": "hello", "ref": [Circular]}) 1`] = `
"<dim>expect(</><red>received</><dim>).</>toMatchObject<dim>(</><green>expected</><dim>)</>
Expand Down Expand Up @@ -4133,7 +4144,20 @@ Expected: not <green>{}</>
Received: <red>{\\"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`] = `
"<dim>expect(</><red>received</><dim>).</>toMatchObject<dim>(</><green>expected</><dim>)</>

<green>- Expected</>
<red>+ Received</>

<dim> Object {</>
<green>- \\"a\\": \\"hello\\",</>
<red>+ \\"a\\": \\"world\\",</>
<dim> \\"nestedObj\\": Object {</>
<dim> \\"parentObj\\": [Circular],</>
<dim> },</>
<dim> }</>"
`;

exports[`toMatchObject() circular references transitive circular references {pass: false} expect({"nestedObj": {"parentObj": "not the parent ref"}}).toMatchObject({"a": "hello", "nestedObj": {"parentObj": [Circular]}}) 1`] = `
"<dim>expect(</><red>received</><dim>).</>toMatchObject<dim>(</><green>expected</><dim>)</>
Expand Down
68 changes: 68 additions & 0 deletions packages/expect/src/__tests__/utils.test.js
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
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 a97950f

Please sign in to comment.