// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== // ======================================================================== // SC.Observable Tests // ======================================================================== /*globals module test ok isObj equals expects Namespace */ var object, ObjectC, ObjectD, objectA, objectB ; // .......................................................... // GET() // module("object.get()", { setup: function() { object = SC.Object.create({ normal: 'value', numberVal: 24, toggleVal: true, computed: function() { return 'value'; }.property(), method: function() { return "value"; }, nullProperty: null, unknownProperty: function(key, value) { this.lastUnknownProperty = key ; return "unknown" ; } }); } }); test("should get normal properties", function() { equals(object.get('normal'), 'value') ; }); test("should call computed properties and return their result", function() { equals(object.get("computed"), "value") ; }); test("should return the function for a non-computed property", function() { var value = object.get("method") ; equals(SC.typeOf(value), SC.T_FUNCTION) ; }); test("should return null when property value is null", function() { equals(object.get("nullProperty"), null) ; }); test("should call unknownProperty when value is undefined", function() { equals(object.get("unknown"), "unknown") ; equals(object.lastUnknownProperty, "unknown") ; }); // .......................................................... // SC.GET() // module("SC.get()", { setup: function() { objectA = SC.Object.create({ normal: 'value', numberVal: 24, toggleVal: true, computed: function() { return 'value'; }.property(), method: function() { return "value"; }, nullProperty: null, unknownProperty: function(key, value) { this.lastUnknownProperty = key ; return "unknown" ; } }); objectB = { normal: 'value', nullProperty: null }; } }); test("should get normal properties on SC.Observable", function() { equals(SC.get(objectA, 'normal'), 'value') ; }); test("should call computed properties on SC.Observable and return their result", function() { equals(SC.get(objectA, "computed"), "value") ; }); test("should return the function for a non-computed property on SC.Observable", function() { var value = SC.get(objectA, "method") ; equals(SC.typeOf(value), SC.T_FUNCTION) ; }); test("should return null when property value is null on SC.Observable", function() { equals(SC.get(objectA, "nullProperty"), null) ; }); test("should call unknownProperty when value is undefined on SC.Observable", function() { equals(SC.get(object, "unknown"), "unknown") ; equals(object.lastUnknownProperty, "unknown") ; }); test("should get normal properties on standard objects", function() { equals(SC.get(objectB, 'normal'), 'value'); }); test("should return null when property is null on standard objects", function() { equals(SC.get(objectB, 'nullProperty'), null); }); test("should return undefined if the provided object is null", function() { equals(SC.get(null, 'key'), undefined); }); test("should return undefined if the provided object is undefined", function() { equals(SC.get(undefined, 'key'), undefined); }); test("should work when object is SC (used in SC.objectForPropertyPath)", function() { equals(SC.objectForPropertyPath('SC.RunLoop'), SC.RunLoop); equals(SC.get('RunLoop'), SC.RunLoop); equals(SC.get(SC, 'RunLoop'), SC.RunLoop); }); module("SC.getPath()"); test("should return a property at a given path relative to the window", function() { window.Foo = SC.Object.create({ Bar: SC.Object.create({ Baz: function() { return "blargh"; }.property() }) }); try { equals(SC.getPath('Foo.Bar.Baz'), "blargh"); } finally { window.Foo = undefined; } }); test("should return a property at a given path relative to the passed object", function() { var foo = SC.Object.create({ bar: SC.Object.create({ baz: function() { return "blargh"; }.property() }) }); equals(SC.getPath(foo, 'bar.baz'), "blargh"); }); test("should return a property at a given path relative to the window - JavaScript hash", function() { window.Foo = { Bar: { Baz: "blargh" } }; try { equals(SC.getPath('Foo.Bar.Baz'), "blargh"); } finally { window.Foo = undefined; } }); test("should return a property at a given path relative to the passed object - JavaScript hash", function() { var foo = { bar: { baz: "blargh" } }; equals(SC.getPath(foo, 'bar.baz'), "blargh"); }); // .......................................................... // SET() // module("object.set()", { setup: function() { object = SC.Object.create({ // normal property normal: 'value', // computed property _computed: "computed", computed: function(key, value) { if (value !== undefined) { this._computed = value ; } return this._computed ; }.property(), // method, but not a property _method: "method", method: function(key, value) { if (value !== undefined) { this._method = value ; } return this._method ; }, // null property nullProperty: null, // unknown property _unknown: 'unknown', unknownProperty: function(key, value) { if (value !== undefined) { this._unknown = value ; } return this._unknown ; } }); } }); test("should change normal properties and return this", function() { var ret = object.set("normal", "changed") ; equals(object.normal, "changed") ; equals(ret, object) ; }); test("should call computed properties passing value and return this", function() { var ret = object.set("computed", "changed") ; equals(object._computed, "changed") ; equals(SC.typeOf(object.computed), SC.T_FUNCTION) ; equals(ret, object) ; }); test("should replace the function for a non-computed property and return this", function() { var ret = object.set("method", "changed") ; equals(object._method, "method") ; // make sure this was NOT run ok(SC.typeOf(object.method) !== SC.T_FUNCTION) ; equals(ret, object) ; }); test("should replace prover when property value is null", function() { var ret = object.set("nullProperty", "changed") ; equals(object.nullProperty, "changed") ; equals(object._unknown, "unknown"); // verify unknownProperty not called. equals(ret, object) ; }); test("should call unknownProperty with value when property is undefined", function() { var ret = object.set("unknown", "changed") ; equals(object._unknown, "changed") ; equals(ret, object) ; }); // .......................................................... // COMPUTED PROPERTIES // module("Computed properties", { setup: function() { object = SC.Object.create({ // REGULAR computedCalls: [], computed: function(key, value) { this.computedCalls.push(value); return 'computed'; }.property(), computedCachedCalls: [], computedCached: function(key, value) { this.computedCachedCalls.push(value); return 'computedCached'; }.property().cacheable(), // DEPENDENT KEYS changer: 'foo', dependentCalls: [], dependent: function(key, value) { this.dependentCalls.push(value); return 'dependent'; }.property('changer'), dependentCachedCalls: [], dependentCached: function(key, value) { this.dependentCachedCalls.push(value); return 'dependentCached'; }.property('changer').cacheable(), // every time it is recomputed, increments call incCallCount: 0, inc: function() { return this.incCallCount++; }.property('changer').cacheable(), // depends on cached property which depends on another property... nestedIncCallCount: 0, nestedInc: function(key, value) { return this.nestedIncCallCount++; }.property('inc').cacheable(), // two computed properties that depend on a third property state: 'on', isOn: function(key, value) { if (value !== undefined) this.set('state', 'on'); return this.get('state') === 'on'; }.property('state'), isOff: function(key, value) { if (value !== undefined) this.set('state', 'off'); return this.get('state') === 'off'; }.property('state') }) ; } }); test("getting values should call function return value", function() { // get each property twice. Verify return. var keys = 'computed computedCached dependent dependentCached'.w(); keys.forEach(function(key) { equals(object.get(key), key, 'Try #1: object.get(%@) should run function'.fmt(key)); equals(object.get(key), key, 'Try #2: object.get(%@) should run function'.fmt(key)); }); // verify each call count. cached should only be called once 'computedCalls dependentCalls'.w().forEach(function(key) { equals(object[key].length, 2, 'non-cached property %@ should be called 2x'.fmt(key)); }); 'computedCachedCalls dependentCachedCalls'.w().forEach(function(key) { equals(object[key].length, 1, 'non-cached property %@ should be called 1x'.fmt(key)); }); }); test("setting values should call function return value", function() { // get each property twice. Verify return. var keys = 'computed dependent computedCached dependentCached'.w(); var values = 'value1 value2'.w(); keys.forEach(function(key) { equals(object.set(key, values[0]), object, 'Try #1: object.set(%@, %@) should run function'.fmt(key, values[0])); equals(object.set(key, values[1]), object, 'Try #2: object.set(%@, %@) should run function'.fmt(key, values[1])); equals(object.set(key, values[1]), object, 'Try #3: object.set(%@, %@) should not run function since it is setting same value as before'.fmt(key, values[1])); }); // verify each call count. cached should only be called once keys.forEach(function(key) { var calls = object[key + 'Calls'], idx; equals(calls.length, 2, 'set(%@) should be called 2x'.fmt(key)); for(idx=0;idx<2;idx++) { equals(calls[idx], values[idx], 'call #%@ to set(%@) should have passed value %@'.fmt(idx+1, key, values[idx])); } }); }); test("notify change should clear cache", function() { // call get several times to collect call count object.get('computedCached'); // should run func object.get('computedCached'); // should not run func object.propertyWillChange('computedCached') .propertyDidChange('computedCached'); object.get('computedCached'); // should run again equals(object.computedCachedCalls.length, 2, 'should have invoked method 2x'); }); test("change dependent should clear cache", function() { // call get several times to collect call count var ret1 = object.get('inc'); // should run func equals(object.get('inc'), ret1, 'multiple calls should not run cached prop'); object.set('changer', 'bar'); equals(object.get('inc'), ret1+1, 'should increment after dependent key changes'); // should run again }); test("just notifying change of dependent should clear cache", function() { // call get several times to collect call count var ret1 = object.get('inc'); // should run func equals(object.get('inc'), ret1, 'multiple calls should not run cached prop'); object.notifyPropertyChange('changer'); equals(object.get('inc'), ret1+1, 'should increment after dependent key changes'); // should run again }); test("changing dependent should clear nested cache", function() { // call get several times to collect call count var ret1 = object.get('nestedInc'); // should run func equals(object.get('nestedInc'), ret1, 'multiple calls should not run cached prop'); object.set('changer', 'bar'); equals(object.get('nestedInc'), ret1+1, 'should increment after dependent key changes'); // should run again }); test("just notifying change of dependent should clear nested cache", function() { // call get several times to collect call count var ret1 = object.get('nestedInc'); // should run func equals(object.get('nestedInc'), ret1, 'multiple calls should not run cached prop'); object.notifyPropertyChange('changer'); equals(object.get('nestedInc'), ret1+1, 'should increment after dependent key changes'); // should run again }); // This verifies a specific bug encountered where observers for computed // properties would fire before their prop caches were cleared. test("change dependent should clear cache when observers of dependent are called", function() { // call get several times to collect call count var ret1 = object.get('inc'); // should run func equals(object.get('inc'), ret1, 'multiple calls should not run cached prop'); // add observer to verify change... object.addObserver('inc', this, function() { equals(object.get('inc'), ret1+1, 'should increment after dependent key changes'); // should run again }); // now run object.set('changer', 'bar'); }); test("allPropertiesDidChange should clear cache", function() { // note: test this with a computed method that returns a different value // each time to ensure clean function. var ret1 = object.get('inc'); equals(object.get('inc'), ret1, 'should not change after first call'); // flush all props object.allPropertiesDidChange(); equals(object.get('inc'), ret1+1, 'should increment after change'); }); test('setting one of two computed properties that depend on a third property should clear the kvo cache', function() { // we have to call set twice to fill up the cache object.set('isOff', YES); object.set('isOn', YES); // setting isOff to YES should clear the kvo cache object.set('isOff', YES); equals(object.get('isOff'), YES, 'object.isOff should be YES'); equals(object.get('isOn'), NO, 'object.isOn should be NO'); }); test("dependent keys should be able to be specified as property paths", function() { var depObj = SC.Object.create({ menu: SC.Object.create({ price: 5 }), menuPrice: function() { return this.getPath('menu.price'); }.property('menu.price').cacheable() }); equals(depObj.get('menuPrice'), 5, "precond - initial value returns 5"); depObj.setPath('menu.price', 6); equals(depObj.get('menuPrice'), 6, "cache is properly invalidated after nested property changes"); }); test("nested dependent keys should propagate after they update", function() { window.DepObj = SC.Object.create({ restaurant: SC.Object.create({ menu: SC.Object.create({ price: 5 }) }), price: function() { return this.getPath('restaurant.menu.price'); }.property('restaurant.menu.price') }); var bindObj = SC.Object.create({ priceBinding: "DepObj.price" }); SC.run(); equals(bindObj.get('price'), 5, "precond - binding propagates"); DepObj.setPath('restaurant.menu.price', 10); SC.run(); equals(bindObj.get('price'), 10, "binding propagates after a nested dependent keys updates"); DepObj.setPath('restaurant.menu', SC.Object.create({ price: 15 })); SC.run(); equals(bindObj.get('price'), 15, "binding propagates after a middle dependent keys updates"); }); test("cacheable nested dependent keys should clear after their dependencies update", function() { window.DepObj = SC.Object.create({ restaurant: SC.Object.create({ menu: SC.Object.create({ price: 5 }) }), price: function() { return this.getPath('restaurant.menu.price'); }.property('restaurant.menu.price').cacheable() }); SC.run(); equals(DepObj.get('price'), 5, "precond - computed property is correct"); DepObj.setPath('restaurant.menu.price', 10); equals(DepObj.get('price'), 10, "cacheable computed properties are invalidated even if no run loop occurred"); DepObj.setPath('restaurant.menu.price', 20); equals(DepObj.get('price'), 20, "cacheable computed properties are invalidated after a second get before a run loop"); SC.run(); equals(DepObj.get('price'), 20, "precond - computed properties remain correct after a run loop"); DepObj.setPath('restaurant.menu', SC.Object.create({ price: 15 })); equals(DepObj.get('price'), 15, "cacheable computed properties are invalidated after a middle property changes"); DepObj.setPath('restaurant.menu', SC.Object.create({ price: 25 })); equals(DepObj.get('price'), 25, "cacheable computed properties are invalidated after a middle property changes again, before a run loop"); }); // .......................................................... // OBSERVABLE OBJECTS // module("Observable objects & object properties ", { setup: function() { window.NormalArray = [1,2,3,4,5]; object = SC.Object.create({ normal: 'value', abnormal: 'zeroValue', abnormal2: 'zeroValue', abnormal3: 'zeroValue', numberVal: 24, toggleVal: true, observedProperty: 'beingWatched', testRemove: 'observerToBeRemoved', normalArray: [1,2,3,4,5], automaticallyNotifiesObserversFor : function(key) { return NO; }, getEach: function() { var keys = ['normal','abnormal']; var ret = []; for(var idx=0; idx 0, "observes array handler should be notified aftering pushing object to array"); objectA.arrayHandlerNotifiedCount = 0; objectA.removeObservesHandler(objectA.arrayHandler, 'arrayValue.[]'); objectA.arrayValue.pushObject(SC.Object.create()); equals(objectA.arrayHandlerNotifiedCount, 0, "observes array handler should not be notified after removing observes handler"); objectA.addObservesHandler(objectA.arrayHandler, 'arrayValue.[]'); objectA.set('arrayValue', []); equals(objectA.arrayHandlerNotifiedCount, 0, "observes array handler should not be notified after assigning new array"); objectA.arrayValue.pushObject(SC.Object.create()); equals(objectA.arrayHandlerNotifiedCount, 0, "observes array handler should not be notified after pushing object to new array"); }); test("add and remove observer array handler with chain observes", function() { objectA.addObservesHandler(objectA.arrayHandler, '*arrayValue.[]'); objectA.arrayValue.pushObject(SC.Object.create()); ok(objectA.arrayHandlerNotifiedCount > 0, "observes array handler should be notified aftering pushing object to array"); objectA.arrayHandlerNotifiedCount = 0; objectA.removeObservesHandler(objectA.arrayHandler, '*arrayValue.[]'); objectA.arrayValue.pushObject(SC.Object.create()); equals(objectA.arrayHandlerNotifiedCount, 0, "observes array handler should not be notified of push after removing observes handler"); objectA.set('arrayValue', []); equals(objectA.arrayHandlerNotifiedCount, 0, "observes array handler should not be notified of new array after removing observes handler"); objectA.addObservesHandler(objectA.arrayHandler, '*arrayValue.[]'); objectA.set('arrayValue', []); ok(objectA.arrayHandlerNotifiedCount > 0, "observes array handler should be notified after assigning new array"); objectA.arrayValue.pushObject(SC.Object.create()); ok(objectA.arrayHandlerNotifiedCount > 0, "observes array handler should be notified after pushing object to new array"); });