Skip to content

Commit

Permalink
implement merge for nested objects and unit tests, see phetsims/phet-…
Browse files Browse the repository at this point in the history
  • Loading branch information
mbarlow12 committed Feb 19, 2019
1 parent d2cd9a6 commit 01b688c
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 0 deletions.
30 changes: 30 additions & 0 deletions js/merge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2019, University of Colorado Boulder


define( require => {
'use strict';

// modules
const phetCore = require( 'PHET_CORE/phetCore' );

function merge( obj ) {
_.each( Array.prototype.slice.call( arguments, 1 ), function( source ) {
if ( source ) {
for ( var prop in source ) {
if ( prop.includes( 'Options' ) && obj.hasOwnProperty( prop ) ) {
// ensure that the ...Options property is a POJSO
assert && assert( Object.getPrototypeOf( source[ prop ] ) === Object.prototype, 'merge can only take place between Objects declared by {}' );
assert && assert( !( Object.getOwnPropertyDescriptor( source, prop ).hasOwnProperty( 'get' ) ), 'cannot use merge with a getter' );
Object.defineProperty( obj, prop, merge( obj[ prop ], source[ prop ] ) );
}
else {
Object.defineProperty( obj, prop, Object.getOwnPropertyDescriptor( source, prop ) );
}
}
}
} );
return obj;
}

return phetCore.register( 'merge', merge );
} );
251 changes: 251 additions & 0 deletions js/mergeTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// Copyright 2019, University of Colorado Boulder

define( require => {
'use strict';

// modules
var merge = require( 'PHET_CORE/merge' );

QUnit.module( 'merge' );

// test proper merger for 2 objects
QUnit.test( 'merge two objects', function( assert ) {
var original = {
prop1: 'value1',
prop2: 'value2',
subcomponentOptions: {
subProp1: 'subValue1',
subProp2: 'subValue2'
},
subcomponentOptions2: {
subSubcomponentOptions: {
subSubProp1: 'subSubValue1'
}
},
prop3: 'value3'
};

var merge1 = {
subcomponentOptions: {
subProp1: 'subvalue1 changed',
subProp3: 'new subvalue'
},
subcomponentOptions2: {
subSubcomponentOptions: {
subSubProp1: 'all gone now',
test: 'this is here too'
}
},
prop3: 'new value3',
prop4: 'value4'
};
var preMergeSourceCopy = Object.assign( {}, merge1 );
var merged = merge( original, merge1 );

assert.equal( merged.prop1, 'value1', 'merge should not alter target keys that aren\'t in the source' );
assert.equal( merged.prop4, 'value4', 'merge should not alter source keys that aren\'t in the target');

var shouldBe = {
subProp1: 'subvalue1 changed',
subProp2: 'subValue2',
subProp3: 'new subvalue'
};
assert.deepEqual( merged.subcomponentOptions, shouldBe, 'merge should combine singly nested objects' );

shouldBe = {
prop1: 'value1',
prop2: 'value2',
subcomponentOptions: {
subProp1: 'subvalue1 changed',
subProp3: 'new subvalue',
subProp2: 'subValue2'
},
subcomponentOptions2: {
subSubcomponentOptions: {
subSubProp1: 'all gone now',
test: 'this is here too'
}
},
prop3: 'new value3',
prop4: 'value4'
};
assert.deepEqual( merged, shouldBe, 'merge should combine arbitrarily nested objects' );
assert.deepEqual( merge1, preMergeSourceCopy, 'merge should not alter sources' );
} );

// test multiple objects
QUnit.test( 'test multiple objects', function( assert ) {
var original = {
prop1: 'value1',
prop2: 'value2',
subcomponentOptions: {
subProp1: 'subValue1',
subProp2: 'subValue2'
},
subcomponentOptions2: {
subSubcomponentOptions: {
subSubProp1: 'subSubValue1'
}
},
prop3: 'value3'
};

var merge1 = {
subcomponentOptions: {
subProp1: 'subvalue1 changed',
subProp3: 'new subvalue',
except: 'me'
},
subcomponentOptions2: {
subSubcomponentOptions: {
subSubProp1: 'all gone now',
test: 'this is here too'
}
},
prop3: 'new value3',
prop4: 'value4'
};

var merge2 = {
prop5: 'value5',
subcomponentOptions: {
subProp1: 'everything',
subProp2: 'here is',
subProp3: 'from',
subProp4: 'merge2'
}
};

var merge3 = {
prop6: 'value6',
prop5: 'value5 from merge3',
subcomponentOptions2: {
test2: ['test2', 'test3'],
subSubcomponentOptions: {
test: 'test form merge3',
subSubProp1: 'subSub from merge3'
}
}
};
var merge1Copy = Object.assign( {}, merge1 );
var merge2Copy = Object.assign( {}, merge2 );
var merge3Copy = Object.assign( {}, merge3 );


var merged = merge( original, merge1, merge2, merge3 );

var expected = {
prop1: 'value1',
prop2: 'value2',
subcomponentOptions: {
subProp1: 'everything',
subProp2: 'here is',
subProp3: 'from',
subProp4: 'merge2',
except: 'me'
},
subcomponentOptions2: {
test2: ['test2', 'test3'],
subSubcomponentOptions: {
test: 'test form merge3',
subSubProp1: 'subSub from merge3'
}
},
prop3: 'new value3',
prop4: 'value4',
prop5: 'value5 from merge3',
prop6: 'value6'
};
assert.notEqual( merged, expected, 'sanity check: ensure merged and expected objects are not the same reference' );
assert.deepEqual( merged, expected, 'merge should properly combine multiple objects' );
assert.deepEqual( merge1, merge1Copy, 'merge should not alter source objects' );
assert.deepEqual( merge2, merge2Copy, 'merge should not alter source objects' );
assert.deepEqual( merge3, merge3Copy, 'merge should not alter source objects' );
} );

// check that it errors loudly if something other than an object is used
QUnit.test( 'check for proper assertion errors', function( assert ) {
var original = {
subOptions: {
test: 'val',
test2: 'val2'
}
};

function TestClass() {
this.test = 'class';
}

var merges = {
a: {
subOptions: [ 'val', 'val2' ]
},
b: {
subOptions: Object.create( { test: 'a', test1: 3 } )
},
c: {
subOptions: 'a string to test'
},
d: {
subOptions: 42
},
e: {
subOptions: function() { this.a = 42; }
},
f: {
subOptions: new TestClass()
}
};

var getterMerge = {
get subOptions() {
return {
test: 'should not work'
};
}
};

if ( window.assert ) {
assert.throws( function() { merge( original, merges.a ); }, 'merge should not allow arrays to be merged' );
assert.throws( function() { merge( original, merges.b ); }, 'merge should not allow inherited objects to be merged' );
assert.throws( function() { merge( original, merges.f ); }, 'merge should not allow instances to be merged' );
assert.throws( function() { merge( original, merges.c ); }, 'merge should not allow strings to be merged' );
assert.throws( function() { merge( original, merges.d ); }, 'merge should not allow numbers to be merged' );
assert.throws( function() { merge( original, merges.e ); }, 'merge should not allow functions to be merged' );
assert.throws( function() { merge( original, getterMerge ); }, 'merge should not work with getters' );
}
} );

QUnit.test( 'try a horribly nested case', function( assert ) {
var original = {
p1Options: { n1Options: { n2Options: { n3Options: { n4Options: { n5: 'overwrite me' } } } } },
p2Options: {
n1Options: {
p3: 'keep me'
}
}
};
var merge1 = {
p1Options: { n1Options: { n2Options: { n3Options: { n4Options: { n5: 'overwritten' } } } } },
p2Options: {
n1Options: {
p4: 'p3 kept',
n2Options: { n3Options: { n4Options: { n5Options: { n6Options: { p5: 'never make options like this' } } } } }
}
}
};

var merged = merge( original, merge1 );
var expected = {
p1Options: { n1Options: { n2Options: { n3Options: { n4Options: { n5: 'overwritten' } } } } },
p2Options: {
n1Options: {
p3: 'keep me',
p4: 'p3 kept',
n2Options: { n3Options: { n4Options: { n5Options: { n6Options: { p5: 'never make options like this' } } } } }
}
}
};
assert.deepEqual( merged, expected, 'merge should handle some deeply nested stuff' );
} );
} );
1 change: 1 addition & 0 deletions js/phet-core-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ define( function( require ) {
require( 'PHET_CORE/arrayDifferenceTests' );
require( 'PHET_CORE/interleaveTests' );
require( 'PHET_CORE/EnumerationTests' );
require( 'PHET_CORE/mergeTests' );

// Since our tests are loaded asynchronously, we must direct QUnit to begin the tests
QUnit.start();
Expand Down

0 comments on commit 01b688c

Please sign in to comment.