Bug 1140481 - Use the StoreMixin in some of the Loop conversation views. r=dmose
☠☠ backed out by 1cf3f2f38f4e ☠ ☠
authorMark Banner <standard8@mozilla.com>
Wed, 11 Mar 2015 10:34:25 +0000
changeset 263476 35827fc86c80f5a69abba9de8bb605c645acec0d
parent 263475 c6fc468921096d699b28fc40b82bf28b0f644f21
child 263477 fb64168bf663570e4cec92d049bbd1072147fc61
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdmose
bugs1140481
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1140481 - Use the StoreMixin in some of the Loop conversation views. r=dmose
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/shared/js/store.js
browser/components/loop/test/desktop-local/conversationViews_test.js
browser/components/loop/test/desktop-local/conversation_test.js
browser/components/loop/test/shared/store_test.js
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -22,62 +22,51 @@ loop.conversation = (function(mozL10n) {
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
   var GenericFailureView = loop.conversationViews.GenericFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * 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.
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       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() {
       switch(this.state.windowType) {
         case "incoming": {
           return (React.createElement(IncomingConversationView, {
             client: this.props.client, 
             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}
           ));
         }
         case "room": {
           return (React.createElement(DesktopRoomConversationView, {
             dispatcher: this.props.dispatcher, 
             roomStore: this.props.roomStore}
           ));
@@ -156,17 +145,21 @@ loop.conversation = (function(mozL10n) {
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
     var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       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).
     var conversation = new sharedModels.ConversationModel({}, {
       sdk: window.OT,
       mozLoop: navigator.mozLoop
     });
 
@@ -186,19 +179,17 @@ loop.conversation = (function(mozL10n) {
       // XXX Move to the conversation models, when we transition
       // incoming calls to flux (bug 1088672).
       navigator.mozLoop.calls.clearCallInProgress(windowId);
 
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
     React.render(React.createElement(AppControllerView, {
-      conversationAppStore: conversationAppStore, 
       roomStore: roomStore, 
-      conversationStore: conversationStore, 
       client: client, 
       conversation: conversation, 
       dispatcher: dispatcher, 
       sdk: window.OT}
     ), document.querySelector('#main'));
 
     dispatcher.dispatch(new sharedActions.GetWindowData({
       windowId: windowId
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -22,62 +22,51 @@ loop.conversation = (function(mozL10n) {
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
   var GenericFailureView = loop.conversationViews.GenericFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * 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.
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       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() {
       switch(this.state.windowType) {
         case "incoming": {
           return (<IncomingConversationView
             client={this.props.client}
             conversation={this.props.conversation}
             sdk={this.props.sdk}
             isDesktop={true}
-            conversationAppStore={this.props.conversationAppStore}
+            conversationAppStore={this.getStore()}
           />);
         }
         case "outgoing": {
           return (<OutgoingConversationView
-            store={this.props.conversationStore}
             dispatcher={this.props.dispatcher}
           />);
         }
         case "room": {
           return (<DesktopRoomConversationView
             dispatcher={this.props.dispatcher}
             roomStore={this.props.roomStore}
           />);
@@ -156,17 +145,21 @@ loop.conversation = (function(mozL10n) {
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
     var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       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).
     var conversation = new sharedModels.ConversationModel({}, {
       sdk: window.OT,
       mozLoop: navigator.mozLoop
     });
 
@@ -186,19 +179,17 @@ loop.conversation = (function(mozL10n) {
       // XXX Move to the conversation models, when we transition
       // incoming calls to flux (bug 1088672).
       navigator.mozLoop.calls.clearCallInProgress(windowId);
 
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
     React.render(<AppControllerView
-      conversationAppStore={conversationAppStore}
       roomStore={roomStore}
-      conversationStore={conversationStore}
       client={client}
       conversation={conversation}
       dispatcher={dispatcher}
       sdk={window.OT}
     />, document.querySelector('#main'));
 
     dispatcher.dispatch(new sharedActions.GetWindowData({
       windowId: windowId
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -715,50 +715,49 @@ loop.conversationViews = (function(mozL1
   });
 
   /**
    * Call failed view. Displayed when a call fails.
    */
   var CallFailedView = React.createClass({displayName: "CallFailedView",
     mixins: [
       Backbone.Events,
+      loop.store.StoreMixin("conversationStore"),
       sharedMixins.AudioMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      store: React.PropTypes.instanceOf(
-        loop.store.ConversationStore).isRequired,
       contact: React.PropTypes.object.isRequired,
       // This is used by the UI showcase.
       emailLinkError: React.PropTypes.bool,
     },
 
     getInitialState: function() {
       return {
         emailLinkError: this.props.emailLinkError,
         emailLinkButtonDisabled: false
       };
     },
 
     componentDidMount: function() {
       this.play("failure");
-      this.listenTo(this.props.store, "change:emailLink",
+      this.listenTo(this.getStore(), "change:emailLink",
                     this._onEmailLinkReceived);
-      this.listenTo(this.props.store, "error:emailLink",
+      this.listenTo(this.getStore(), "error:emailLink",
                     this._onEmailLinkError);
     },
 
     componentWillUnmount: function() {
-      this.stopListening(this.props.store);
+      this.stopListening(this.getStore());
     },
 
     _onEmailLinkReceived: function() {
-      var emailLink = this.props.store.getStoreState("emailLink");
+      var emailLink = this.getStoreState().emailLink;
       var contactEmail = _getPreferredEmail(this.props.contact).value;
       sharedUtils.composeCallUrlEmail(emailLink, contactEmail);
       this.closeWindow();
     },
 
     _onEmailLinkError: function() {
       this.setState({
         emailLinkError: true,
@@ -770,17 +769,17 @@ loop.conversationViews = (function(mozL1
       if (!this.state.emailLinkError) {
         return;
       }
       return React.createElement("p", {className: "error"}, mozL10n.get("unable_retrieve_url"));
     },
 
     _getTitleMessage: function() {
       var callStateReason =
-        this.props.store.getStoreState("callStateReason");
+        this.getStoreState().callStateReason;
 
       if (callStateReason === WEBSOCKET_REASONS.REJECT || callStateReason === WEBSOCKET_REASONS.BUSY ||
           callStateReason === REST_ERRNOS.USER_UNAVAILABLE) {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
@@ -923,39 +922,26 @@ loop.conversationViews = (function(mozL1
 
   /**
    * Master View Controller for outgoing calls. This manages
    * the different views that need displaying.
    */
   var OutgoingConversationView = React.createClass({displayName: "OutgoingConversationView",
     mixins: [
       sharedMixins.AudioMixin,
+      loop.store.StoreMixin("conversationStore"),
       Backbone.Events
     ],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      store: React.PropTypes.instanceOf(
-        loop.store.ConversationStore).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
-      return this.props.store.getStoreState();
-    },
-
-    componentWillMount: function() {
-      this.listenTo(this.props.store, "change", function() {
-        this.setState(this.props.store.getStoreState());
-      }, this);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.props.store, "change", function() {
-        this.setState(this.props.store.getStoreState());
-      }, this);
+      return this.getStoreState();
     },
 
     _closeWindow: function() {
       window.close();
     },
 
     /**
      * Returns true if the call is in a cancellable state, during call setup.
@@ -982,17 +968,16 @@ loop.conversationViews = (function(mozL1
       switch (this.state.callState) {
         case CALL_STATES.CLOSE: {
           this._closeWindow();
           return null;
         }
         case CALL_STATES.TERMINATED: {
           return (React.createElement(CallFailedView, {
             dispatcher: this.props.dispatcher, 
-            store: this.props.store, 
             contact: this.state.contact}
           ));
         }
         case CALL_STATES.ONGOING: {
           return (React.createElement(OngoingConversationView, {
             dispatcher: this.props.dispatcher, 
             video: {enabled: !this.state.videoMuted}, 
             audio: {enabled: !this.state.audioMuted}}
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -715,50 +715,49 @@ loop.conversationViews = (function(mozL1
   });
 
   /**
    * Call failed view. Displayed when a call fails.
    */
   var CallFailedView = React.createClass({
     mixins: [
       Backbone.Events,
+      loop.store.StoreMixin("conversationStore"),
       sharedMixins.AudioMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      store: React.PropTypes.instanceOf(
-        loop.store.ConversationStore).isRequired,
       contact: React.PropTypes.object.isRequired,
       // This is used by the UI showcase.
       emailLinkError: React.PropTypes.bool,
     },
 
     getInitialState: function() {
       return {
         emailLinkError: this.props.emailLinkError,
         emailLinkButtonDisabled: false
       };
     },
 
     componentDidMount: function() {
       this.play("failure");
-      this.listenTo(this.props.store, "change:emailLink",
+      this.listenTo(this.getStore(), "change:emailLink",
                     this._onEmailLinkReceived);
-      this.listenTo(this.props.store, "error:emailLink",
+      this.listenTo(this.getStore(), "error:emailLink",
                     this._onEmailLinkError);
     },
 
     componentWillUnmount: function() {
-      this.stopListening(this.props.store);
+      this.stopListening(this.getStore());
     },
 
     _onEmailLinkReceived: function() {
-      var emailLink = this.props.store.getStoreState("emailLink");
+      var emailLink = this.getStoreState().emailLink;
       var contactEmail = _getPreferredEmail(this.props.contact).value;
       sharedUtils.composeCallUrlEmail(emailLink, contactEmail);
       this.closeWindow();
     },
 
     _onEmailLinkError: function() {
       this.setState({
         emailLinkError: true,
@@ -770,17 +769,17 @@ loop.conversationViews = (function(mozL1
       if (!this.state.emailLinkError) {
         return;
       }
       return <p className="error">{mozL10n.get("unable_retrieve_url")}</p>;
     },
 
     _getTitleMessage: function() {
       var callStateReason =
-        this.props.store.getStoreState("callStateReason");
+        this.getStoreState().callStateReason;
 
       if (callStateReason === WEBSOCKET_REASONS.REJECT || callStateReason === WEBSOCKET_REASONS.BUSY ||
           callStateReason === REST_ERRNOS.USER_UNAVAILABLE) {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
@@ -923,39 +922,26 @@ loop.conversationViews = (function(mozL1
 
   /**
    * Master View Controller for outgoing calls. This manages
    * the different views that need displaying.
    */
   var OutgoingConversationView = React.createClass({
     mixins: [
       sharedMixins.AudioMixin,
+      loop.store.StoreMixin("conversationStore"),
       Backbone.Events
     ],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      store: React.PropTypes.instanceOf(
-        loop.store.ConversationStore).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
-      return this.props.store.getStoreState();
-    },
-
-    componentWillMount: function() {
-      this.listenTo(this.props.store, "change", function() {
-        this.setState(this.props.store.getStoreState());
-      }, this);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.props.store, "change", function() {
-        this.setState(this.props.store.getStoreState());
-      }, this);
+      return this.getStoreState();
     },
 
     _closeWindow: function() {
       window.close();
     },
 
     /**
      * Returns true if the call is in a cancellable state, during call setup.
@@ -982,17 +968,16 @@ loop.conversationViews = (function(mozL1
       switch (this.state.callState) {
         case CALL_STATES.CLOSE: {
           this._closeWindow();
           return null;
         }
         case CALL_STATES.TERMINATED: {
           return (<CallFailedView
             dispatcher={this.props.dispatcher}
-            store={this.props.store}
             contact={this.state.contact}
           />);
         }
         case CALL_STATES.ONGOING: {
           return (<OngoingConversationView
             dispatcher={this.props.dispatcher}
             video={{enabled: !this.state.videoMuted}}
             audio={{enabled: !this.state.audioMuted}}
--- a/browser/components/loop/content/shared/js/store.js
+++ b/browser/components/loop/content/shared/js/store.js
@@ -131,17 +131,17 @@ loop.store.StoreMixin = (function() {
         return this.getStore().getStoreState();
       },
       componentWillMount: function() {
         this.getStore().on("change", function() {
           this.setState(this.getStoreState());
         }, this);
       },
       componentWillUnmount: function() {
-        this.getStore().off("change");
+        this.getStore().off("change", null, this);
       }
     };
   }
   StoreMixin.register = function(stores) {
     _.extend(_stores, stores);
   };
   return StoreMixin;
 })();
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -278,27 +278,30 @@ describe("loop.conversationViews", funct
 
     var contact = {email: [{value: "test@test.tld"}]};
 
     function mountTestComponent(options) {
       options = options || {};
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.CallFailedView, {
           dispatcher: dispatcher,
-          store: store,
           contact: options.contact
         }));
     }
 
     beforeEach(function() {
       store = new loop.store.ConversationStore(dispatcher, {
         client: {},
         mozLoop: navigator.mozLoop,
         sdkDriver: {}
       });
+      loop.store.StoreMixin.register({
+        conversationStore: store
+      });
+
       fakeAudio = {
         play: sinon.spy(),
         pause: sinon.spy(),
         removeAttribute: sinon.spy()
       };
       sandbox.stub(window, "Audio").returns(fakeAudio);
     });
 
@@ -577,26 +580,29 @@ describe("loop.conversationViews", funct
 
   describe("OutgoingConversationView", function() {
     var store, feedbackStore;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.OutgoingConversationView, {
           dispatcher: dispatcher,
-          store: store
         }));
     }
 
     beforeEach(function() {
       store = new loop.store.ConversationStore(dispatcher, {
         client: {},
         mozLoop: fakeMozLoop,
         sdkDriver: {}
       });
+      loop.store.StoreMixin.register({
+        conversationStore: store
+      });
+
       feedbackStore = new loop.store.FeedbackStore(dispatcher, {
         feedbackClient: {}
       });
     });
 
     it("should render the CallFailedView when the call state is 'terminated'",
       function() {
         store.setStoreState({
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -134,18 +134,16 @@ describe("loop.conversation", function()
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversation.AppControllerView, {
           client: client,
           conversation: conversation,
           roomStore: roomStore,
           sdk: {},
-          conversationStore: conversationStore,
-          conversationAppStore: conversationAppStore,
           dispatcher: dispatcher,
           mozLoop: navigator.mozLoop
         }));
     }
 
     beforeEach(function() {
       oldTitle = document.title;
       client = new loop.Client();
@@ -171,16 +169,21 @@ describe("loop.conversation", function()
 
       roomStore = new loop.store.RoomStore(dispatcher, {
         mozLoop: navigator.mozLoop,
       });
       conversationAppStore = new loop.store.ConversationAppStore({
         dispatcher: dispatcher,
         mozLoop: navigator.mozLoop
       });
+
+      loop.store.StoreMixin.register({
+        conversationAppStore: conversationAppStore,
+        conversationStore: conversationStore
+      });
     });
 
     afterEach(function() {
       ccView = undefined;
       document.title = oldTitle;
     });
 
     it("should display the OutgoingConversationView for outgoing calls", function() {
--- a/browser/components/loop/test/shared/store_test.js
+++ b/browser/components/loop/test/shared/store_test.js
@@ -1,160 +1,205 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 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() {
-    var dispatcher;
-
-    beforeEach(function() {
-      dispatcher = new loop.Dispatcher();
+    it("should implement Backbone.Events", function() {
+      expect(loop.store.createStore({}).prototype).to.include.keys(["on", "off"]);
     });
 
-    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() {}
+    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/);
         });
 
-        var store = new TestStore(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};
 
-        sinon.assert.calledOnce(dispatcher.register);
-        sinon.assert.calledWithExactly(dispatcher.register, store, ["a", "b"]);
-      });
+          new TestStore(dispatcher, options);
 
-      it("should throw if a registered action isn't implemented", function() {
-        var TestStore = loop.store.createStore({
-          actions: ["a", "b"],
-          a: function() {} // missing b
+          sinon.assert.calledOnce(initialize);
+          sinon.assert.calledWithExactly(initialize, options);
         });
 
-        expect(function() {
-          new TestStore(dispatcher);
-        }).to.Throw(/should implement an action handler for b/);
-      });
-    });
+        it("should register actions", function() {
+          sandbox.stub(dispatcher, "register");
+          var TestStore = loop.store.createStore({
+            actions: ["a", "b"],
+            a: function() {},
+            b: function() {}
+          });
 
-    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);
+
+          sinon.assert.calledOnce(dispatcher.register);
+          sinon.assert.calledWithExactly(dispatcher.register, store, ["a", "b"]);
         });
 
-        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);
+        it("should throw if a registered action isn't implemented", function() {
+          var TestStore = loop.store.createStore({
+            actions: ["a", "b"],
+            a: function() {} // missing b
+          });
 
-        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"});
+          expect(function() {
+            new TestStore(dispatcher);
+          }).to.Throw(/should implement an action handler for b/);
+        });
       });
 
-      it("should retrieve the whole state by default", function() {
-        expect(store.getStoreState()).eql({foo: "bar", bar: "baz"});
+      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);
+        });
       });
 
-      it("should retrieve a given property state", function() {
-        expect(store.getStoreState("bar")).eql("baz");
-      });
-    });
+      describe("#getStoreState", function() {
+        var TestStore = loop.store.createStore({});
+        var store;
 
-    describe("#setStoreState", function() {
-      var TestStore = loop.store.createStore({});
-      var store;
+        beforeEach(function() {
+          store = new TestStore(dispatcher);
+          store.setStoreState({foo: "bar", bar: "baz"});
+        });
 
-      beforeEach(function() {
-        store = new TestStore(dispatcher);
-        store.setStoreState({foo: "bar"});
+        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");
+        });
       });
 
-      it("should update store state data", function() {
-        store.setStoreState({foo: "baz"});
+      describe("#setStoreState", function() {
+        var TestStore = loop.store.createStore({});
+        var store;
 
-        expect(store.getStoreState("foo")).eql("baz");
-      });
+        beforeEach(function() {
+          store = new TestStore(dispatcher);
+          store.setStoreState({foo: "bar"});
+        });
 
-      it("should trigger a `change` event", function(done) {
-        store.once("change", function() {
-          done();
+        it("should update store state data", function() {
+          store.setStoreState({foo: "baz"});
+
+          expect(store.getStoreState("foo")).eql("baz");
         });
 
-        store.setStoreState({foo: "baz"});
-      });
+        it("should trigger a `change` event", function(done) {
+          store.once("change", function() {
+            done();
+          });
 
-      it("should trigger a `change:<prop>` event", function(done) {
-        store.once("change:foo", function() {
-          done();
+          store.setStoreState({foo: "baz"});
         });
 
-        store.setStoreState({foo: "baz"});
+        it("should trigger a `change:<prop>` event", function(done) {
+          store.once("change:foo", function() {
+            done();
+          });
+
+          store.setStoreState({foo: "baz"});
+        });
       });
     });
   });
+
+  describe("loop.store.StoreMixin", function() {
+    var view1, view2, store, storeClass, testComp;
+
+    beforeEach(function() {
+      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));
+    });
+
+    it("should update the state when the store changes", function() {
+      store.setStoreState({test: true});
+
+      expect(view1.state).eql({test: true});
+    });
+
+    it("should stop listening to state changes", function() {
+      // There's no easy way in TestUtils to unmount, so simulate it.
+      view1.componentWillUnmount();
+
+      store.setStoreState({test2: true});
+
+      expect(view1.state).eql(null);
+    });
+
+    it("should not stop listening to state changes on other components", function() {
+      view2 = TestUtils.renderIntoDocument(React.createElement(testComp));
+
+      // There's no easy way in TestUtils to unmount, so simulate it.
+      view1.componentWillUnmount();
+
+      store.setStoreState({test3: true});
+
+      expect(view2.state).eql({test3: true});
+    });
+  });
 });