/*______________ | ______ | 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.Class.mChildObjectBindings 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: 3 codeCompleteness: 100 docCompleteness: 100 */ /*? Introduction The =Uize.Test.Uize.Class.mChildObjectBindings= module defines a suite of unit tests for the =Uize.Class.mChildObjectBindings= mixin module. *DEVELOPERS:* `Ben Ilegbodu`, original code contributed by `Zazzle Inc.` */ Uize.module ({ name:'Uize.Test.Uize.Class.mChildObjectBindings', superclass:'Uize.Test.Class', required:'Uize.Widget', builder:function (_superclass) { 'use strict'; var _Uize = Uize, _bindingsFunctionName = 'childBindings', _addMethodName = 'addChild', _removeMethodName = 'removeChild', _childObjectsInstancePropertyName = 'children', _bindingWiringsStaticDataName = 'mChildObjectBindings_' + _bindingsFunctionName + '_wirings', _class = _superclass.subclass({ moduleToTest:'Uize.Class.mChildObjectBindings' }) ; function _getRandomPropertyValue() { return Math.floor(Math.random() * 10000) } function _addChildren(_instance, _children) { _children = Uize.map ( _children, function (_childProperties, _childName) { if (_childName) { var _objectClass = _Uize.Widget; if ('objectClass' in _childProperties) { _childProperties = _Uize.clone(_childProperties); _objectClass = _childProperties.objectClass; delete _childProperties.objectClass; } _instance[_addMethodName](_childName, _objectClass, _childProperties); } } ); } function _getTestClass(_bindings, _stateProperties, _children) { return Uize.Widget.subclass ( _Uize.copyInto( { mixins:_Uize.Class.mChildObjectBindings, childObjectBindings:{ declaration:_bindingsFunctionName, instanceProperty:_childObjectsInstancePropertyName, addedInstanceProperty:'addedChildren', stateProperty:_childObjectsInstancePropertyName }, omegastructor:function() { _addChildren(this, _children) }, stateProperties:_stateProperties }, _Uize.pairUp(_bindingsFunctionName, _bindings) ) ); } function _getTestClassInstance(_bindings, _stateProperties, _children) { return _getTestClass(_bindings, _stateProperties, _children)({name:'parent'}); } function _generateTest(_title, _bindingsVerbose, _bindingsShorthand, _expectedBindings) { var _generatedTests = [], _stateProperties, _children ; function _generateSyntaxTests(_isVerbose) { var _bindings = _isVerbose ? _bindingsVerbose : _bindingsShorthand; function _getSyntaxTestClass(_omitChildren) { return _getTestClass(_bindings, _stateProperties, !_omitChildren && _children); } function _getSyntaxTestClassInstance(_omitChildren) { return _getSyntaxTestClass(_omitChildren)({name:'parent'}); } function _generateTestsForAll(_expectFunc, _omitChildren) { var _tests = []; if (!Uize.isEmpty(_expectFunc)) { Uize.forEach( _expectedBindings, function(_bindingsForProperty, _propertyName) { Uize.forEach( _bindingsForProperty, function(_bindingsForChild, _childName) { Uize.forEach( _bindingsForChild, function(_binding) { _tests.push({ title:'Test for: ' + _propertyName + ' ' + _binding.direction + ' ' + (_childName || '[self]') + '.' + _binding.property, test:function(_continue) { _expectFunc.call( this, Uize.copyInto( { instance:_getSyntaxTestClassInstance(_omitChildren), instanceProperty:_propertyName, child:_childName }, _binding ), _continue ) ;} }); } ); } ); } ); } return _tests; } return { title:(_isVerbose ? 'Verbose' : 'Shorthand') + ' Syntax', test:[ { title:'Class is not null', test:function() { return this.expectNonNull(_getSyntaxTestClass()) } }, { title:'Instance without any children is not null (instance works without any children)', test:function() { return this.expectNonNull(_getSyntaxTestClassInstance(true)) } }, { title:'Instance with children is not null (children aren\'t wired before they are added)', test:function() { return this.expectNonNull(_getSyntaxTestClassInstance()) } }, { title:'State property is synched with child\'s state property in the correct direction when child is added', test:_generateTestsForAll( function(_binding, _continue) { var _instance = _binding.instance, _initialValue = _instance.get(_binding.instanceProperty), _aToB = _binding.aToB || Uize.returnX, _bToA = Uize.returnX ; // Only when the direction is from child to instance does the child's initial value get synched to the instance if (_binding.direction == '<-') { _initialValue = _binding.child ? _children[_binding.child][_binding.property] : _instance.get(_binding.property) ; _aToB = Uize.returnX; _bToA = _binding.bToA || Uize.returnX; } _addChildren(_instance, _children); var _child = _binding.child ? _instance[_childObjectsInstancePropertyName][_binding.child] : _instance ; _continue( this.expect(_bToA(_initialValue), _instance.get(_binding.instanceProperty)) && this.expect(_aToB(_initialValue), _child.get(_binding.property)) ); }, true ) }, { title:'When instance value changes, child value updates to same value (when applicable)', test:_generateTestsForAll( function(_binding, _continue) { // we're not synching parent -> child so just keep going if (_binding.direction == '<-') { _continue(true); return; } var m = this, _instance = _binding.instance, _newValue = _getRandomPropertyValue(), _aToB = _binding.aToB || Uize.returnX, _child = _binding.child ? _instance[_childObjectsInstancePropertyName][_binding.child] : _instance ; // set instance to new value _instance.set(_binding.instanceProperty, _newValue); _continue(m.expect(_aToB(_newValue), _child.get(_binding.property))); } ) }, { title:'When child object value changes, instance value updates to same value (when applicable)', test:_generateTestsForAll( function(_binding, _continue) { // we're not synching child -> parent so just skip if (_binding.direction == '->') { _continue(true); return; } var m = this, _instance = _binding.instance, _newValue = _getRandomPropertyValue(), _bToA = _binding.bToA || Uize.returnX, _child = _binding.child ? _instance[_childObjectsInstancePropertyName][_binding.child] : _instance ; // set instance to new value _child.set(_binding.property, _newValue); _continue(m.expect(_bToA(_newValue), _instance.get(_binding.instanceProperty))); } ) } ] }; } if (_expectedBindings) { _children = {}; _stateProperties = {}; for (var _property in _expectedBindings) { var _bindingsForProperty = _expectedBindings[_property]; _stateProperties[_property] = { name:_property, value:_getRandomPropertyValue() }; for (var _childName in _bindingsForProperty) { var _bindingsForChild = _bindingsForProperty[_childName]; for (var _bindingNo = -1; ++_bindingNo < _bindingsForChild.length;) { var _childPropertyName = _bindingsForChild[_bindingNo].property, _propertyValue = _getRandomPropertyValue() ; if (_childName) (_children[_childName] = _children[_childName] || {})[_childPropertyName] = _propertyValue; else _stateProperties[_childPropertyName] = { name:_childPropertyName, value:_propertyValue }; } } } } (!_expectedBindings || _bindingsShorthand) && _generatedTests.push(_generateSyntaxTests()); (!_expectedBindings || _bindingsVerbose) && _generatedTests.push(_generateSyntaxTests(true)); return { title:_title, test:_generatedTests }; } return _class.declare({ set:{ test:[ _class.requiredModulesTest(), { title:'Empty', test:[ _generateTest('When no bindings are specified, no state properties are bound'), _generateTest('When empty bindings are specified, no state properties are bound', {}, {}) ] }, { title:'Errors', test:[ _generateTest( 'When no child is specified, nothing is bound (and no errors are thrown)', { propertyA:{} }, { propertyA:'' } ), _generateTest( 'When malformed shorthand syntax is specified, nothing is bound (and no errors are thrown)', undefined, { propertyA:'#child', propertyB:'child.' } ) ] }, { title:'Single State Property Binding', test:[ { title:'Single Child Binding', test:[ _generateTest( 'When only the child property is specified, the child is bound to same-named state property bi-directionally', { propertyA:{ child:'childA' } }, { propertyA:'childA' }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ] } } ), _generateTest( 'When the child & property properties are specified, the child is bound to the state property bi-directionally', { propertyA:{ child:'childA', property:'childPropertyA' } }, { propertyA:'childA.childPropertyA' }, { propertyA:{ childA:[ { property:'childPropertyA', direction:'<->' } ] } } ), { title:'When the child & direction properties are specified, the child is bound to same-named state property according to the direction', test:[ _generateTest( '<->', { propertyA:{ child:'childA', direction:'<->' } }, { propertyA:'<->childA' }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ] } } ), _generateTest( '->', { propertyA:{ child:'childA', direction:'->' } }, { propertyA:'->childA' }, { propertyA:{ childA:[ { property:'propertyA', direction:'->' } ] } } ), _generateTest( '<-', { propertyA:{ child:'childA', direction:'<-' } }, { propertyA:'<-childA' }, { propertyA:{ childA:[ { property:'propertyA', direction:'<-' } ] } } ) ] }, { title:'When the child & valueAdapter properties are specified, the child is bound to same-named state property via the appropriate value transformer', test:[ _generateTest( 'Empty value adapter', { propertyA:{ child:'childA', valueAdapter:{} } }, { propertyA:'childA' }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ] } } ), _generateTest( 'String value transformers', { propertyA:{ child:'childA', valueAdapter:{ aToB:'value * 2', bToA:'value / 2' } } }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value * 2 }, bToA:function(_value) { return _value / 2 } } ] } } ), _generateTest( 'Function value transformers', { propertyA:{ child:'childA', valueAdapter:{ aToB:function(_value) { return _value / 2 }, bToA:function(_value) { return _value * 2 } } } }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value / 2 }, bToA:function(_value) { return _value * 2 } } ] } } ), _generateTest( '->', { propertyA:{ child:'childA', direction:'->', valueAdapter:{ aToB:function(_value) { return _value - 15 } } } }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'->', aToB:function(_value) { return _value - 15 } } ] } } ), _generateTest( '<-', { propertyA:{ child:'childA', direction:'<-', valueAdapter:{ bToA:'value + 7' } } }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<-', bToA:function(_value) { return _value + 7 } } ] } } ) ] } ] }, { title:'Multiple Child Binding', test:[ _generateTest( 'When only the child property is specified, each child is bound to same-named state property bi-directionally', { propertyA:[ { child:'childA' }, { child:'childB' } ] }, { propertyA:[ 'childA', 'childB' ] }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ], childB:[ { property:'propertyA', direction:'<->' } ] } } ), _generateTest( 'When the child & property properties are specified, each child is bound to the state property bi-directionally', { propertyA:[ { child:'childA', property:'childPropertyA' }, { child:'childB', property:'childPropertyA' } ] }, { propertyA:[ 'childA.childPropertyA', 'childB.childPropertyA' ] }, { propertyA:{ childA:[ { property:'childPropertyA', direction:'<->' } ], childB:[ { property:'childPropertyA', direction:'<->' } ] } } ), { title:'When the child & direction properties are specified, each child is bound to same-named state property according to the direction', test:[ _generateTest( '<->', { propertyA:[ { child:'childA', direction:'<->' }, { child:'childB', direction:'<->' } ] }, { propertyA:[ '<->childA', '<->childB' ] }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ], childB:[ { property:'propertyA', direction:'<->' } ] } } ), _generateTest( '->', { propertyA:[ { child:'childA', direction:'->' }, { child:'childB', direction:'->' } ] }, { propertyA:[ '->childA', '->childB' ] }, { propertyA:{ childA:[ { property:'propertyA', direction:'->' } ], childB:[ { property:'propertyA', direction:'->' } ] } } )/*, _generateTest( '<-', { propertyA:[ { child:'childA', direction:'<-' }, { child:'childB', direction:'<-' } ] }, { propertyA:[ '<-childA', '<-childB' ] }, { propertyA:{ childA:[ { property:'propertyA', direction:'<-' } ], childB:[ { property:'propertyA', direction:'<-' } ] } } )*/ ] }, { title:'When the child & valueAdapter properties are specified, each child is bound to same-named state property via the appropriate value transformer', test:[ _generateTest( 'Empty value adapter', { propertyA:[ { child:'childA', valueAdapter:{} }, { child:'childB', valueAdapter:{} } ] }, { propertyA:['childA', 'childB'] }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ], childB:[ { property:'propertyA', direction:'<->' } ] } } ), _generateTest( 'String value transformers', { propertyA:[ { child:'childA', valueAdapter:{ aToB:'value * 2', bToA:'value / 2' } }, { child:'childB', valueAdapter:{ aToB:'value + 2', bToA:'value - 2' } } ] }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value * 2 }, bToA:function(_value) { return _value / 2 } } ], childB:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value + 2 }, bToA:function(_value) { return _value - 2 } } ] } } ), _generateTest( 'Function value transformers', { propertyA:[ { child:'childA', valueAdapter:{ aToB:function(_value) { return _value / 2 }, bToA:function(_value) { return _value * 2 } } }, { child:'childB', valueAdapter:{ aToB:function(_value) { return _value - 2 }, bToA:function(_value) { return _value + 2 } } } ] }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value / 2 }, bToA:function(_value) { return _value * 2 } } ], childB:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value - 2 }, bToA:function(_value) { return _value + 2 } } ] } } ), _generateTest( '->', { propertyA:[ { child:'childA', direction:'->', valueAdapter:{ aToB:function(_value) { return _value - 15 } } }, { child:'childB', direction:'->', valueAdapter:{ aToB:function(_value) { return _value * 15 } } }, { child:'childC', direction:'->', valueAdapter:{ aToB:function(_value) { return _value / 15 } } } ] }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'->', aToB:function(_value) { return _value - 15 } } ], childB:[ { property:'propertyA', direction:'->', aToB:function(_value) { return _value * 15 } } ], childC:[ { property:'propertyA', direction:'->', aToB:function(_value) { return _value / 15 } } ] } } )/*, _generateTest( '<-', { propertyA:[ { child:'childA', direction:'<-', valueAdapter:{ bToA:'value + 7' } }, { child:'childB', direction:'<-', valueAdapter:{ bToA:'value + 17' } } ] }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<-', bToA:function(_value) { return _value + 7 } } ], childB:[ { property:'propertyA', direction:'<-', bToA:function(_value) { return _value + 17 } } ] } } )*/ ] } ] } ] }, { title:'Multiple State Property Binding', test:[ { title:'Single Child Binding', test:[ _generateTest( 'When only the child property is specified, the child is bound to same-named state property bi-directionally', { propertyA:{ child:'childA' }, propertyB:{ child:'childA' } }, { propertyA:'childA', propertyB:'childA' }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( 'When the child & property properties are specified, the child is bound to the state property bi-directionally', { propertyA:{ child:'childA', property:'childPropertyA' }, propertyB:{ child:'childA', property:'childPropertyB' } }, { propertyA:'childA.childPropertyA', propertyB:'childA.childPropertyB' }, { propertyA:{ childA:[ { property:'childPropertyA', direction:'<->' } ] }, propertyB:{ childA:[ { property:'childPropertyB', direction:'<->' } ] } } ), { title:'When the child & direction properties are specified, the child is bound to same-named state property according to the direction', test:[ _generateTest( '<->', { propertyA:{ child:'childA', direction:'<->' }, propertyB:{ child:'childA', direction:'<->' } }, { propertyA:'<->childA', propertyB:'<->childA' }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( '->', { propertyA:{ child:'childA', direction:'->' }, propertyB:{ child:'childA', direction:'->' } }, { propertyA:'->childA', propertyB:'->childA' }, { propertyA:{ childA:[ { property:'propertyA', direction:'->' } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'->' } ] } } ), _generateTest( '<-', { propertyA:{ child:'childA', direction:'<-' }, propertyB:{ child:'childA', direction:'<-' } }, { propertyA:'<-childA', propertyB:'<-childA' }, { propertyA:{ childA:[ { property:'propertyA', direction:'<-' } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<-' } ] } } ) ] }, { title:'When the child & valueAdapter properties are specified, the child is bound to same-named state property via the appropriate value transformer', test:[ _generateTest( 'Empty value adapter', { propertyA:{ child:'childA', valueAdapter:{} }, propertyB:{ child:'childA', valueAdapter:{} } }, { propertyA:'childA', propertyB:'childA' }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( 'String value transformers', { propertyA:{ child:'childA', valueAdapter:{ aToB:'value * 2', bToA:'value / 2' } }, propertyB:{ child:'childA', valueAdapter:{ aToB:'value + 2', bToA:'value - 2' } } }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value * 2 }, bToA:function(_value) { return _value / 2 } } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<->', aToB:function(_value) { return _value + 2 }, bToA:function(_value) { return _value - 2 } } ] } } ), _generateTest( 'Function value transformers', { propertyA:{ child:'childA', valueAdapter:{ aToB:function(_value) { return _value / 2 }, bToA:function(_value) { return _value * 2 } } }, propertyB:{ child:'childA', valueAdapter:{ aToB:function(_value) { return _value - 2 }, bToA:function(_value) { return _value + 2 } } } }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value / 2 }, bToA:function(_value) { return _value * 2 } } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<->', aToB:function(_value) { return _value - 2 }, bToA:function(_value) { return _value + 2 } } ] } } ), _generateTest( '->', { propertyA:{ child:'childA', direction:'->', valueAdapter:{ aToB:function(_value) { return _value - 15 } } }, propertyB:{ child:'childA', direction:'->', valueAdapter:{ aToB:function(_value) { return _value * 15 } } }, propertyC:{ child:'childA', direction:'->', valueAdapter:{ aToB:function(_value) { return _value / 15 } } } }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'->', aToB:function(_value) { return _value - 15 } } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'->', aToB:function(_value) { return _value * 15 } } ] }, propertyC:{ childA:[ { property:'propertyC', direction:'->', aToB:function(_value) { return _value / 15 } } ] } } ), _generateTest( '<-', { propertyA:{ child:'childA', direction:'<-', valueAdapter:{ bToA:'value + 7' } }, propertyB:{ child:'childA', direction:'<-', valueAdapter:{ bToA:'value + 17' } } }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<-', bToA:function(_value) { return _value + 7 } } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<-', bToA:function(_value) { return _value + 17 } } ] } } ) ] } ] }, { title:'Multiple Child Binding', test:[ _generateTest( 'When only the child property is specified, each child is bound to same-named state property bi-directionally', { propertyA:[ { child:'childA' }, { child:'childB' } ], propertyB:[ { child:'childA' }, { child:'childB' } ] }, { propertyA:[ 'childA', 'childB' ], propertyB:[ 'childA', 'childB' ] }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ], childB:[ { property:'propertyA', direction:'<->' } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<->' } ], childB:[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( 'When the child & property properties specified, each child is bound to the state property bi-directionally', { propertyA:[ { child:'childA', property:'childPropertyA' }, { child:'childB', property:'childPropertyA' } ], propertyB:[ { child:'childA', property:'childPropertyB' }, { child:'childB', property:'childPropertyB' }, { child:'childC', property:'childPropertyB' } ] }, { propertyA:[ 'childA.childPropertyA', 'childB.childPropertyA' ], propertyB:[ 'childA.childPropertyB', 'childB.childPropertyB', 'childC.childPropertyB' ] }, { propertyA:{ childA:[ { property:'childPropertyA', direction:'<->' } ], childB:[ { property:'childPropertyA', direction:'<->' } ] }, propertyB:{ childA:[ { property:'childPropertyB', direction:'<->' } ], childB:[ { property:'childPropertyB', direction:'<->' } ], childC:[ { property:'childPropertyB', direction:'<->' } ] } } ), { title:'When the child & direction properties are specified, each child is bound to same-named state property according to the direction', test:[ _generateTest( '<->', { propertyA:[ { child:'childA', direction:'<->' }, { child:'childB', direction:'<->' }, { child:'childC', direction:'<->' }, { child:'childD', direction:'<->' } ], propertyB:[ { child:'childA', direction:'<->' }, { child:'childB', direction:'<->' } ] }, { propertyA:[ '<->childA', '<->childB', '<->childC', '<->childD' ], propertyB:[ '<->childA', '<->childB' ] }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ], childB:[ { property:'propertyA', direction:'<->' } ], childC:[ { property:'propertyA', direction:'<->' } ], childD:[ { property:'propertyA', direction:'<->' } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<->' } ], childB:[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( '->', { propertyA:[ { child:'childA', direction:'->' }, { child:'childB', direction:'->' }, { child:'childC', direction:'->' } ], propertyB:[ { child:'childA', direction:'->' }, { child:'childB', direction:'->' }, { child:'childC', direction:'->' } ], propertyC:[ { child:'childA', direction:'->' }, { child:'childB', direction:'->' } ] }, { propertyA:[ '->childA', '->childB', '->childC' ], propertyB:[ '->childA', '->childB', '->childC' ], propertyC:[ '->childA', '->childB' ] }, { propertyA:{ childA:[ { property:'propertyA', direction:'->' } ], childB:[ { property:'propertyA', direction:'->' } ], childC:[ { property:'propertyA', direction:'->' } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'->' } ], childB:[ { property:'propertyB', direction:'->' } ], childC:[ { property:'propertyB', direction:'->' } ] }, propertyC:{ childA:[ { property:'propertyC', direction:'->' } ], childB:[ { property:'propertyC', direction:'->' } ] } } )/*, _generateTest( '<-', { propertyA:[ { child:'childA', direction:'<-' }, { child:'childB', direction:'<-' }, { child:'childC', direction:'<-' }, { child:'childD', direction:'<-' } ], propertyB:[ { child:'childA', direction:'<-' }, { child:'childB', direction:'<-' } ] }, { propertyA:[ '<-childA', '<-childB', '<-childC', '<-childD' ], propertyB:[ '<-childA', '<-childB' ] }, { propertyA:{ childA:[ { property:'propertyA', direction:'<-' } ], childB:[ { property:'propertyA', direction:'<-' } ], childC:[ { property:'propertyA', direction:'<-' } ], childD:[ { property:'propertyA', direction:'<-' } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<-' } ], childB:[ { property:'propertyB', direction:'<-' } ] } } )*/ ] }, { title:'When the child & valueAdapter properties are specified, each child is bound to same-named state property via the appropriate value transformer', test:[ _generateTest( 'Empty value adapter', { propertyA:[ { child:'childA', valueAdapter:{} }, { child:'childB', valueAdapter:{} } ], propertyB:[ { child:'childA', valueAdapter:{} }, { child:'childB', valueAdapter:{} } ] }, { propertyA:['childA', 'childB'], propertyB:['childA', 'childB'] }, { propertyA:{ childA:[ { property:'propertyA', direction:'<->' } ], childB:[ { property:'propertyA', direction:'<->' } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<->' } ], childB:[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( 'String value transformers', { propertyA:[ { child:'childA', valueAdapter:{ aToB:'value * 2', bToA:'value / 2' } }, { child:'childB', valueAdapter:{ aToB:'value + 2', bToA:'value - 2' } } ], propertyB:[ { child:'childA', valueAdapter:{ aToB:'value - 20', bToA:'value + 20' } }, { child:'childB', valueAdapter:{ aToB:'value + 20', bToA:'value - 20' } } ] }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value * 2 }, bToA:function(_value) { return _value / 2 } } ], childB:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value + 2 }, bToA:function(_value) { return _value - 2 } } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<->', aToB:function(_value) { return _value - 20 }, bToA:function(_value) { return _value + 20 } } ], childB:[ { property:'propertyB', direction:'<->', aToB:function(_value) { return _value + 20 }, bToA:function(_value) { return _value - 20 } } ] } } ), _generateTest( 'Function value transformers', { propertyA:[ { child:'childA', valueAdapter:{ aToB:function(_value) { return _value / 2 }, bToA:function(_value) { return _value * 2 } } }, { child:'childB', valueAdapter:{ aToB:function(_value) { return _value - 2 }, bToA:function(_value) { return _value + 2 } } } ], propertyB:[ { child:'childA', valueAdapter:{ aToB:function(_value) { return _value / 10 }, bToA:function(_value) { return _value * 10 } } }, { child:'childB', valueAdapter:{ aToB:function(_value) { return _value - 10 }, bToA:function(_value) { return _value + 10 } } } ] }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value / 2 }, bToA:function(_value) { return _value * 2 } } ], childB:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value - 2 }, bToA:function(_value) { return _value + 2 } } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'<->', aToB:function(_value) { return _value / 10 }, bToA:function(_value) { return _value * 10 } } ], childB:[ { property:'propertyB', direction:'<->', aToB:function(_value) { return _value - 10 }, bToA:function(_value) { return _value + 10 } } ] } } ), _generateTest( '->', { propertyA:[ { child:'childA', direction:'->', valueAdapter:{ aToB:function(_value) { return _value - 15 } } }, { child:'childB', direction:'->', valueAdapter:{ aToB:function(_value) { return _value * 15 } } }, { child:'childC', direction:'->', valueAdapter:{ aToB:function(_value) { return _value / 15 } } } ], propertyB:[ { child:'childA', direction:'->', valueAdapter:{ aToB:function(_value) { return _value - 15 } } }, { child:'childB', direction:'->', valueAdapter:{ aToB:function(_value) { return _value * 15 } } }, { child:'childC', direction:'->', valueAdapter:{ aToB:function(_value) { return _value / 15 } } } ] }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'->', aToB:function(_value) { return _value - 15 } } ], childB:[ { property:'propertyA', direction:'->', aToB:function(_value) { return _value * 15 } } ], childC:[ { property:'propertyA', direction:'->', aToB:function(_value) { return _value / 15 } } ] }, propertyB:{ childA:[ { property:'propertyB', direction:'->', aToB:function(_value) { return _value - 15 } } ], childB:[ { property:'propertyB', direction:'->', aToB:function(_value) { return _value * 15 } } ], childC:[ { property:'propertyB', direction:'->', aToB:function(_value) { return _value / 15 } } ] } } )/*, _generateTest( '<-', { propertyA:[ { child:'childA', direction:'<-', valueAdapter:{ bToA:'value + 7' } }, { child:'childB', direction:'<-', valueAdapter:{ bToA:'value + 17' } } ] }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<-', bToA:function(_value) { return _value + 7 } } ], childB:[ { property:'propertyA', direction:'<-', bToA:function(_value) { return _value + 17 } } ] } } )*/ ] } ] } ] }, { title:'Conditional binding (when)', test:[ { title:'When *when* condition is specified and it isn\'t initially met, the children do *not* initially have the instance\'s properties', test:_Uize.map( [ { child:'childA', when:'whenProperty' }, 'childA:whenProperty' ], function(_propertyBinding) { return { title:_Uize.isPlainObject(_propertyBinding) ? 'Verbose Syntax' : 'Shorthand Syntax', test:function() { var m = this, _childInstancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( {propertyA:_propertyBinding}, { propertyA:{name:'propertyA',value:_getRandomPropertyValue()}, propertyB:{name:'whenProperty', value:false} }, {childA:{propertyA:_childInstancePropertyValue}} ) ; return m.expect(_childInstancePropertyValue, _instance[_childObjectsInstancePropertyName].childA.get('propertyA')); } }; } ) }, { title:'When *when* condition is specified and it isn\'t initially met, the instance\'s properties do not initially have the child\'s', test:_Uize.map( [ { child:'childA', property:'propertyA', direction:'<-', when:'whenProperty' }, '<-childA.propertyA:whenProperty' ], function(_propertyBinding) { return { title:_Uize.isPlainObject(_propertyBinding) ? 'Verbose Syntax' : 'Shorthand Syntax', test:function() { var m = this, _instancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( {propertyA:_propertyBinding}, { propertyA:{name:'propertyA',value:_instancePropertyValue}, propertyB:{name:'whenProperty', value:false} }, {childA:{propertyA:_getRandomPropertyValue()}} ) ; return m.expect(_instancePropertyValue, _instance.get('propertyA')); } }; } ) }, { title:'When *when* condition is specified and it isn\'t initially met, but becomes met, the children\'s properties are synced', test:_Uize.map( [ { child:'childA', when:'whenProperty' }, 'childA:whenProperty' ], function(_propertyBinding) { return { title:_Uize.isPlainObject(_propertyBinding) ? 'Verbose Syntax' : 'Shorthand Syntax', test:function() { var m = this, _instancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( {propertyA:_propertyBinding}, { propertyA:{name:'propertyA',value:_instancePropertyValue}, propertyB:{name:'whenProperty', value:false} }, {childA:{propertyA:_getRandomPropertyValue()}} ) ; _instance.met('whenProperty'); return m.expect(_instancePropertyValue, _instance[_childObjectsInstancePropertyName].childA.get('propertyA')); } }; } ) }, { title:'When *when* condition is specified and it isn\'t initially met, but becomes met, the intance\'s properties are synced', test:function() { var m = this, _childInstancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( { propertyA:{ child:'childA', direction:'<-', when:'whenProperty' } }, { propertyA:{name:'propertyA',value:_getRandomPropertyValue()}, propertyB:{name:'whenProperty', value:false} }, {childA:{propertyA:_childInstancePropertyValue}} ) ; _instance.met('whenProperty'); return m.expect(_childInstancePropertyValue, _instance.get('propertyA')); } }, { title:'When *when* condition is specified and it isn\'t initially met and the instance\'s value changes, the children still do *not* have the instance\'s properties', test:function() { var m = this, _childInstancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( { propertyA:{ child:'childA', when:'whenProperty' } }, { propertyA:{name:'propertyA',value:_getRandomPropertyValue()}, propertyB:{name:'whenProperty', value:false} }, {childA:{propertyA:_childInstancePropertyValue}} ) ; _instance.set('propertyA', _getRandomPropertyValue()); return m.expect(_childInstancePropertyValue, _instance[_childObjectsInstancePropertyName].childA.get('propertyA')); } }, { title:'When *when* condition is specified and it isn\'t initially met and the child\'s value changes, the instance\'s properties still do *not* have the child\'s', test:function() { var m = this, _instancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( { propertyA:{ child:'childA', direction:'<-', when:'whenProperty' } }, { propertyA:{name:'propertyA',value:_instancePropertyValue}, propertyB:{name:'whenProperty', value:false} }, {childA:{propertyA:_getRandomPropertyValue()}} ), _child = _instance[_childObjectsInstancePropertyName].childA ; _child.set('propertyA', _getRandomPropertyValue()); return m.expect(_instancePropertyValue, _instance.get('propertyA')); } }, { title:'When *when* condition is specified and it is initially met, the children\'s properties are synced', test:function() { var m = this, _instancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( { propertyA:{ child:'childA', when:'whenProperty' } }, { propertyA:{name:'propertyA',value:_instancePropertyValue}, propertyB:{name:'whenProperty', value:true} }, {childA:{propertyA:_getRandomPropertyValue()}} ) ; return m.expect(_instancePropertyValue, _instance[_childObjectsInstancePropertyName].childA.get('propertyA')); } }, { title:'When *when* condition is specified and it isn initially met, the intance\'s properties are synced', test:function() { var m = this, _childInstancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( { propertyA:{ child:'childA', direction:'<-', when:'whenProperty' } }, { propertyA:{name:'propertyA',value:_getRandomPropertyValue()}, propertyB:{name:'whenProperty', value:true} }, {childA:{propertyA:_childInstancePropertyValue}} ) ; return m.expect(_childInstancePropertyValue, _instance.get('propertyA')); } }, { title:'When *when* condition is specified and it is initially met, but becomes unmet and the instance\'s value changes, the children still do *not* have the instance\'s properties', test:function() { var m = this, _instancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( { propertyA:{ child:'childA', when:'whenProperty' } }, { propertyA:{name:'propertyA',value:_instancePropertyValue}, propertyB:{name:'whenProperty', value:true} }, {childA:{propertyA:_getRandomPropertyValue()}} ) ; _instance.unmet('whenProperty'); _instance.set('propertyA', _getRandomPropertyValue()); return m.expect(_instancePropertyValue, _instance[_childObjectsInstancePropertyName].childA.get('propertyA')); } }, { title:'When *when* condition is specified and it is initially met, but becomes unmet and the child\'s value changes, the instance\'s properties still do *not* have the child\'s', test:function() { var m = this, _childInstancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( { propertyA:{ child:'childA', direction:'<-', when:'whenProperty' } }, { propertyA:{name:'propertyA',value:_getRandomPropertyValue()}, propertyB:{name:'whenProperty', value:true} }, {childA:{propertyA:_childInstancePropertyValue}} ), _child = _instance[_childObjectsInstancePropertyName].childA ; _instance.unmet('whenProperty'); _child.set('propertyA', _getRandomPropertyValue()); return m.expect(_childInstancePropertyValue, _instance.get('propertyA')); } }, { title:'When *when* condition is specified and it isn\'t initially met, the child is removed, and condition becomes met, the child\'s value is *not* synched', test:function() { var m = this, _childInstancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( { propertyA:{ child:'childA', when:'whenProperty' } }, { propertyA:{name:'propertyA',value:_getRandomPropertyValue()}, propertyB:{name:'whenProperty', value:false} }, {childA:{propertyA:_childInstancePropertyValue}} ), _child = _instance[_childObjectsInstancePropertyName].childA ; _instance.removeChild(_child); _instance.met('whenProperty'); return m.expect(_childInstancePropertyValue, _child.get('propertyA')); } }, { title:'When *when* condition is specified and it isn\'t initially met, the child is removed, and condition becomes met, the instance\'s properties still do *not* have the child\'s', test:function() { var m = this, _instancePropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( { propertyA:{ child:'childA', direction:'<-', when:'whenProperty' } }, { propertyA:{name:'propertyA',value:_instancePropertyValue}, propertyB:{name:'whenProperty', value:false} }, {childA:{propertyA:_getRandomPropertyValue()}} ), _child = _instance[_childObjectsInstancePropertyName].childA ; _instance.removeChild(_child); _instance.met('whenProperty'); return m.expect(_instancePropertyValue, _instance.get('propertyA')); } } ] }, { title:'Edge Cases', test:[ { title:'Child is self (parent) widget', test:[ _generateTest( 'Omit child name (and no child property specified so it\'s pointing back to itself)', { propertyA:{} }, { propertyA:'' }, { propertyA:{ '':[ { property:'propertyA', direction:'<->' } ] } } ), _generateTest( 'Omit child name (same property name specified so it\'s pointing back to itself)', { propertyA:{ property:'propertyA' } }, { propertyA:'.propertyA' }, { propertyA:{ '':[ { property:'propertyA', direction:'<->' } ] } } ), _generateTest( 'Omit child name (different property name)', { propertyA:{ property:'propertyB' } }, { propertyA:'.propertyB' }, { propertyA:{ '':[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( 'Specify child name (null)', { propertyA:{ child:null, property:'propertyB' } }, { propertyA:'.propertyB' }, { propertyA:{ '':[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( 'Specify child name (undefined)', { propertyA:{ child:undefined, property:'propertyB' } }, { propertyA:'.propertyB' }, { propertyA:{ '':[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( 'Specify child name (empty)', { propertyA:{ child:'', property:'propertyB' } }, { propertyA:'.propertyB' }, { propertyA:{ '':[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( 'Specify child name, property & direction (<->)', { propertyA:{ child:'', property:'propertyB', direction:'<->' } }, { propertyA:'<->.propertyB' }, { propertyA:{ '':[ { property:'propertyB', direction:'<->' } ] } } ), _generateTest( 'Specify child name, property & direction (->)', { propertyA:{ child:'', property:'propertyB', direction:'->' } }, { propertyA:'->.propertyB' }, { propertyA:{ '':[ { property:'propertyB', direction:'->' } ] } } ), _generateTest( 'Specify child name, property & direction (<-)', { propertyA:{ child:'', property:'propertyB', direction:'<-' } }, { propertyA:'<-.propertyB' }, { propertyA:{ '':[ { property:'propertyB', direction:'<-' } ] } } ) ] }, { title:'When a child is added, it is created with its parent\'s initial state for its bound properties to the parent', test:function() { var m = this, _childChangeHandlerCallCount = 0, _instance = _getTestClassInstance( {propertyA:'childA'}, {propertyA:{name:'propertyA',value:_getRandomPropertyValue()}} ), _childClass = _Uize.Widget.subclass({ stateProperties:{ propertyA:{ name:'propertyA', onChange:function() { _childChangeHandlerCallCount++; }, value:_getRandomPropertyValue() } } }) ; _instance[_addMethodName]('childA', _childClass); return m.expect(1, _childChangeHandlerCallCount); } }, { title:'When a child is bound bi-directionally and the instance has data and the child doesn\'t, the instance should be the driver object', test:function() { var _instanceInitialPropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( {propertyA:'childA'}, {propertyA:{name:'propertyA',value:_instanceInitialPropertyValue}}, {childA:{}} ) ; return this.expect(_instanceInitialPropertyValue, _instance.get('propertyA')) && this.expect(_instance.get('propertyA'), _instance[_childObjectsInstancePropertyName].childA.get('propertyA')) ; } }, { title:'When a child is bound bi-directionally and it has data and the instance doesn\'t, the child should be the driver object', test:function() { var _childObjectInitialPropertyValue = _getRandomPropertyValue(), _instance = _getTestClassInstance( {propertyA:'childA'}, {propertyA:{name:'propertyA'}}, {childA:{propertyA:_childObjectInitialPropertyValue}} ), _childAObject = _instance[_childObjectsInstancePropertyName].childA ; return this.expect(_childObjectInitialPropertyValue, _childAObject.get('propertyA')) && this.expect(_childAObject.get('propertyA'), _instance.get('propertyA')) ; } }, /*_generateTest( 'Thrashing doesn\'t occurr with bi-directional bindings (best demonstrated with mis-match value adapters)', { propertyA:{ child:'childA', valueAdapter:{ aToB:'value * 3', bToA:'value / 2' } } }, null, { propertyA:{ childA:[ { property:'propertyA', direction:'<->', aToB:function(_value) { return _value * 3}, bToA:function(_value) { return _value / 2 } } ] } } ),*/ { title:'When a bound child is removed, a state property change in parent after removal is properly handled (no errors and not fired on child)', test:function() { var _instance = _getTestClassInstance( {propertyA:'childA'}, {propertyA:{name:'propertyA',value:_getRandomPropertyValue()}}, {childA:{propertyA:_getRandomPropertyValue()}} ), _childToRemove = _instance[_childObjectsInstancePropertyName].childA // keep reference so we'll have it after removal ; _instance[_removeMethodName](_childToRemove); // set instance to new value to see if child will also update (which it shouldn't) _instance.set('propertyA', _getRandomPropertyValue()); return this.expect(true, _instance.get('propertyA') != _childToRemove.get('propertyA')); } }, { title:'When a bound child is removed, a state property change in child after removal is properly handled (no errors and not fired on parent)', test:function() { var _instance = _getTestClassInstance( {propertyA:'childA'}, {propertyA:{name:'propertyA',value:_getRandomPropertyValue()}}, {childA:{propertyA:_getRandomPropertyValue()}} ), _childToRemove = _instance[_childObjectsInstancePropertyName].childA // keep reference so we'll have it after removal ; _instance[_removeMethodName](_childToRemove); // set instance to new value to see if child will also update (which it shouldn't) _childToRemove.set('propertyA', _getRandomPropertyValue()); return this.expect(true, _instance.get('propertyA') != _childToRemove.get('propertyA')); } }, { title:'When a bound child is removed, and a new same-named child is re-added, the new child\'s state is sync\'d', test:function() { var _instance = _getTestClassInstance( {propertyA:'childA'}, {propertyA:{name:'propertyA',value:_getRandomPropertyValue()}}, {childA:{propertyA:_getRandomPropertyValue()}} ) ; // first remove the child _instance[_removeMethodName]('childA'); // then add back a new child with the same name (which should get bound again) _instance.addChild('childA', Uize.Widget, {propertyA:_getRandomPropertyValue()}); // state should match when the child's state is synched return this.expect(_instance.get('propertyA'), _instance[_childObjectsInstancePropertyName].childA.get('propertyA')); } }, { title:'When a bound child is removed, and a new same-named child is re-added, a change in instance state should be reflected in child\'s', test:function() { var _instance = _getTestClassInstance( {propertyA:'childA'}, {propertyA:{name:'propertyA',value:_getRandomPropertyValue()}}, {childA:{propertyA:_getRandomPropertyValue()}} ) ; // first remove the child _instance[_removeMethodName]('childA'); // then add back a new child with the same name (which should get bound again) _instance.addChild('childA', Uize.Widget, {propertyA:_getRandomPropertyValue()}); // set instance to new value (which should get bound to child) _instance.set('propertyA', _getRandomPropertyValue()); return this.expect(_instance.get('propertyA'), _instance[_childObjectsInstancePropertyName].childA.get('propertyA')); } }, { title:'When a subclass declares the same state property/child/child state property combination, the base class\' declaration is overridden', test:function() { var _Class = _getTestClass( { propertyA:[ { child:'childA', property:'childPropertyA', direction:'<->', valueAdapter:{ aToB:'value * 2', bToA:'value / 2' } }, 'childB' ] }, { propertyA:{name:'propertyA',value:_getRandomPropertyValue()}, propertyB:{name:'propertyB',value:_getRandomPropertyValue()} }, { childA:{ propertyA:_getRandomPropertyValue(), propertyB:_getRandomPropertyValue() }, childB:{ propertyA:_getRandomPropertyValue() } } ), _Subclass = _Class.subclass( _Uize.pairUp( _bindingsFunctionName, { propertyA:'->childA.childPropertyA' } ) ), _ClassBindingsData = _Class[_bindingWiringsStaticDataName], _SubclassBindingsData = _Subclass[_bindingWiringsStaticDataName] ; return this.expectSameAs(_SubclassBindingsData.childB['propertyA/propertyA'], _ClassBindingsData.childB['propertyA/propertyA']) && this.expectNotSameAs(_SubclassBindingsData.childA['propertyA/childPropertyA'], _ClassBindingsData.childA['propertyA/childPropertyA']) && this.expect(1, Uize.keys(_SubclassBindingsData.childA).length) && this.expectNonNull(1, _SubclassBindingsData.childA['propertyA/childPropertyA']) && this.expect(1, Uize.keys(_SubclassBindingsData.childB).length) && this.expectNonNull(1, _SubclassBindingsData.childB['propertyA/propertyA']) ; } } ] } ] } }); } });