SOURCE CODE: Uize.Test.Uize.Data.Diff (view docs)

/*______________
|       ______  |   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}
                }
              ]
          ]]
        ])
      ]
    });
  }
});