Bug 1140481 - Use the StoreMixin in some of the Loop conversation views. r=dmose

This commit is contained in:
Mark Banner 2015-03-11 10:34:25 +00:00
parent 1dcd5b097c
commit 897951bc3a
8 changed files with 223 additions and 217 deletions

View File

@ -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,

View File

@ -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 (<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(<AppControllerView
conversationAppStore={conversationAppStore}
roomStore={roomStore}
conversationStore={conversationStore}
client={client}
conversation={conversation}
dispatcher={dispatcher}

View File

@ -720,14 +720,13 @@ loop.conversationViews = (function(mozL10n) {
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,
@ -742,18 +741,18 @@ loop.conversationViews = (function(mozL10n) {
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();
@ -775,7 +774,7 @@ loop.conversationViews = (function(mozL10n) {
_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) {
@ -928,29 +927,16 @@ loop.conversationViews = (function(mozL10n) {
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() {
@ -987,7 +973,6 @@ loop.conversationViews = (function(mozL10n) {
case CALL_STATES.TERMINATED: {
return (React.createElement(CallFailedView, {
dispatcher: this.props.dispatcher,
store: this.props.store,
contact: this.state.contact}
));
}

View File

@ -720,14 +720,13 @@ loop.conversationViews = (function(mozL10n) {
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,
@ -742,18 +741,18 @@ loop.conversationViews = (function(mozL10n) {
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();
@ -775,7 +774,7 @@ loop.conversationViews = (function(mozL10n) {
_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) {
@ -928,29 +927,16 @@ loop.conversationViews = (function(mozL10n) {
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() {
@ -987,7 +973,6 @@ loop.conversationViews = (function(mozL10n) {
case CALL_STATES.TERMINATED: {
return (<CallFailedView
dispatcher={this.props.dispatcher}
store={this.props.store}
contact={this.state.contact}
/>);
}

View File

@ -136,7 +136,7 @@ loop.store.StoreMixin = (function() {
}, this);
},
componentWillUnmount: function() {
this.getStore().off("change");
this.getStore().off("change", null, this);
}
};
}

View File

@ -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: {}
});

View File

@ -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() {

View File

@ -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:<prop>` 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:<prop>` event", function(done) {
store.once("change:foo", function() {
done();
});
store.setStoreState({foo: "baz"});
});
expect(view2.state).eql({test3: true});
});
});
});