1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 // Author: Richard Klancer <rpk@pobox.com>
  8 
  9 /*globals module, test, ok, equals, expect */
 10 
 11 module("Problematic SC.ObserverSet.getMethods() removal", {
 12   setup: function () {
 13     SC.LOG_OBSERVERS = YES;
 14   },
 15   teardown: function () {
 16     SC.LOG_OBSERVERS = NO;
 17   }
 18 });
 19 
 20 // This test succeeds using master up to commit 5826c745874f903f1e4765e5a2bcb5244ff72113 and using the 1-4-stable HEAD
 21 // This test fails using subsequent master commit c78e1bf25087a3ebd553dbc513923e0d32d09f7b
 22 // It triggers an error at line 987 of frameworks/system/runtime/mixins/observable.js (in the method
 23 // SC.Observable._notifyPropertyObservers()) as of current master commit, 789fe805cd08976b7aab1c346c71cf22b78b7285
 24 
 25 test("Observers that remove themselves should fire at least once, and shouldn't cause an error", function () {
 26   expect(1);
 27 
 28   var observed = SC.Object.create({
 29     key: 'val'
 30   });
 31 
 32   var observer1, observer2;
 33   var observerFired = NO;
 34 
 35   function removeObservers() {
 36     observed.removeObserver('key', observer1);
 37     observed.removeObserver('key', observer2);
 38   }
 39 
 40   observer1 = function () {
 41     observerFired = YES;
 42     removeObservers();
 43   };
 44   observer2 = function () {
 45     observerFired = YES;
 46     removeObservers();
 47   };
 48 
 49   observed.addObserver('key', observer1);
 50   observed.addObserver('key', observer2);
 51 
 52   observed.set('key', 'newval');
 53 
 54   ok(observerFired, "At least one observer should have fired.");
 55 });
 56 
 57 /**
 58   This test highlights a problem with SC.ObserverSet.  It would clear out the
 59   method tracking on the target tracking hash, but when the number of methods
 60   tracked went to zero, it never cleared out the target.
 61 
 62   This is bad if a core object is bound to repeatedly by dynamically created
 63   objects.  Each new object tracking creates a new hash in the observer set
 64   that is never reclaimed.
 65 
 66   Here's an example that would create 3,000 empty hashes in
 67   coreOb._kvo_observers_key._members:
 68 
 69     obs = [];
 70     window.coreOb = SC.Object.create({ key: '1' });
 71     SC.run(function() {
 72       for (var i = 2999; i >= 0; i--) {
 73         obs.push(SC.Object.create({ myKeyBinding: SC.Binding.from('coreOb.key') }));
 74       }
 75     });
 76 
 77     SC.run(function() {
 78       for (var i = obs.length - 1; i >= 0; i--) {
 79         obs.pop().destroy();
 80       }
 81     });
 82 
 83 
 84   The fix is to have SC.ObserverSet remove the target hash when the number of
 85   methods for the target goes to zero.
 86 */
 87 test("Removing all the methods on a target should clear the internal tracking of that target", function() {
 88   var methodGuid1, methodGuid2,
 89     target = {},
 90     method1 = function () { }, method2 = function () { },
 91     observerSet,
 92     targetGuid;
 93 
 94   observerSet = SC.ObserverSet.create();
 95 
 96   targetGuid = SC.guidFor(target);
 97   methodGuid1 = SC.guidFor(method1);
 98   methodGuid2 = SC.guidFor(method2);
 99 
100   equals(observerSet.members.length, 0, "The ObserverSet should have a members length of");
101 
102   observerSet.add(target, method1);
103   observerSet.add(target, method2);
104   equals(observerSet.members.length, 2, "The ObserverSet should have a members length of");
105   ok(observerSet._members[targetGuid], "The ObserverSet should be tracking methods for the target: %@".fmt(targetGuid));
106   ok(!SC.none(observerSet._members[targetGuid][methodGuid1]), "The ObserverSet should be tracking method: %@ for the target: %@".fmt(methodGuid1, targetGuid));
107   ok(!SC.none(observerSet._members[targetGuid][methodGuid2]), "The ObserverSet should be tracking method: %@ for the target: %@".fmt(methodGuid2, targetGuid));
108 
109   observerSet.remove(target, method1);
110   equals(observerSet.members.length, 1, "The ObserverSet should have a members length of");
111   ok(observerSet._members[targetGuid], "The ObserverSet should be tracking methods for the target: %@".fmt(targetGuid));
112   ok(SC.none(observerSet._members[targetGuid][methodGuid1]), "The ObserverSet should not be tracking method: %@ for the target: %@".fmt(methodGuid1, targetGuid));
113   ok(!SC.none(observerSet._members[targetGuid][methodGuid2]), "The ObserverSet should be tracking method: %@ for the target: %@".fmt(methodGuid2, targetGuid));
114 
115   observerSet.remove(target, method2);
116   equals(observerSet.members.length, 0, "The ObserverSet should have a members length of");
117   ok(!observerSet._members[targetGuid], "The ObserverSet should not be tracking methods for the target: %@".fmt(targetGuid));
118 });
119 
120 test("Observers should pass the context along to their method", function () {
121   var target = {},
122     method = function (passedContext) {
123       equals(passedContext, originalContext, "The observer function should receive the context");
124     },
125     observerSet,
126     originalContext = "ABCD";
127 
128   observerSet = SC.ObserverSet.create();
129   observerSet.add(target, method, originalContext);
130   observerSet.invokeMethods();
131 });
132