-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement merge for nested objects and unit tests, see phetsims/phet-…
- Loading branch information
Showing
3 changed files
with
282 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' ); | ||
} ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters