Updated Branches: refs/heads/master d30179b30 -> c6917ee59
[all] Separate Channel into sticky and non-sticky versions - This is a retry of aa15ac60d6cbabae83f619880a3a6e8be14817ce. - It fixes forgetting to update channel.fired -> channel.state == 2 in iOS/exec.js - It removes an extra ) in android/platform.js Project: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/commit/c6917ee5 Tree: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/tree/c6917ee5 Diff: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/diff/c6917ee5 Branch: refs/heads/master Commit: c6917ee59031ec2cb11dc46d73f3e7b7a8a1cd34 Parents: d30179b Author: Andrew Grieve <agri...@chromium.org> Authored: Tue Sep 18 11:39:58 2012 -0400 Committer: Andrew Grieve <agri...@chromium.org> Committed: Tue Sep 18 11:39:58 2012 -0400 ---------------------------------------------------------------------- lib/android/platform.js | 2 +- lib/bada/plugin/bada/device.js | 2 +- lib/common/channel.js | 166 ++++++++--------- lib/common/plugin/device.js | 2 +- lib/common/plugin/network.js | 2 +- lib/cordova.js | 13 +- lib/ios/exec.js | 2 +- lib/scripts/bootstrap.js | 2 +- lib/tizen/plugin/tizen/Device.js | 2 +- test/test.channel.js | 327 +++++++++++++++++++-------------- 10 files changed, 280 insertions(+), 240 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/android/platform.js ---------------------------------------------------------------------- diff --git a/lib/android/platform.js b/lib/android/platform.js index af5ab4f..b2cde22 100644 --- a/lib/android/platform.js +++ b/lib/android/platform.js @@ -32,7 +32,7 @@ module.exports = { // If we just attached the first handler or detached the last handler, // let native know we need to override the back button. exec(null, null, "App", "overrideBackbutton", [this.numHandlers == 1]); - }); + }; // Add hardware MENU and SEARCH button handlers cordova.addDocumentEventHandler('menubutton'); http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/bada/plugin/bada/device.js ---------------------------------------------------------------------- diff --git a/lib/bada/plugin/bada/device.js b/lib/bada/plugin/bada/device.js index 3d34166..b1da422 100644 --- a/lib/bada/plugin/bada/device.js +++ b/lib/bada/plugin/bada/device.js @@ -34,7 +34,7 @@ function Device() { var me = this; - channel.onCordovaReady.subscribeOnce(function() { + channel.onCordovaReady.subscribe(function() { me.getDeviceInfo(function (device) { me.platform = device.platform; me.version = device.version; http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/common/channel.js ---------------------------------------------------------------------- diff --git a/lib/common/channel.js b/lib/common/channel.js index 21f0e6e..1ad7227 100644 --- a/lib/common/channel.js +++ b/lib/common/channel.js @@ -25,19 +25,22 @@ var utils = require('cordova/utils'), /** * Custom pub-sub "channel" that can have functions subscribed to it * This object is used to define and control firing of events for - * cordova initialization. + * cordova initialization, as well as for custom events thereafter. * * The order of events during page load and Cordova startup is as follows: * - * onDOMContentLoaded Internal event that is received when the web page is loaded and parsed. - * onNativeReady Internal event that indicates the Cordova native side is ready. - * onCordovaReady Internal event fired when all Cordova JavaScript objects have been created. - * onCordovaInfoReady Internal event fired when device properties are available. - * onCordovaConnectionReady Internal event fired when the connection property has been set. - * onDeviceReady User event fired to indicate that Cordova is ready - * onResume User event fired to indicate a start/resume lifecycle event - * onPause User event fired to indicate a pause lifecycle event - * onDestroy Internal event fired when app is being destroyed (User should use window.onunload event, not this one). + * onDOMContentLoaded* Internal event that is received when the web page is loaded and parsed. + * onNativeReady* Internal event that indicates the Cordova native side is ready. + * onCordovaReady* Internal event fired when all Cordova JavaScript objects have been created. + * onCordovaInfoReady* Internal event fired when device properties are available. + * onCordovaConnectionReady* Internal event fired when the connection property has been set. + * onDeviceReady* User event fired to indicate that Cordova is ready + * onResume User event fired to indicate a start/resume lifecycle event + * onPause User event fired to indicate a pause lifecycle event + * onDestroy* Internal event fired when app is being destroyed (User should use window.onunload event, not this one). + * + * The events marked with an * are sticky. Once they have fired, they will stay in the fired state. + * All listeners that subscribe after the event is fired will be executed right away. * * The only Cordova events that user code should register for are: * deviceready Cordova native code is initialized and Cordova APIs can be called from JavaScript @@ -60,12 +63,16 @@ var utils = require('cordova/utils'), * @constructor * @param type String the channel name */ -var Channel = function(type) { +var Channel = function(type, sticky) { this.type = type; + // Map of guid -> function. this.handlers = {}; + // 0 = Non-sticky, 1 = Sticky non-fired, 2 = Sticky fired. + this.state = sticky ? 1 : 0; + // Used in sticky mode to remember args passed to fire(). + this.fireArgs = null; + // Used by onHasSubscribersChange to know if there are any listeners. this.numHandlers = 0; - this.fired = false; - this.enabled = true; // Function that is called when the first listener is subscribed, or when // the last listener is unsubscribed. this.onHasSubscribersChange = null; @@ -73,22 +80,27 @@ var Channel = function(type) { channel = { /** * Calls the provided function only after all of the channels specified - * have been fired. + * have been fired. All channels must be sticky channels. */ - join: function (h, c) { - var i = c.length; - var len = i; - var f = function() { - if (!(--i)) h(); - }; + join: function(h, c) { + var len = c.length, + i = len, + f = function() { + if (!(--i)) h(); + }; for (var j=0; j<len; j++) { - !c[j].fired?c[j].subscribeOnce(f):i--; + if (c[j].state == 0) { + throw Error('Can only use join with sticky channels.') + } + c[j].subscribe(f); } - if (!i) h(); + if (!len) h(); + }, + create: function(type) { + return channel[type] = new Channel(type, false); }, - create: function (type, opts) { - channel[type] = new Channel(type, opts); - return channel[type]; + createSticky: function(type) { + return channel[type] = new Channel(type, true); }, /** @@ -106,13 +118,7 @@ var Channel = function(type) { */ waitForInitialization: function(feature) { if (feature) { - var c = null; - if (this[feature]) { - c = this[feature]; - } - else { - c = this.create(feature); - } + var c = channel[feature] || this.createSticky(feature); this.deviceReadyChannelsMap[feature] = c; this.deviceReadyChannelsArray.push(c); } @@ -132,7 +138,7 @@ var Channel = function(type) { }; function forceFunction(f) { - if (f === null || f === undefined || typeof f != 'function') throw "Function required as first argument!"; + if (typeof f != 'function') throw "Function required as first argument!"; } /** @@ -142,67 +148,46 @@ function forceFunction(f) { * and a guid that can be used to stop subscribing to the channel. * Returns the guid. */ -Channel.prototype.subscribe = function(f, c, g) { +Channel.prototype.subscribe = function(f, c) { // need a function to call forceFunction(f); + if (this.state == 2) { + f.apply(c || this, this.fireArgs); + return; + } - var func = f; + var func = f, + guid = f.observer_guid; if (typeof c == "object") { func = utils.close(c, f); } - g = g || func.observer_guid || f.observer_guid; - if (!g) { + if (!guid) { // first time any channel has seen this subscriber - g = nextGuid++; + guid = '' + nextGuid++; } - func.observer_guid = g; - f.observer_guid = g; + func.observer_guid = guid; + f.observer_guid = guid; // Don't add the same handler more than once. - if (!this.handlers[g]) { - this.handlers[g] = func; + if (!this.handlers[guid]) { + this.handlers[guid] = func; this.numHandlers++; if (this.numHandlers == 1) { this.onHasSubscribersChange && this.onHasSubscribersChange(); } - if (this.fired) func.apply(this, this.fireArgs); } - return g; -}; - -/** - * Like subscribe but the function is only called once and then it - * auto-unsubscribes itself. - */ -Channel.prototype.subscribeOnce = function(f, c) { - // need a function to call - forceFunction(f); - - var g = null; - var _this = this; - if (this.fired) { - f.apply(c || null, this.fireArgs); - } else { - g = this.subscribe(function() { - _this.unsubscribe(g); - f.apply(c || null, arguments); - }); - f.observer_guid = g; - } - return g; }; /** * Unsubscribes the function with the given guid from the channel. */ -Channel.prototype.unsubscribe = function(g) { +Channel.prototype.unsubscribe = function(f) { // need a function to unsubscribe - if (g === null || g === undefined) { throw "You must pass _something_ into Channel.unsubscribe"; } + forceFunction(f); - if (typeof g == 'function') { g = g.observer_guid; } - var handler = this.handlers[g]; + var guid = f.observer_guid, + handler = this.handlers[guid]; if (handler) { - if (handler.observer_guid) handler.observer_guid=null; - delete this.handlers[g]; + delete this.handlers[guid]; this.numHandlers--; if (this.numHandlers == 0) { this.onHasSubscribersChange && this.onHasSubscribersChange(); @@ -214,10 +199,14 @@ Channel.prototype.unsubscribe = function(g) { * Calls all functions subscribed to this channel. */ Channel.prototype.fire = function(e) { - if (this.enabled) { - var fail = false; - this.fired = true; - this.fireArgs = arguments; + var fail = false, + fireArgs = Array.prototype.slice.call(arguments); + // Apply stickiness. + if (this.state == 1) { + this.state = 2; + this.fireArgs = fireArgs; + } + if (this.numHandlers) { // Copy the values first so that it is safe to modify it from within // callbacks. var toCall = []; @@ -225,33 +214,36 @@ Channel.prototype.fire = function(e) { toCall.push(this.handlers[item]); } for (var i = 0; i < toCall.length; ++i) { - var rv = (toCall[i].apply(this, arguments)===false); - fail = fail || rv; + toCall[i].apply(this, fireArgs); + } + if (this.state == 2 && this.numHandlers) { + this.numHandlers = 0; + this.handlers = {}; + this.onHasSubscribersChange && this.onHasSubscribersChange(); } - return !fail; } - return true; }; + // defining them here so they are ready super fast! // DOM event that is received when the web page is loaded and parsed. -channel.create('onDOMContentLoaded'); +channel.createSticky('onDOMContentLoaded'); // Event to indicate the Cordova native side is ready. -channel.create('onNativeReady'); +channel.createSticky('onNativeReady'); // Event to indicate that all Cordova JavaScript objects have been created // and it's time to run plugin constructors. -channel.create('onCordovaReady'); +channel.createSticky('onCordovaReady'); // Event to indicate that device properties are available -channel.create('onCordovaInfoReady'); +channel.createSticky('onCordovaInfoReady'); // Event to indicate that the connection property has been set. -channel.create('onCordovaConnectionReady'); +channel.createSticky('onCordovaConnectionReady'); // Event to indicate that Cordova is ready -channel.create('onDeviceReady'); +channel.createSticky('onDeviceReady'); // Event to indicate a resume lifecycle event channel.create('onResume'); @@ -260,7 +252,7 @@ channel.create('onResume'); channel.create('onPause'); // Event to indicate a destroy lifecycle event -channel.create('onDestroy'); +channel.createSticky('onDestroy'); // Channels that must fire before "deviceready" is fired. channel.waitForInitialization('onCordovaReady'); http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/common/plugin/device.js ---------------------------------------------------------------------- diff --git a/lib/common/plugin/device.js b/lib/common/plugin/device.js index 3477ff8..905bfe1 100644 --- a/lib/common/plugin/device.js +++ b/lib/common/plugin/device.js @@ -41,7 +41,7 @@ function Device() { var me = this; - channel.onCordovaReady.subscribeOnce(function() { + channel.onCordovaReady.subscribe(function() { me.getInfo(function(info) { me.available = true; me.platform = info.platform; http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/common/plugin/network.js ---------------------------------------------------------------------- diff --git a/lib/common/plugin/network.js b/lib/common/plugin/network.js index 637ea89..adaba5a 100644 --- a/lib/common/plugin/network.js +++ b/lib/common/plugin/network.js @@ -41,7 +41,7 @@ var NetworkConnection = function () { var me = this; - channel.onCordovaReady.subscribeOnce(function() { + channel.onCordovaReady.subscribe(function() { me.getInfo(function (info) { me.type = info; if (info === "none") { http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/cordova.js ---------------------------------------------------------------------- diff --git a/lib/cordova.js b/lib/cordova.js index e1650fa..fc4a744 100644 --- a/lib/cordova.js +++ b/lib/cordova.js @@ -50,11 +50,7 @@ var documentEventHandlers = {}, document.addEventListener = function(evt, handler, capture) { var e = evt.toLowerCase(); if (typeof documentEventHandlers[e] != 'undefined') { - if (evt === 'deviceready') { - documentEventHandlers[e].subscribeOnce(handler); - } else { - documentEventHandlers[e].subscribe(handler); - } + documentEventHandlers[e].subscribe(handler); } else { m_document_addEventListener.call(document, evt, handler, capture); } @@ -117,6 +113,9 @@ var cordova = { addWindowEventHandler:function(event) { return (windowEventHandlers[event] = channel.create(event)); }, + addStickyDocumentEventHandler:function(event) { + return (documentEventHandlers[event] = channel.createSticky(event)); + }, addDocumentEventHandler:function(event) { return (documentEventHandlers[event] = channel.create(event)); }, @@ -234,7 +233,7 @@ var cordova = { } }, addConstructor: function(func) { - channel.onCordovaReady.subscribeOnce(function() { + channel.onCordovaReady.subscribe(function() { try { func(); } catch(e) { @@ -247,6 +246,6 @@ var cordova = { // Register pause, resume and deviceready channels as events on document. channel.onPause = cordova.addDocumentEventHandler('pause'); channel.onResume = cordova.addDocumentEventHandler('resume'); -channel.onDeviceReady = cordova.addDocumentEventHandler('deviceready'); +channel.onDeviceReady = cordova.addStickyDocumentEventHandler('deviceready'); module.exports = cordova; http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/ios/exec.js ---------------------------------------------------------------------- diff --git a/lib/ios/exec.js b/lib/ios/exec.js index 95b49e8..5d91540 100644 --- a/lib/ios/exec.js +++ b/lib/ios/exec.js @@ -65,7 +65,7 @@ function shouldBundleCommandJson() { } function iOSExec() { - if (!channel.onCordovaReady.fired) { + if (channel.onCordovaReady.state != 2) { utils.alert("ERROR: Attempting to call cordova.exec()" + " before 'deviceready'. Ignoring."); return; http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/scripts/bootstrap.js ---------------------------------------------------------------------- diff --git a/lib/scripts/bootstrap.js b/lib/scripts/bootstrap.js index 2bf8075..6aa08af 100644 --- a/lib/scripts/bootstrap.js +++ b/lib/scripts/bootstrap.js @@ -69,7 +69,7 @@ }; // boot up once native side is ready - channel.onNativeReady.subscribeOnce(_self.boot); + channel.onNativeReady.subscribe(_self.boot); // _nativeReady is global variable that the native side can set // to signify that the native code is ready. It is a global since http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/tizen/plugin/tizen/Device.js ---------------------------------------------------------------------- diff --git a/lib/tizen/plugin/tizen/Device.js b/lib/tizen/plugin/tizen/Device.js index 908f05d..c5532fb 100644 --- a/lib/tizen/plugin/tizen/Device.js +++ b/lib/tizen/plugin/tizen/Device.js @@ -45,7 +45,7 @@ function Device() { console.log("error initializing cordova: " + error); } - channel.onCordovaReady.subscribeOnce(function() { + channel.onCordovaReady.subscribe(function() { me.getDeviceInfo(onSuccessCallback, onErrorCallback); }); } http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/test/test.channel.js ---------------------------------------------------------------------- diff --git a/test/test.channel.js b/test/test.channel.js index e71f993..cd14d38 100644 --- a/test/test.channel.js +++ b/test/test.channel.js @@ -21,266 +21,315 @@ describe("channel", function () { var channel = require('cordova/channel'), - c; - + multiChannel, + stickyChannel; + + function callCount(spy) { + return spy.argsForCall.length; + } + function expectCallCount(spy, count) { + expect(callCount(spy)).toEqual(count); + } beforeEach(function() { - c = channel.create('masterexploder'); + multiChannel = channel.create('multiChannel'); + stickyChannel = channel.createSticky('stickyChannel'); }); describe("subscribe method", function() { it("should throw an exception if no function is provided", function() { expect(function() { - c.subscribe(); + multiChannel.subscribe(); }).toThrow(); expect(function() { - c.subscribe(null); + multiChannel.subscribe(null); }).toThrow(); expect(function() { - c.subscribe(undefined); + multiChannel.subscribe(undefined); }).toThrow(); expect(function() { - c.subscribe({apply:function(){},call:function(){}}); + multiChannel.subscribe({apply:function(){},call:function(){}}); }).toThrow(); }); it("should not change number of handlers if no function is provided", function() { - var initialLength = c.numHandlers; + var initialLength = multiChannel.numHandlers; try { - c.subscribe(); + multiChannel.subscribe(); } catch(e) {} - expect(c.numHandlers).toEqual(initialLength); + expect(multiChannel.numHandlers).toEqual(initialLength); try { - c.subscribe(null); + multiChannel.subscribe(null); } catch(e) {} - expect(c.numHandlers).toEqual(initialLength); + expect(multiChannel.numHandlers).toEqual(initialLength); }); it("should not change number of handlers when subscribing same function multiple times", function() { - var initialLength = c.numHandlers; - var handler = function(){}; - - c.subscribe(handler); - c.subscribe(handler); - c.subscribe(handler); - - expect(c.numHandlers).toEqual(initialLength+1); - }); - it("should be able to use the same function with multiple channels.", function() { - var c2 = channel.create('jables'); var handler = function(){}; - c.subscribe(handler); - c2.subscribe(handler); + multiChannel.subscribe(handler); + multiChannel.subscribe(handler); + stickyChannel.subscribe(handler); + stickyChannel.subscribe(handler); - expect(c.numHandlers).toEqual(1); - expect(c2.numHandlers).toEqual(1); + expect(multiChannel.numHandlers).toEqual(1); + expect(stickyChannel.numHandlers).toEqual(1); }); }); describe("unsubscribe method", function() { it("should throw an exception if passed in null or undefined", function() { expect(function() { - c.unsubscribe(); + multiChannel.unsubscribe(); }).toThrow(); expect(function() { - c.unsubscribe(null); + multiChannel.unsubscribe(null); }).toThrow(); }); it("should not decrement numHandlers if unsubscribing something that does not exist", function() { - var initialLength = c.numHandlers; - c.unsubscribe('blah'); - expect(c.numHandlers).toEqual(initialLength); - c.unsubscribe(2); - expect(c.numHandlers).toEqual(initialLength); - c.unsubscribe({balls:false}); - expect(c.numHandlers).toEqual(initialLength); + multiChannel.subscribe(function() {}); + multiChannel.unsubscribe(function() {}); + expect(multiChannel.numHandlers).toEqual(1); }); it("should change the handlers length appropriately", function() { var firstHandler = function() {}; var secondHandler = function() {}; var thirdHandler = function() {}; - c.subscribe(firstHandler); - c.subscribe(secondHandler); - c.subscribe(thirdHandler); - - var initialLength = c.numHandlers; + multiChannel.subscribe(firstHandler); + multiChannel.subscribe(secondHandler); + multiChannel.subscribe(thirdHandler); + expect(multiChannel.numHandlers).toEqual(3); - c.unsubscribe(thirdHandler); + multiChannel.unsubscribe(thirdHandler); + expect(multiChannel.numHandlers).toEqual(2); - expect(c.numHandlers).toEqual(initialLength - 1); + multiChannel.unsubscribe(firstHandler); + multiChannel.unsubscribe(secondHandler); - c.unsubscribe(firstHandler); - c.unsubscribe(secondHandler); - - expect(c.numHandlers).toEqual(0); + expect(multiChannel.numHandlers).toEqual(0); }); it("should not decrement handlers length more than once if unsubing a single handler", function() { var firstHandler = function(){}; - c.subscribe(firstHandler); + multiChannel.subscribe(firstHandler); - expect(c.numHandlers).toEqual(1); + expect(multiChannel.numHandlers).toEqual(1); - c.unsubscribe(firstHandler); - c.unsubscribe(firstHandler); - c.unsubscribe(firstHandler); - c.unsubscribe(firstHandler); + multiChannel.unsubscribe(firstHandler); + multiChannel.unsubscribe(firstHandler); + multiChannel.unsubscribe(firstHandler); + multiChannel.unsubscribe(firstHandler); - expect(c.numHandlers).toEqual(0); + expect(multiChannel.numHandlers).toEqual(0); }); it("should not unregister a function registered with a different handler", function() { var cHandler = function(){}; var c2Handler = function(){}; var c2 = channel.create('jables'); - c.subscribe(cHandler); + multiChannel.subscribe(cHandler); c2.subscribe(c2Handler); - expect(c.numHandlers).toEqual(1); + expect(multiChannel.numHandlers).toEqual(1); expect(c2.numHandlers).toEqual(1); - c.unsubscribe(c2Handler); + multiChannel.unsubscribe(c2Handler); c2.unsubscribe(cHandler); - expect(c.numHandlers).toEqual(1); + expect(multiChannel.numHandlers).toEqual(1); expect(c2.numHandlers).toEqual(1); }); - it("should be able to unsubscribe a subscribeOnce.", function() { - var handler = function(){}; - c.subscribeOnce(handler); - - expect(c.numHandlers).toEqual(1); - - c.unsubscribe(handler); - - expect(c.numHandlers).toEqual(0); - }); }); - describe("fire method", function() { + function commonFireTests(multi) { it("should fire all subscribed handlers", function() { + var testChannel = multi ? multiChannel : stickyChannel; var handler = jasmine.createSpy(); var anotherOne = jasmine.createSpy(); - c.subscribe(handler); - c.subscribe(anotherOne); + testChannel.subscribe(handler); + testChannel.subscribe(anotherOne); + + testChannel.fire(); - c.fire(); + expectCallCount(handler, 1); + expectCallCount(anotherOne, 1); + }); + it("should pass params to handlers", function() { + var testChannel = multi ? multiChannel : stickyChannel; + var handler = jasmine.createSpy(); + + testChannel.subscribe(handler); - expect(handler).toHaveBeenCalled(); - expect(anotherOne).toHaveBeenCalled(); + testChannel.fire(1, 2, 3); + expect(handler.argsForCall[0]).toEqual({0:1, 1:2, 2:3}); }); it("should not fire a handler that was unsubscribed", function() { + var testChannel = multi ? multiChannel : stickyChannel; var handler = jasmine.createSpy(); var anotherOne = jasmine.createSpy(); - c.subscribe(handler); - c.subscribe(anotherOne); - c.unsubscribe(handler); + testChannel.subscribe(handler); + testChannel.subscribe(anotherOne); + testChannel.unsubscribe(handler); - c.fire(); + testChannel.fire(); - expect(handler).not.toHaveBeenCalled(); - expect(anotherOne).toHaveBeenCalled(); + expectCallCount(handler, 0); + expectCallCount(anotherOne, 1); }); it("should not fire a handler more than once if it was subscribed more than once", function() { - var count = 0; - var handler = jasmine.createSpy().andCallFake(function() { count++; }); + var testChannel = multi ? multiChannel : stickyChannel; + var handler = jasmine.createSpy(); - c.subscribe(handler); - c.subscribe(handler); - c.subscribe(handler); + testChannel.subscribe(handler); + testChannel.subscribe(handler); + testChannel.subscribe(handler); - c.fire(); + testChannel.fire(); - expect(handler).toHaveBeenCalled(); - expect(count).toEqual(1); + expectCallCount(handler, 1); }); it("handler should be called when subscribed, removed, and subscribed again", function() { - var count = 0; - var handler = jasmine.createSpy().andCallFake(function() { count++; }); - - c.subscribe(handler); - c.unsubscribe(handler); - c.subscribe(handler); + var testChannel = multi ? multiChannel : stickyChannel; + var handler = jasmine.createSpy(); - c.fire(); + testChannel.subscribe(handler); + testChannel.unsubscribe(handler); + testChannel.subscribe(handler); - expect(handler).toHaveBeenCalled(); - expect(count).toEqual(1); + testChannel.fire(); + expectCallCount(handler, 1); + }); + it("should not prevent a callback from firing when it is removed during firing.", function() { + var testChannel = multi ? multiChannel : stickyChannel; + var handler = jasmine.createSpy().andCallFake(function() { testChannel.unsubscribe(handler2); }); + var handler2 = jasmine.createSpy(); + testChannel.subscribe(handler); + testChannel.subscribe(handler2); + testChannel.fire(); + expectCallCount(handler, 1); + expectCallCount(handler2, 1); }); + } + describe("fire method for sticky channels", function() { + commonFireTests(false); it("should instantly trigger the callback if the event has already been fired", function () { - var chan = channel.create("foo"), - before = jasmine.createSpy('before'), + var before = jasmine.createSpy('before'), after = jasmine.createSpy('after'); - chan.subscribe(before); - chan.fire(); - chan.subscribe(after); + stickyChannel.subscribe(before); + stickyChannel.fire(1, 2, 3); + stickyChannel.subscribe(after); - expect(before).toHaveBeenCalled(); - expect(after).toHaveBeenCalled(); + expectCallCount(before, 1); + expectCallCount(after, 1); + expect(after.argsForCall[0]).toEqual({0:1, 1:2, 2:3}); }); it("should instantly trigger the callback if the event is currently being fired.", function () { - var handler1 = jasmine.createSpy().andCallFake(function() { c.subscribe(handler2); }), + var handler1 = jasmine.createSpy().andCallFake(function() { stickyChannel.subscribe(handler2); }), handler2 = jasmine.createSpy().andCallFake(function(arg1) { expect(arg1).toEqual('foo');}); - c.subscribe(handler1); - c.fire('foo'); + stickyChannel.subscribe(handler1); + stickyChannel.fire('foo'); - expect(handler2).toHaveBeenCalled(); + expectCallCount(handler2, 1); + }); + it("should unregister all handlers after being fired.", function() { + var handler = jasmine.createSpy(); + stickyChannel.subscribe(handler); + stickyChannel.fire(); + stickyChannel.fire(); + expectCallCount(handler, 1); }); }); - describe("subscribeOnce method", function() { - it("should be unregistered after being fired.", function() { - var count = 0; - var handler = jasmine.createSpy().andCallFake(function() { count++; }); - c.subscribeOnce(handler); - c.fire(); - c.fire(); - expect(count).toEqual(1); + describe("fire method for multi channels", function() { + commonFireTests(true); + it("should not trigger the callback if the event has already been fired", function () { + var before = jasmine.createSpy('before'), + after = jasmine.createSpy('after'); + + multiChannel.subscribe(before); + multiChannel.fire(); + multiChannel.subscribe(after); + + expectCallCount(before, 1); + expectCallCount(after, 0); }); - it("should be safe to add listeners from within callback.", function() { - var count = 0; - var handler = jasmine.createSpy().andCallFake(function() { count++; c.subscribeOnce(handler2); }); - var handler2 = jasmine.createSpy().andCallFake(function() { count++; }); - c.subscribeOnce(handler); - c.fire(); - expect(count).toEqual(2); + it("should not trigger the callback if the event is currently being fired.", function () { + var handler1 = jasmine.createSpy().andCallFake(function() { multiChannel.subscribe(handler2); }), + handler2 = jasmine.createSpy(); + + multiChannel.subscribe(handler1); + multiChannel.fire(); + multiChannel.fire(); + + expectCallCount(handler1, 2); + expectCallCount(handler2, 1); }); - it("should not prevent a callback from firing when it is removed during firing.", function() { - var count = 0; - var handler = jasmine.createSpy().andCallFake(function() { count++; c.unsubscribe(handler2); }); - var handler2 = jasmine.createSpy().andCallFake(function() { count++; }); - c.subscribeOnce(handler); - c.subscribeOnce(handler2); - c.fire(); - expect(count).toEqual(2); + it("should not unregister handlers after being fired.", function() { + var handler = jasmine.createSpy(); + multiChannel.subscribe(handler); + multiChannel.fire(); + multiChannel.fire(); + expectCallCount(handler, 2); + }); + }); + describe("channel.join()", function() { + it("should be called when all functions start unfired", function() { + var handler = jasmine.createSpy(), + stickyChannel2 = channel.createSticky('stickyChannel'); + channel.join(handler, [stickyChannel, stickyChannel2]); + expectCallCount(handler, 0); + stickyChannel.fire(); + expectCallCount(handler, 0); + stickyChannel2.fire(); + expectCallCount(handler, 1); + }); + it("should be called when one functions start fired", function() { + var handler = jasmine.createSpy(), + stickyChannel2 = channel.createSticky('stickyChannel'); + stickyChannel.fire(); + channel.join(handler, [stickyChannel, stickyChannel2]); + expectCallCount(handler, 0); + stickyChannel2.fire(); + expectCallCount(handler, 1); + }); + it("should be called when all functions start fired", function() { + var handler = jasmine.createSpy(), + stickyChannel2 = channel.createSticky('stickyChannel'); + stickyChannel.fire(); + stickyChannel2.fire(); + channel.join(handler, [stickyChannel, stickyChannel2]); + expectCallCount(handler, 1); + }); + it("should throw if a channel is not sticky", function() { + expect(function() { + channel.join(function(){}, [stickyChannel, multiChannel]); + }).toThrow(); }); }); describe("onHasSubscribersChange", function() { it("should be called only when the first subscriber is added and last subscriber is removed.", function() { var handler = jasmine.createSpy().andCallFake(function() { - var callCount = handler.argsForCall.length; - if (callCount == 1) { + if (callCount(handler) == 1) { expect(this.numHandlers).toEqual(1); } else { expect(this.numHandlers).toEqual(0); } }); - c.onHasSubscribersChange = handler; + multiChannel.onHasSubscribersChange = handler; function foo1() {} function foo2() {} - c.subscribe(foo1); - c.subscribe(foo2); - c.unsubscribe(foo1); - c.unsubscribe(foo2); - expect(handler.argsForCall.length).toEqual(2); + multiChannel.subscribe(foo1); + multiChannel.subscribe(foo2); + multiChannel.unsubscribe(foo1); + multiChannel.unsubscribe(foo2); + expectCallCount(handler, 2); }); }); });