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