/*______________ | ______ | U I Z E J A V A S C R I P T F R A M E W O R K | / / | --------------------------------------------------- | / O / | MODULE : Uize.Test.Uize.Data.Diff Class | / / / | | / / / /| | ONLINE : http://uize.com | /____/ /__/_| | COPYRIGHT : (c)2014-2016 UIZE | /___ | LICENSE : Available under MIT License or GNU General Public License |_______________| http://uize.com/license.html */ /* Module Meta Data type: Test importance: 1 codeCompleteness: 100 docCompleteness: 100 */ /*? Introduction The =Uize.Test.Uize.Data.Diff= module defines a suite of unit tests for the =Uize.Data.Diff= module. *DEVELOPERS:* `Chris van Rensburg` */ Uize.module ({ name:'Uize.Test.Uize.Data.Diff', required:'Uize.Test', builder:function () { 'use strict'; function _addedOrModifiedValuesPropertyComparer (_object1Property,_object2Property) { return ( _object2Property && (!_object1Property || _object2Property.value !== _object1Property.value) ? _object2Property : undefined ); } function _addedPropertyComparer (_object1Property,_object2Property) { return !_object1Property && _object2Property ? {value:'added'} : undefined; } function _modifiedPropertyComparer (_object1Property,_object2Property) { return ( _object1Property && _object2Property && _object1Property.value !== _object2Property.value ? {value:'modified'} : undefined ); } function _removedPropertyComparer (_object1Property,_object2Property) { return _object1Property && !_object2Property ? {value:'removed'} : undefined; } function _unmodifiedPropertyComparer (_object1Property,_object2Property) { return ( _object1Property && _object2Property && _object1Property.value === _object2Property.value ? {value:'unchanged'} : undefined ); } return Uize.Test.resolve ({ title:'Test for Uize.Data.Diff Module', test:[ Uize.Test.requiredModulesTest ('Uize.Data.Diff'), Uize.Test.staticMethodsTest ([ ['Uize.Data.Diff.diff',[ /*** test the default property comparer ***/ ['With the default property comparer, modified properties are marked by the value "modified"', [ {foo:'bar'}, {foo:'BAR'} ], {foo:'modified'} ], ['With the default property comparer, added properties are marked by the value "added"', [ {}, {foo:'bar'} ], {foo:'added'} ], ['With the default property comparer, removed properties are marked by the value "removed"', [ {foo:'bar'}, {} ], {foo:'removed'} ], ['With the default property comparer, unmodified properties are marked by the value "unchanged"', [ {foo:'bar'}, {foo:'bar'} ], {foo:'unchanged'} ], ['An arbitrarily complex data structure can be diff\'ed', [ { prop1:'foo', prop2:'bar', prop4:{ prop1:'foo', prop2:'bar' }, prop6:{ prop1:'foo', prop2:'bar', prop3:'baz' } }, { prop1:'FOO', prop3:'bar', prop5:{ prop1:'foo', prop2:'bar' }, prop6:{ prop1:'foo', prop2:'BAR', prop4:'qux' } } ], { prop1:'modified', prop2:'removed', prop3:'added', prop4:{ prop1:'removed', prop2:'removed' }, prop5:{ prop1:'added', prop2:'added' }, prop6:{ prop1:'unchanged', prop2:'modified', prop3:'removed', prop4:'added' } } ], /*** test array diffing ***/ ['When the value of a property in both objects being compared is an array, then all the elements of the two array values will be compared', [ {array:['foo','bar']}, {array:['foo','baz','qux']} ], {array:['unchanged','modified','added']} ], ['When the value of a property is an array in the first object and missing in the second object, all the values from the array in the first object will appear to have been removed', [ {array:['foo','bar','baz','qux']}, {} ], {array:['removed','removed','removed','removed']} ], ['When the value of a property is an array in the second object and missing in the first object, all the values from the array in the second object will appear to have been added', [ {}, {array:['foo','bar','baz','qux']} ], {array:['added','added','added','added']} ], /*** test the skeleton option ***/ ['When the value false is specified for the optional skeleton option, then the returned comparison result will not contain empty nodes in which there are no property comparison values', [ { foo:{ bar:{}, baz:{ qux:[] } } }, { foo:{ BAR:{ BAZ:[] } }, QUX:{} }, {skeleton:false} ], {} ], ['When the value true is specified for the optional skeleton option, then the returned comparison result will at a minimum be a skeleton object that is the union of the structures of the two objects being compared', [ { foo:{ bar:{}, baz:{ qux:[] } } }, { foo:{ BAR:{ BAZ:[] } }, QUX:{} }, null, {skeleton:true} ], { foo:{ bar:{}, baz:{ qux:[] }, BAR:{ BAZ:[] } }, QUX:{} } ], ['When no value is specified for the optional skeleton option, it is defaulted to false and the skeleton behavior is disabled', [ { foo:{ bar:{}, baz:{ qux:[] } } }, { foo:{ BAR:{ BAZ:[] } }, QUX:{} }, null, {} ], {} ], /*** test the dual option ***/ ['When the value false is specified for the optional dual option, then the recursion is driven by the first object', [ { foo:{ bar:{0:'fee',2:'FO',3:'fum'}, BAR:['fee','fi','fo'], baz:{ qux:'nux', dux:['bux','pux'] } }, hello:5 }, { foo:{ bar:['fee','fi','fo'], BAR:{0:'fee',2:'FO',3:'fum'}, baz:'baz' }, hello:{ thebird:['is','the','word'] } }, null, {dual:false,skeleton:true} ], { foo:{ bar:{0:'removed',2:'removed',3:'removed'}, BAR:['removed','removed','removed'], baz:{ qux:'removed', dux:['removed','removed'] } }, hello:'modified' } ], ['When a non-dual diff is performed, properties in object B whose values are not arrays when the values of the corresponding properties in object A are arrays are treated as being empty arrays for the purpose of the diff recursion', [ { prop1:['foo','bar'], prop2:['foo','bar'], prop3:['foo','bar'], prop4:['foo','bar'], prop5:['foo','bar'], prop6:['foo','bar'], prop7:['foo','bar'] }, { prop1:42, prop2:false, prop3:'foo', prop4:{foo:'bar'}, prop5:null, prop6:/\d+/ }, null, {dual:false} ], { prop1:['removed','removed'], prop2:['removed','removed'], prop3:['removed','removed'], prop4:['removed','removed'], prop5:['removed','removed'], prop6:['removed','removed'], prop7:['removed','removed'] } ], ['When a non-dual diff is performed, properties in object B whose values are not plain objects when the values of the corresponding properties in object A are plain objects are treated as being empty objects for the purpose of the diff recursion', [ { prop1:{foo:'FOO',bar:'BAR'}, prop2:{foo:'FOO',bar:'BAR'}, prop3:{foo:'FOO',bar:'BAR'}, prop4:{foo:'FOO',bar:'BAR'}, prop5:{foo:'FOO',bar:'BAR'}, prop6:{foo:'FOO',bar:'BAR'}, prop7:{foo:'FOO',bar:'BAR'} }, { prop1:42, prop2:false, prop3:'foo', prop4:['foo','bar'], prop5:null, prop6:/\d+/ }, null, {dual:false} ], { prop1:{foo:'removed',bar:'removed'}, prop2:{foo:'removed',bar:'removed'}, prop3:{foo:'removed',bar:'removed'}, prop4:{foo:'removed',bar:'removed'}, prop5:{foo:'removed',bar:'removed'}, prop6:{foo:'removed',bar:'removed'}, prop7:{foo:'removed',bar:'removed'} } ], ['When performing a non-dual diff, properties in object B whose values are arrays or objects, when the values of the corresponding properties in object A are non-object types, are not recursed and are compared directly against the values in object A', [ { prop1:42, prop2:false, prop3:'foo', prop4:undefined, prop5:null, prop6:/\d+/ }, { prop1:{foo:'FOO',bar:'BAR'}, prop2:{foo:'FOO',bar:'BAR'}, prop3:{foo:'FOO',bar:'BAR'}, prop4:{foo:'FOO',bar:'BAR'}, prop5:{foo:'FOO',bar:'BAR'}, prop6:{foo:'FOO',bar:'BAR'} }, null, {dual:false} ], { prop1:'modified', prop2:'modified', prop3:'modified', prop4:'modified', prop5:'modified', prop6:'modified' } ], ['When performing a non-dual diff, properties that are present in objects in object B that are not present in the corresponding objects in object A are not iterated over', [ { foo:{foo:'FOO',bar:'BAR'} }, { foo:{foo:'FOO!',bar:'BAR',baz:'BAZ',qux:'QUX'} }, null, {dual:false} ], { foo:{foo:'modified',bar:'unchanged'} } ], ['When performing a non-dual diff, elements that are present in arrays in object B that are not present in the corresponding arrays in object A are not iterated over', [ { foo:['FOO','BAR'] }, { foo:['FOO!','BAR','BAZ','QUX'] }, null, {dual:false} ], { foo:['modified','unchanged'] } ], ['When the value true is specified explicitly for the optional dual option, then the standard dual diff is performed', [ { prop1:{foo:'FOO',bar:'BAR'}, prop2:['FOO','BAR'] }, { prop1:{foo:'FOO!',bar:'BAR',baz:'BAZ',qux:'QUX'}, prop2:['FOO!','BAR','BAZ','QUX'] }, null, {dual:true} ], { prop1:{foo:'modified',bar:'unchanged',baz:'added',qux:'added'}, prop2:['modified','unchanged','added','added'] } ], /*** test a custom added-or-modified property comparer ***/ ['With a custom added-or-modified property comparer, modified properties are marked by their new value', [ {foo:'bar'}, {foo:'BAR'}, _addedOrModifiedValuesPropertyComparer ], {foo:'BAR'} ], ['With a custom added-or-modified property comparer, added properties are marked by their new value', [ {}, {foo:'bar'}, _addedOrModifiedValuesPropertyComparer ], {foo:'bar'} ], ['With a custom added-or-modified property comparer, removed properties are omitted from the result', [ {foo:'bar'}, {}, _addedOrModifiedValuesPropertyComparer ], {} ], ['With a custom added-or-modified property comparer, unmodified properties are omitted from the result', [ {foo:'bar'}, {foo:'bar'}, _addedOrModifiedValuesPropertyComparer ], {} ], /*** test a custom added property comparer ***/ ['With a custom added property comparer, modified properties are omitted from the result', [ {foo:'bar'}, {foo:'BAR'}, _addedPropertyComparer ], {} ], ['With a custom added property comparer, added properties are marked by the value "added"', [ {}, {foo:'bar'}, _addedPropertyComparer ], {foo:'added'} ], ['With a custom added property comparer, removed properties are omitted from the result', [ {foo:'bar'}, {}, _addedPropertyComparer ], {} ], ['With a custom added property comparer, unmodified properties are omitted from the result', [ {foo:'bar'}, {foo:'bar'}, _addedPropertyComparer ], {} ], /*** test a custom modified property comparer ***/ ['With a custom modified property comparer, modified properties are marked by the value "modified"', [ {foo:'bar'}, {foo:'BAR'}, _modifiedPropertyComparer ], {foo:'modified'} ], ['With a custom modified property comparer, added properties are omitted from the result', [ {}, {foo:'bar'}, _modifiedPropertyComparer ], {} ], ['With a custom modified property comparer, removed properties are omitted from the result', [ {foo:'bar'}, {}, _modifiedPropertyComparer ], {} ], ['With a custom modified property comparer, unmodified properties are omitted from the result', [ {foo:'bar'}, {foo:'bar'}, _modifiedPropertyComparer ], {} ], /*** test a custom removed property comparer ***/ ['With a custom removed property comparer, modified properties are omitted from the result', [ {foo:'bar'}, {foo:'BAR'}, _removedPropertyComparer ], {} ], ['With a custom removed property comparer, added properties are omitted from the result', [ {}, {foo:'bar'}, _removedPropertyComparer ], {} ], ['With a custom removed property comparer, removed properties are marked by the value "removed"', [ {foo:'bar'}, {}, _removedPropertyComparer ], {foo:'removed'} ], ['With a custom removed property comparer, unmodified properties are omitted from the result', [ {foo:'bar'}, {foo:'bar'}, _removedPropertyComparer ], {} ], /*** test a custom unmodified property comparer ***/ ['With a custom unmodified property comparer, modified properties are omitted from the result', [ {foo:'bar'}, {foo:'BAR'}, _unmodifiedPropertyComparer ], {} ], ['With a custom unmodified property comparer, added properties are omitted from the result', [ {}, {foo:'bar'}, _unmodifiedPropertyComparer ], {} ], ['With a custom unmodified property comparer, removed properties are omitted from the result', [ {foo:'bar'}, {}, _unmodifiedPropertyComparer ], {} ], ['With a custom unmodified property comparer, unmodified properties are marked by the value "unchanged"', [ {foo:'bar'}, {foo:'bar'}, _unmodifiedPropertyComparer ], {foo:'unchanged'} ], /*** miscellaneous tests ***/ ['The property info arguments that are provided to the property comparer function each contain a key property which indicates the name of the current property being compared', [ { foo:'FOO', bar:{baz:'BAZ'} }, { qux:'QUX', hello:{world:'WORLD'} }, function (_object1PropertyInfo,_object2PropertyInfo) { return { value:[ _object1PropertyInfo && _object1PropertyInfo.key, _object2PropertyInfo && _object2PropertyInfo.key ] }; } ], { foo:['foo',undefined], bar:{ baz:['baz',undefined] }, qux:[undefined,'qux'], hello:{ world:[undefined,'world'] } } ], { title:'The property comparer function is passed a third argument, whose value is an array representing the path to the current property being compared', test:function () { var _comparedPropertiesPaths = []; Uize.Data.Diff.diff ( { prop1:'foo', prop2:'bar', prop4:{ prop1:'foo', prop2:'bar' }, prop6:{ prop1:'foo', prop2:'bar', prop3:'baz', prop5:['foo','bar','baz'] } }, { prop1:'FOO', prop3:'bar', prop5:{ prop1:'foo', prop2:'bar' }, prop6:{ prop1:'foo', prop2:'BAR', prop4:'qux', prop5:['foo'] }, prop7:['foo','bar'] }, function (_object1PropertyInfo,_object2PropertyInfo,_path) { _comparedPropertiesPaths.push (_path.concat ()); } ); return this.expect ( [ ['prop1'], ['prop2'], ['prop4','prop1'], ['prop4','prop2'], ['prop6','prop1'], ['prop6','prop2'], ['prop6','prop3'], ['prop6','prop5',0], ['prop6','prop5',1], ['prop6','prop5',2], ['prop6','prop4'], ['prop3'], ['prop5','prop1'], ['prop5','prop2'], ['prop7',0], ['prop7',1] ], _comparedPropertiesPaths ); } }, ['The key property in the property profile object returned by a property comparer function is used to determine the name of the property added to the diff result', [ { leafNode1:'foo', leafNode2:'bar', nonLeadNode1:{ leafNode1:'foo', leadNode2:'bar' }, nonLeafNode2:{ leafNode1:'foo', leafNode2:'bar', nonLeafNode1:{ leafNode1:'foo', leadNode2:'bar' } } }, {}, function (_propertyProfile) { _propertyProfile.key = _propertyProfile.key.toUpperCase (); return _propertyProfile; } ], { LEAFNODE1:'foo', LEAFNODE2:'bar', nonLeadNode1:{ LEAFNODE1:'foo', LEADNODE2:'bar' }, nonLeafNode2:{ LEAFNODE1:'foo', LEAFNODE2:'bar', nonLeafNode1:{ LEAFNODE1:'foo', LEADNODE2:'bar' } } } ], ['When a property in one object is a leaf node and the corresponding property in the other object is a non-leaf node, then the non-leaf node is recursed as if the leaf node was a non-leaf node', [ { foo:{hello:1,world:2}, bar:{hello:1,world:2}, baz:{hello:1,world:2}, FOO:'foo', BAR:2, BAZ:false }, { foo:'foo', bar:2, baz:false, FOO:{hello:1,world:2}, BAR:{hello:1,world:2}, BAZ:{hello:1,world:2} }, function (_object1PropertyInfo,_object2PropertyInfo) { return _object1PropertyInfo || _object2PropertyInfo; } ], { foo:{hello:1,world:2}, bar:{hello:1,world:2}, baz:{hello:1,world:2}, FOO:{hello:1,world:2}, BAR:{hello:1,world:2}, BAZ:{hello:1,world:2} } ] ]] ]) ] }); } });