diff --git a/browser/components/loop/content/js/conversation.js b/browser/components/loop/content/js/conversation.js index f39698b7eab..eddf678e890 100644 --- a/browser/components/loop/content/js/conversation.js +++ b/browser/components/loop/content/js/conversation.js @@ -27,7 +27,11 @@ loop.conversation = (function(mozL10n) { * in progress, and hence, which view to display. */ var AppControllerView = React.createClass({displayName: "AppControllerView", - mixins: [Backbone.Events, sharedMixins.WindowCloseMixin], + mixins: [ + Backbone.Events, + loop.store.StoreMixin("conversationAppStore"), + sharedMixins.WindowCloseMixin + ], propTypes: { // XXX Old types required for incoming call view. @@ -37,26 +41,12 @@ loop.conversation = (function(mozL10n) { sdk: React.PropTypes.object.isRequired, // XXX New types for flux style - conversationAppStore: React.PropTypes.instanceOf( - loop.store.ConversationAppStore).isRequired, - conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore) - .isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, roomStore: React.PropTypes.instanceOf(loop.store.RoomStore) }, getInitialState: function() { - return this.props.conversationAppStore.getStoreState(); - }, - - componentWillMount: function() { - this.listenTo(this.props.conversationAppStore, "change", function() { - this.setState(this.props.conversationAppStore.getStoreState()); - }, this); - }, - - componentWillUnmount: function() { - this.stopListening(this.props.conversationAppStore); + return this.getStoreState(); }, render: function() { @@ -67,12 +57,11 @@ loop.conversation = (function(mozL10n) { conversation: this.props.conversation, sdk: this.props.sdk, isDesktop: true, - conversationAppStore: this.props.conversationAppStore} + conversationAppStore: this.getStore()} )); } case "outgoing": { return (React.createElement(OutgoingConversationView, { - store: this.props.conversationStore, dispatcher: this.props.dispatcher} )); } @@ -161,7 +150,11 @@ loop.conversation = (function(mozL10n) { feedbackClient: feedbackClient }); - loop.store.StoreMixin.register({feedbackStore: feedbackStore}); + loop.store.StoreMixin.register({ + conversationAppStore: conversationAppStore, + conversationStore: conversationStore, + feedbackStore: feedbackStore, + }); // XXX Old class creation for the incoming conversation view, whilst // we transition across (bug 1072323). @@ -191,9 +184,7 @@ loop.conversation = (function(mozL10n) { }); React.render(React.createElement(AppControllerView, { - conversationAppStore: conversationAppStore, roomStore: roomStore, - conversationStore: conversationStore, client: client, conversation: conversation, dispatcher: dispatcher, diff --git a/browser/components/loop/content/js/conversation.jsx b/browser/components/loop/content/js/conversation.jsx index d6cf8624588..8936e708fb3 100644 --- a/browser/components/loop/content/js/conversation.jsx +++ b/browser/components/loop/content/js/conversation.jsx @@ -27,7 +27,11 @@ loop.conversation = (function(mozL10n) { * in progress, and hence, which view to display. */ var AppControllerView = React.createClass({ - mixins: [Backbone.Events, sharedMixins.WindowCloseMixin], + mixins: [ + Backbone.Events, + loop.store.StoreMixin("conversationAppStore"), + sharedMixins.WindowCloseMixin + ], propTypes: { // XXX Old types required for incoming call view. @@ -37,26 +41,12 @@ loop.conversation = (function(mozL10n) { sdk: React.PropTypes.object.isRequired, // XXX New types for flux style - conversationAppStore: React.PropTypes.instanceOf( - loop.store.ConversationAppStore).isRequired, - conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore) - .isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, roomStore: React.PropTypes.instanceOf(loop.store.RoomStore) }, getInitialState: function() { - return this.props.conversationAppStore.getStoreState(); - }, - - componentWillMount: function() { - this.listenTo(this.props.conversationAppStore, "change", function() { - this.setState(this.props.conversationAppStore.getStoreState()); - }, this); - }, - - componentWillUnmount: function() { - this.stopListening(this.props.conversationAppStore); + return this.getStoreState(); }, render: function() { @@ -67,12 +57,11 @@ loop.conversation = (function(mozL10n) { conversation={this.props.conversation} sdk={this.props.sdk} isDesktop={true} - conversationAppStore={this.props.conversationAppStore} + conversationAppStore={this.getStore()} />); } case "outgoing": { return (); } @@ -161,7 +150,11 @@ loop.conversation = (function(mozL10n) { feedbackClient: feedbackClient }); - loop.store.StoreMixin.register({feedbackStore: feedbackStore}); + loop.store.StoreMixin.register({ + conversationAppStore: conversationAppStore, + conversationStore: conversationStore, + feedbackStore: feedbackStore, + }); // XXX Old class creation for the incoming conversation view, whilst // we transition across (bug 1072323). @@ -191,9 +184,7 @@ loop.conversation = (function(mozL10n) { }); React.render(); } diff --git a/browser/components/loop/content/shared/js/store.js b/browser/components/loop/content/shared/js/store.js index 4f2d8125e60..165ffa4d84d 100644 --- a/browser/components/loop/content/shared/js/store.js +++ b/browser/components/loop/content/shared/js/store.js @@ -136,7 +136,7 @@ loop.store.StoreMixin = (function() { }, this); }, componentWillUnmount: function() { - this.getStore().off("change"); + this.getStore().off("change", null, this); } }; } diff --git a/browser/components/loop/test/desktop-local/conversationViews_test.js b/browser/components/loop/test/desktop-local/conversationViews_test.js index 36245f95a1c..7dc1144f3de 100644 --- a/browser/components/loop/test/desktop-local/conversationViews_test.js +++ b/browser/components/loop/test/desktop-local/conversationViews_test.js @@ -283,7 +283,6 @@ describe("loop.conversationViews", function () { return TestUtils.renderIntoDocument( React.createElement(loop.conversationViews.CallFailedView, { dispatcher: dispatcher, - store: store, contact: options.contact })); } @@ -294,6 +293,10 @@ describe("loop.conversationViews", function () { mozLoop: navigator.mozLoop, sdkDriver: {} }); + loop.store.StoreMixin.register({ + conversationStore: store + }); + fakeAudio = { play: sinon.spy(), pause: sinon.spy(), @@ -582,7 +585,6 @@ describe("loop.conversationViews", function () { return TestUtils.renderIntoDocument( React.createElement(loop.conversationViews.OutgoingConversationView, { dispatcher: dispatcher, - store: store })); } @@ -592,6 +594,10 @@ describe("loop.conversationViews", function () { mozLoop: fakeMozLoop, sdkDriver: {} }); + loop.store.StoreMixin.register({ + conversationStore: store + }); + feedbackStore = new loop.store.FeedbackStore(dispatcher, { feedbackClient: {} }); diff --git a/browser/components/loop/test/desktop-local/conversation_test.js b/browser/components/loop/test/desktop-local/conversation_test.js index 5a5780ec71b..28e808b341c 100644 --- a/browser/components/loop/test/desktop-local/conversation_test.js +++ b/browser/components/loop/test/desktop-local/conversation_test.js @@ -139,8 +139,6 @@ describe("loop.conversation", function() { conversation: conversation, roomStore: roomStore, sdk: {}, - conversationStore: conversationStore, - conversationAppStore: conversationAppStore, dispatcher: dispatcher, mozLoop: navigator.mozLoop })); @@ -176,6 +174,11 @@ describe("loop.conversation", function() { dispatcher: dispatcher, mozLoop: navigator.mozLoop }); + + loop.store.StoreMixin.register({ + conversationAppStore: conversationAppStore, + conversationStore: conversationStore + }); }); afterEach(function() { diff --git a/browser/components/loop/test/shared/store_test.js b/browser/components/loop/test/shared/store_test.js index 91726c4468b..14f6112ffd8 100644 --- a/browser/components/loop/test/shared/store_test.js +++ b/browser/components/loop/test/shared/store_test.js @@ -4,157 +4,202 @@ var expect = chai.expect; -describe("loop.store.createStore", function () { +describe("loop.store", function () { "use strict"; + var dispatcher; var sandbox; var sharedActions = loop.shared.actions; beforeEach(function() { sandbox = sinon.sandbox.create(); + dispatcher = new loop.Dispatcher(); }); afterEach(function() { sandbox.restore(); }); - it("should create a store constructor", function() { - expect(loop.store.createStore({})).to.be.a("function"); + describe("loop.store.createStore", function() { + it("should create a store constructor", function() { + expect(loop.store.createStore({})).to.be.a("function"); + }); + + it("should implement Backbone.Events", function() { + expect(loop.store.createStore({}).prototype).to.include.keys(["on", "off"]); + }); + + describe("Store API", function() { + describe("#constructor", function() { + it("should require a dispatcher", function() { + var TestStore = loop.store.createStore({}); + expect(function() { + new TestStore(); + }).to.Throw(/required dispatcher/); + }); + + it("should call initialize() when constructed, if defined", function() { + var initialize = sandbox.spy(); + var TestStore = loop.store.createStore({initialize: initialize}); + var options = {fake: true}; + + new TestStore(dispatcher, options); + + sinon.assert.calledOnce(initialize); + sinon.assert.calledWithExactly(initialize, options); + }); + + it("should register actions", function() { + sandbox.stub(dispatcher, "register"); + var TestStore = loop.store.createStore({ + actions: ["a", "b"], + a: function() {}, + b: function() {} + }); + + var store = new TestStore(dispatcher); + + sinon.assert.calledOnce(dispatcher.register); + sinon.assert.calledWithExactly(dispatcher.register, store, ["a", "b"]); + }); + + it("should throw if a registered action isn't implemented", function() { + var TestStore = loop.store.createStore({ + actions: ["a", "b"], + a: function() {} // missing b + }); + + expect(function() { + new TestStore(dispatcher); + }).to.Throw(/should implement an action handler for b/); + }); + }); + + describe("#getInitialStoreState", function() { + it("should set initial store state if provided", function() { + var TestStore = loop.store.createStore({ + getInitialStoreState: function() { + return {foo: "bar"}; + } + }); + + var store = new TestStore(dispatcher); + + expect(store.getStoreState()).eql({foo: "bar"}); + }); + }); + + describe("#dispatchAction", function() { + it("should dispatch an action", function() { + sandbox.stub(dispatcher, "dispatch"); + var TestStore = loop.store.createStore({}); + var TestAction = sharedActions.Action.define("TestAction", {}); + var action = new TestAction({}); + var store = new TestStore(dispatcher); + + store.dispatchAction(action); + + sinon.assert.calledOnce(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, action); + }); + }); + + describe("#getStoreState", function() { + var TestStore = loop.store.createStore({}); + var store; + + beforeEach(function() { + store = new TestStore(dispatcher); + store.setStoreState({foo: "bar", bar: "baz"}); + }); + + it("should retrieve the whole state by default", function() { + expect(store.getStoreState()).eql({foo: "bar", bar: "baz"}); + }); + + it("should retrieve a given property state", function() { + expect(store.getStoreState("bar")).eql("baz"); + }); + }); + + describe("#setStoreState", function() { + var TestStore = loop.store.createStore({}); + var store; + + beforeEach(function() { + store = new TestStore(dispatcher); + store.setStoreState({foo: "bar"}); + }); + + it("should update store state data", function() { + store.setStoreState({foo: "baz"}); + + expect(store.getStoreState("foo")).eql("baz"); + }); + + it("should trigger a `change` event", function(done) { + store.once("change", function() { + done(); + }); + + store.setStoreState({foo: "baz"}); + }); + + it("should trigger a `change:` event", function(done) { + store.once("change:foo", function() { + done(); + }); + + store.setStoreState({foo: "baz"}); + }); + }); + }); }); - it("should implement Backbone.Events", function() { - expect(loop.store.createStore({}).prototype).to.include.keys(["on", "off"]) - }); - - describe("Store API", function() { - var dispatcher; + describe("loop.store.StoreMixin", function() { + var view1, view2, store, storeClass, testComp; beforeEach(function() { - dispatcher = new loop.Dispatcher(); + storeClass = loop.store.createStore({}); + + store = new storeClass(dispatcher); + + loop.store.StoreMixin.register({store: store}); + + testComp = React.createClass({ + mixins: [loop.store.StoreMixin("store")], + render: function() { + return React.DOM.div(); + } + }); + + view1 = TestUtils.renderIntoDocument(React.createElement(testComp)); }); - describe("#constructor", function() { - it("should require a dispatcher", function() { - var TestStore = loop.store.createStore({}); - expect(function() { - new TestStore(); - }).to.Throw(/required dispatcher/); - }); + it("should update the state when the store changes", function() { + store.setStoreState({test: true}); - it("should call initialize() when constructed, if defined", function() { - var initialize = sandbox.spy(); - var TestStore = loop.store.createStore({initialize: initialize}); - var options = {fake: true}; - - new TestStore(dispatcher, options); - - sinon.assert.calledOnce(initialize); - sinon.assert.calledWithExactly(initialize, options); - }); - - it("should register actions", function() { - sandbox.stub(dispatcher, "register"); - var TestStore = loop.store.createStore({ - actions: ["a", "b"], - a: function() {}, - b: function() {} - }); - - var store = new TestStore(dispatcher); - - sinon.assert.calledOnce(dispatcher.register); - sinon.assert.calledWithExactly(dispatcher.register, store, ["a", "b"]); - }); - - it("should throw if a registered action isn't implemented", function() { - var TestStore = loop.store.createStore({ - actions: ["a", "b"], - a: function() {} // missing b - }); - - expect(function() { - new TestStore(dispatcher); - }).to.Throw(/should implement an action handler for b/); - }); + expect(view1.state).eql({test: true}); }); - describe("#getInitialStoreState", function() { - it("should set initial store state if provided", function() { - var TestStore = loop.store.createStore({ - getInitialStoreState: function() { - return {foo: "bar"}; - } - }); + it("should stop listening to state changes", function() { + // There's no easy way in TestUtils to unmount, so simulate it. + view1.componentWillUnmount(); - var store = new TestStore(dispatcher); + store.setStoreState({test2: true}); - expect(store.getStoreState()).eql({foo: "bar"}); - }); + expect(view1.state).eql(null); }); - describe("#dispatchAction", function() { - it("should dispatch an action", function() { - sandbox.stub(dispatcher, "dispatch"); - var TestStore = loop.store.createStore({}); - var TestAction = sharedActions.Action.define("TestAction", {}); - var action = new TestAction({}); - var store = new TestStore(dispatcher); + it("should not stop listening to state changes on other components", function() { + view2 = TestUtils.renderIntoDocument(React.createElement(testComp)); - store.dispatchAction(action); + // There's no easy way in TestUtils to unmount, so simulate it. + view1.componentWillUnmount(); - sinon.assert.calledOnce(dispatcher.dispatch); - sinon.assert.calledWithExactly(dispatcher.dispatch, action); - }); - }); + store.setStoreState({test3: true}); - describe("#getStoreState", function() { - var TestStore = loop.store.createStore({}); - var store; - - beforeEach(function() { - store = new TestStore(dispatcher); - store.setStoreState({foo: "bar", bar: "baz"}); - }); - - it("should retrieve the whole state by default", function() { - expect(store.getStoreState()).eql({foo: "bar", bar: "baz"}); - }); - - it("should retrieve a given property state", function() { - expect(store.getStoreState("bar")).eql("baz"); - }); - }); - - describe("#setStoreState", function() { - var TestStore = loop.store.createStore({}); - var store; - - beforeEach(function() { - store = new TestStore(dispatcher); - store.setStoreState({foo: "bar"}); - }); - - it("should update store state data", function() { - store.setStoreState({foo: "baz"}); - - expect(store.getStoreState("foo")).eql("baz"); - }); - - it("should trigger a `change` event", function(done) { - store.once("change", function() { - done(); - }); - - store.setStoreState({foo: "baz"}); - }); - - it("should trigger a `change:` event", function(done) { - store.once("change:foo", function() { - done(); - }); - - store.setStoreState({foo: "baz"}); - }); + expect(view2.state).eql({test3: true}); }); }); });