Bug 1090209 - Part 1 Drop the window type from the url that opens a Loop conversation window, and pass it in the call data instead. r=nperriault

Also creates a ConversationAppStore for managing the overall window data and selection of the type of window it is for the views.
This commit is contained in:
Mark Banner 2014-11-03 16:34:02 +00:00
parent 2dd6bd5c87
commit 6292d8459f
19 changed files with 491 additions and 366 deletions

View File

@ -252,7 +252,8 @@ let LoopCallsInternal = {
callData.sessionType = sessionType;
// XXX Bug 1090209 will transiton into a better window id.
callData.windowId = callData.callId;
this._startCall(callData, "incoming");
callData.type = "incoming";
this._startCall(callData);
} else {
this._returnBusy(callData);
}
@ -269,17 +270,17 @@ let LoopCallsInternal = {
* Starts a call, saves the call data, and opens a chat window.
*
* @param {Object} callData The data associated with the call including an id.
* @param {Boolean} conversationType Whether or not the call is "incoming"
* or "outgoing"
* The data should include the type - "incoming" or
* "outgoing".
*/
_startCall: function(callData, conversationType) {
_startCall: function(callData) {
this.callsData.inUse = true;
this.callsData.data = callData;
MozLoopService.openChatWindow(
null,
// No title, let the page set that, to avoid flickering.
"",
"about:loopconversation#" + conversationType + "/" + callData.windowId);
"about:loopconversation#" + callData.windowId);
},
/**
@ -296,11 +297,12 @@ let LoopCallsInternal = {
var callData = {
contact: contact,
callType: callType,
type: "outgoing",
// XXX Really we shouldn't be using random numbers, bug 1090209 will fix this.
windowId: Math.floor((Math.random() * 100000000))
};
this._startCall(callData, "outgoing");
this._startCall(callData);
return true;
},

View File

@ -38,6 +38,7 @@
<script type="text/javascript" src="loop/shared/js/localRoomStore.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
<script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/js/roomViews.js"></script>

View File

@ -179,12 +179,12 @@ loop.conversation = (function(mozL10n) {
});
/**
* Incoming Call failed view. Displayed when a call fails.
* Something went wrong view. Displayed when there's a big problem.
*
* XXX Based on CallFailedView, but built specially until we flux-ify the
* incoming call views (bug 1088672).
*/
var IncomingCallFailedView = React.createClass({displayName: 'IncomingCallFailedView',
var GenericFailureView = React.createClass({displayName: 'GenericFailureView',
propTypes: {
cancelCall: React.PropTypes.func.isRequired
},
@ -283,7 +283,7 @@ loop.conversation = (function(mozL10n) {
case "end": {
// XXX To be handled with the "failed" view state when bug 1047410 lands
if (this.state.callFailed) {
return IncomingCallFailedView({
return GenericFailureView({
cancelCall: this.closeWindow.bind(this)}
)
}
@ -523,6 +523,8 @@ loop.conversation = (function(mozL10n) {
* in progress, and hence, which view to display.
*/
var AppControllerView = React.createClass({displayName: 'AppControllerView',
mixins: [Backbone.Events],
propTypes: {
// XXX Old types required for incoming call view.
client: React.PropTypes.instanceOf(loop.Client).isRequired,
@ -530,51 +532,65 @@ loop.conversation = (function(mozL10n) {
.isRequired,
sdk: React.PropTypes.object.isRequired,
// XXX New types for OutgoingConversationView
store: React.PropTypes.instanceOf(loop.store.ConversationStore).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,
// if not passed, this is not a room view
localRoomStore: React.PropTypes.instanceOf(loop.store.LocalRoomStore)
},
getInitialState: function() {
return this.props.store.attributes;
return this.props.conversationAppStore.getStoreState();
},
componentWillMount: function() {
this.props.store.on("change:outgoing", function() {
this.setState(this.props.store.attributes);
this.listenTo(this.props.conversationAppStore, "change", function() {
this.setState(this.props.conversationAppStore.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.stopListening(this.props.conversationAppStore);
},
closeWindow: function() {
window.close();
},
render: function() {
if (this.props.localRoomStore) {
return (
EmptyRoomView({
switch(this.state.windowType) {
case "incoming": {
return (IncomingConversationView({
client: this.props.client,
conversation: this.props.conversation,
sdk: this.props.sdk}
));
}
case "outgoing": {
return (OutgoingConversationView({
store: this.props.conversationStore,
dispatcher: this.props.dispatcher}
));
}
case "room": {
return (EmptyRoomView({
mozLoop: navigator.mozLoop,
localRoomStore: this.props.localRoomStore}
)
);
));
}
case "failed": {
return (GenericFailureView({
cancelCall: this.closeWindow.bind(this)}
));
}
default: {
// If we don't have a windowType, we don't know what we are yet,
// so don't display anything.
return null;
}
}
// Don't display anything, until we know what type of call we are.
if (this.state.outgoing === undefined) {
return null;
}
if (this.state.outgoing) {
return (OutgoingConversationView({
store: this.props.store,
dispatcher: this.props.dispatcher}
));
}
return (IncomingConversationView({
client: this.props.client,
conversation: this.props.conversation,
sdk: this.props.sdk}
));
}
});
@ -605,11 +621,20 @@ loop.conversation = (function(mozL10n) {
sdk: OT
});
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
var conversationStore = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher,
sdkDriver: sdkDriver
});
var localRoomStore = new loop.store.LocalRoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});;
// XXX Old class creation for the incoming conversation view, whilst
// we transition across (bug 1072323).
@ -622,30 +647,10 @@ loop.conversation = (function(mozL10n) {
var helper = new loop.shared.utils.Helper();
var locationHash = helper.locationData().hash;
var windowId;
var outgoing;
var localRoomStore;
// XXX removeMe, along with noisy comment at the beginning of
// conversation_test.js "when locationHash begins with #room".
if (navigator.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
locationHash = "#room/32";
}
var hash = locationHash.match(/#incoming\/(.*)/);
var hash = locationHash.match(/#(.*)/);
if (hash) {
windowId = hash[1];
outgoing = false;
} else if (hash = locationHash.match(/#room\/(.*)/)) {
localRoomStore = new loop.store.LocalRoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
} else {
hash = locationHash.match(/#outgoing\/(.*)/);
if (hash) {
windowId = hash[1];
outgoing = true;
}
}
conversation.set({windowId: windowId});
@ -656,23 +661,17 @@ loop.conversation = (function(mozL10n) {
});
React.renderComponent(AppControllerView({
conversationAppStore: conversationAppStore,
localRoomStore: localRoomStore,
store: conversationStore,
conversationStore: conversationStore,
client: client,
conversation: conversation,
dispatcher: dispatcher,
sdk: window.OT}
), document.querySelector('#main'));
if (localRoomStore) {
dispatcher.dispatch(
new sharedActions.SetupEmptyRoom({localRoomId: hash[1]}));
return;
}
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
windowId: windowId,
outgoing: outgoing
dispatcher.dispatch(new sharedActions.GetWindowData({
windowId: windowId
}));
}
@ -680,7 +679,7 @@ loop.conversation = (function(mozL10n) {
AppControllerView: AppControllerView,
IncomingConversationView: IncomingConversationView,
IncomingCallView: IncomingCallView,
IncomingCallFailedView: IncomingCallFailedView,
GenericFailureView: GenericFailureView,
init: init
};
})(document.mozL10n);

View File

@ -179,12 +179,12 @@ loop.conversation = (function(mozL10n) {
});
/**
* Incoming Call failed view. Displayed when a call fails.
* Something went wrong view. Displayed when there's a big problem.
*
* XXX Based on CallFailedView, but built specially until we flux-ify the
* incoming call views (bug 1088672).
*/
var IncomingCallFailedView = React.createClass({
var GenericFailureView = React.createClass({
propTypes: {
cancelCall: React.PropTypes.func.isRequired
},
@ -283,7 +283,7 @@ loop.conversation = (function(mozL10n) {
case "end": {
// XXX To be handled with the "failed" view state when bug 1047410 lands
if (this.state.callFailed) {
return <IncomingCallFailedView
return <GenericFailureView
cancelCall={this.closeWindow.bind(this)}
/>
}
@ -523,6 +523,8 @@ loop.conversation = (function(mozL10n) {
* in progress, and hence, which view to display.
*/
var AppControllerView = React.createClass({
mixins: [Backbone.Events],
propTypes: {
// XXX Old types required for incoming call view.
client: React.PropTypes.instanceOf(loop.Client).isRequired,
@ -530,51 +532,65 @@ loop.conversation = (function(mozL10n) {
.isRequired,
sdk: React.PropTypes.object.isRequired,
// XXX New types for OutgoingConversationView
store: React.PropTypes.instanceOf(loop.store.ConversationStore).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,
// if not passed, this is not a room view
localRoomStore: React.PropTypes.instanceOf(loop.store.LocalRoomStore)
},
getInitialState: function() {
return this.props.store.attributes;
return this.props.conversationAppStore.getStoreState();
},
componentWillMount: function() {
this.props.store.on("change:outgoing", function() {
this.setState(this.props.store.attributes);
this.listenTo(this.props.conversationAppStore, "change", function() {
this.setState(this.props.conversationAppStore.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.stopListening(this.props.conversationAppStore);
},
closeWindow: function() {
window.close();
},
render: function() {
if (this.props.localRoomStore) {
return (
<EmptyRoomView
switch(this.state.windowType) {
case "incoming": {
return (<IncomingConversationView
client={this.props.client}
conversation={this.props.conversation}
sdk={this.props.sdk}
/>);
}
case "outgoing": {
return (<OutgoingConversationView
store={this.props.conversationStore}
dispatcher={this.props.dispatcher}
/>);
}
case "room": {
return (<EmptyRoomView
mozLoop={navigator.mozLoop}
localRoomStore={this.props.localRoomStore}
/>
);
/>);
}
case "failed": {
return (<GenericFailureView
cancelCall={this.closeWindow.bind(this)}
/>);
}
default: {
// If we don't have a windowType, we don't know what we are yet,
// so don't display anything.
return null;
}
}
// Don't display anything, until we know what type of call we are.
if (this.state.outgoing === undefined) {
return null;
}
if (this.state.outgoing) {
return (<OutgoingConversationView
store={this.props.store}
dispatcher={this.props.dispatcher}
/>);
}
return (<IncomingConversationView
client={this.props.client}
conversation={this.props.conversation}
sdk={this.props.sdk}
/>);
}
});
@ -605,11 +621,20 @@ loop.conversation = (function(mozL10n) {
sdk: OT
});
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
var conversationStore = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher,
sdkDriver: sdkDriver
});
var localRoomStore = new loop.store.LocalRoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});;
// XXX Old class creation for the incoming conversation view, whilst
// we transition across (bug 1072323).
@ -622,30 +647,10 @@ loop.conversation = (function(mozL10n) {
var helper = new loop.shared.utils.Helper();
var locationHash = helper.locationData().hash;
var windowId;
var outgoing;
var localRoomStore;
// XXX removeMe, along with noisy comment at the beginning of
// conversation_test.js "when locationHash begins with #room".
if (navigator.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
locationHash = "#room/32";
}
var hash = locationHash.match(/#incoming\/(.*)/);
var hash = locationHash.match(/#(.*)/);
if (hash) {
windowId = hash[1];
outgoing = false;
} else if (hash = locationHash.match(/#room\/(.*)/)) {
localRoomStore = new loop.store.LocalRoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
} else {
hash = locationHash.match(/#outgoing\/(.*)/);
if (hash) {
windowId = hash[1];
outgoing = true;
}
}
conversation.set({windowId: windowId});
@ -656,23 +661,17 @@ loop.conversation = (function(mozL10n) {
});
React.renderComponent(<AppControllerView
conversationAppStore={conversationAppStore}
localRoomStore={localRoomStore}
store={conversationStore}
conversationStore={conversationStore}
client={client}
conversation={conversation}
dispatcher={dispatcher}
sdk={window.OT}
/>, document.querySelector('#main'));
if (localRoomStore) {
dispatcher.dispatch(
new sharedActions.SetupEmptyRoom({localRoomId: hash[1]}));
return;
}
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
windowId: windowId,
outgoing: outgoing
dispatcher.dispatch(new sharedActions.GetWindowData({
windowId: windowId
}));
}
@ -680,7 +679,7 @@ loop.conversation = (function(mozL10n) {
AppControllerView: AppControllerView,
IncomingConversationView: IncomingConversationView,
IncomingCallView: IncomingCallView,
IncomingCallFailedView: IncomingCallFailedView,
GenericFailureView: GenericFailureView,
init: init
};
})(document.mozL10n);

View File

@ -0,0 +1,88 @@
/* 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/. */
/* global loop:true */
var loop = loop || {};
loop.store = loop.store || {};
/**
* Manages the conversation window app controller view. Used to get
* the window data and store the window type.
*/
loop.store.ConversationAppStore = (function() {
/**
* Constructor
*
* @param {Object} options Options for the store. Should contain the dispatcher.
*/
var ConversationAppStore = function(options) {
if (!options.dispatcher) {
throw new Error("Missing option dispatcher");
}
if (!options.mozLoop) {
throw new Error("Missing option mozLoop");
}
this._dispatcher = options.dispatcher;
this._mozLoop = options.mozLoop;
this._storeState = {};
this._dispatcher.register(this, [
"getWindowData"
]);
};
ConversationAppStore.prototype = _.extend({
/**
* Retrieves current store state.
*
* @return {Object}
*/
getStoreState: function() {
return this._storeState;
},
/**
* Updates store states and trigger a "change" event.
*
* @param {Object} state The new store state.
*/
setStoreState: function(state) {
this._storeState = state;
this.trigger("change");
},
/**
* Handles the get window data action - obtains the window data,
* updates the store and notifies interested components.
*
* @param {sharedActions.GetWindowData} actionData The action data
*/
getWindowData: function(actionData) {
var windowData;
// XXX Remove me in bug 1074678
if (this._mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
windowData = {type: "room", localRoomId: "42"};
} else {
windowData = this._mozLoop.getCallData(actionData.windowId);
}
if (!windowData) {
console.error("Failed to get the window data");
this.setStoreState({windowType: "failed"});
return;
}
this.setStoreState({windowType: windowData.type});
this._dispatcher.dispatch(new loop.shared.actions.SetupWindowData({
windowData: windowData
}));
}
}, Backbone.Events);
return ConversationAppStore;
})();

View File

@ -499,6 +499,10 @@ loop.conversationViews = (function(mozL10n) {
case CALL_STATES.FINISHED: {
return this._renderFeedbackView();
}
case CALL_STATES.INIT: {
// We know what we are, but we haven't got the data yet.
return null;
}
default: {
return (PendingConversationView({
dispatcher: this.props.dispatcher,

View File

@ -499,6 +499,10 @@ loop.conversationViews = (function(mozL10n) {
case CALL_STATES.FINISHED: {
return this._renderFeedbackView();
}
case CALL_STATES.INIT: {
// We know what we are, but we haven't got the data yet.
return null;
}
default: {
return (<PendingConversationView
dispatcher={this.props.dispatcher}

View File

@ -30,6 +30,21 @@ loop.shared.actions = (function() {
};
return {
/**
* Get the window data for the provided window id
*/
GetWindowData: Action.define("getWindowData", {
windowId: String
}),
/**
* Used to pass round the window data so that stores can
* record the appropriate data.
*/
SetupWindowData: Action.define("setupWindowData", {
windowData: Object
}),
/**
* Fetch a new call url from the server, intended to be sent over email when
* a contact can't be reached.
@ -37,15 +52,6 @@ loop.shared.actions = (function() {
FetchEmailLink: Action.define("fetchEmailLink", {
}),
/**
* Used to trigger gathering of initial call data.
*/
GatherCallData: Action.define("gatherCallData", {
// Specify the callId for an incoming call.
windowId: [String, null],
outgoing: Boolean
}),
/**
* Used to cancel call setup.
*/
@ -167,16 +173,6 @@ loop.shared.actions = (function() {
*/
UpdateRoomList: Action.define("updateRoomList", {
roomList: Array
}),
/**
* Primes localRoomStore with roomLocalId, which triggers the EmptyRoomView
* to do any necessary setup.
*
* XXX should move to localRoomActions module
*/
SetupEmptyRoom: Action.define("setupEmptyRoom", {
localRoomId: String
})
};
})();

View File

@ -121,7 +121,7 @@ loop.store.ConversationStore = (function() {
this.dispatcher.register(this, [
"connectionFailure",
"connectionProgress",
"gatherCallData",
"setupWindowData",
"connectCall",
"hangupCall",
"peerHungupCall",
@ -188,37 +188,24 @@ loop.store.ConversationStore = (function() {
}
},
/**
* Handles the gather call data action, setting the state
* and starting to get the appropriate data for the type of call.
*
* @param {sharedActions.GatherCallData} actionData The action data.
*/
gatherCallData: function(actionData) {
if (!actionData.outgoing) {
// XXX Other types aren't supported yet, but set the state for the
// view selection.
this.set({outgoing: false});
return;
}
var callData = navigator.mozLoop.getCallData(actionData.windowId);
if (!callData) {
console.error("Failed to get the call data");
this.set({callState: CALL_STATES.TERMINATED});
setupWindowData: function(actionData) {
var windowData = actionData.windowData;
var windowType = windowData.type;
if (windowType !== "outgoing" &&
windowType !== "incoming") {
// Not for this store, don't do anything.
return;
}
this.set({
contact: callData.contact,
outgoing: actionData.outgoing,
windowId: actionData.windowId,
callType: callData.callType,
callState: CALL_STATES.GATHER
contact: windowData.contact,
outgoing: windowType === "outgoing",
windowId: windowData.windowId,
callType: windowData.callType,
callState: CALL_STATES.GATHER,
videoMuted: windowData.callType === CALL_TYPES.AUDIO_ONLY
});
this.set({videoMuted: this.get("callType") === CALL_TYPES.AUDIO_ONLY});
if (this.get("outgoing")) {
this._setupOutgoingCall();
} // XXX Else, other types aren't supported yet.

View File

@ -36,7 +36,9 @@ loop.store.LocalRoomStore = (function() {
}
this.mozLoop = options.mozLoop;
this.dispatcher.register(this, ["setupEmptyRoom"]);
this.dispatcher.register(this, [
"setupWindowData"
]);
}
LocalRoomStore.prototype = _.extend({
@ -73,12 +75,13 @@ loop.store.LocalRoomStore = (function() {
*
* XXXremoveMe Can probably be removed when bug 1074664 lands.
*
* @param {sharedActions.setupEmptyRoom} actionData
* @param {Integer} roomId The id of the room.
* @param {Function} cb Callback(error, roomData)
*/
_fetchRoomData: function(actionData, cb) {
if (this.mozLoop.rooms && this.mozLoop.rooms.getRoomData) {
this.mozLoop.rooms.getRoomData(actionData.localRoomId, cb);
_fetchRoomData: function(roomId, cb) {
// XXX Remove me in bug 1074678
if (!this.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
this.mozLoop.rooms.getRoomData(roomId, cb);
} else {
cb(null, {roomName: "Donkeys"});
}
@ -94,16 +97,22 @@ loop.store.LocalRoomStore = (function() {
* When the room name gets set, that will trigger the view to display
* that name.
*
* @param {sharedActions.setupEmptyRoom} actionData
* @param {sharedActions.SetupWindowData} actionData
*/
setupEmptyRoom: function(actionData) {
this._fetchRoomData(actionData, function(error, roomData) {
this.setStoreState({
error: error,
localRoomId: actionData.localRoomId,
serverData: roomData
});
}.bind(this));
setupWindowData: function(actionData) {
if (actionData.windowData.type !== "room") {
// Nothing for us to do here, leave it to other stores.
return;
}
this._fetchRoomData(actionData.windowData.localRoomId,
function(error, roomData) {
this.setStoreState({
error: error,
localRoomId: actionData.windowData.localRoomId,
serverData: roomData
});
}.bind(this));
}
}, Backbone.Events);

View File

@ -13,6 +13,7 @@ browser.jar:
# Desktop script
content/browser/loop/js/client.js (content/js/client.js)
content/browser/loop/js/conversation.js (content/js/conversation.js)
content/browser/loop/js/conversationAppStore.js (content/js/conversationAppStore.js)
content/browser/loop/js/otconfig.js (content/js/otconfig.js)
content/browser/loop/js/panel.js (content/js/panel.js)
content/browser/loop/js/contacts.js (content/js/contacts.js)

View File

@ -0,0 +1,84 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var expect = chai.expect;
describe("loop.store.ConversationAppStore", function () {
var sharedActions = loop.shared.actions;
var sandbox, dispatcher;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
});
afterEach(function() {
sandbox.restore();
});
describe("#constructor", function() {
it("should throw an error if the dispatcher is missing", function() {
expect(function() {
new loop.store.ConversationAppStore({mozLoop: {}});
}).to.Throw(/dispatcher/);
});
it("should throw an error if mozLoop is missing", function() {
expect(function() {
new loop.store.ConversationAppStore({dispatcher: dispatcher});
}).to.Throw(/mozLoop/);
});
});
describe("#getWindowData", function() {
var fakeCallData, fakeGetWindowData, fakeMozLoop, store;
beforeEach(function() {
fakeCallData = {
type: "incoming",
callId: "123456"
};
fakeGetWindowData = {
windowId: "42"
};
fakeMozLoop = {
// XXX Remove me in bug 1074678
getLoopBoolPref: function() { return false; },
getCallData: function(windowId) {
if (windowId === "42") {
return fakeCallData;
}
return null;
}
};
store = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: fakeMozLoop
});
});
it("should fetch the window type from the mozLoop API", function() {
dispatcher.dispatch(new sharedActions.GetWindowData(fakeGetWindowData));
expect(store.getStoreState()).eql({windowType: "incoming"});
});
it("should dispatch a SetupWindowData action with the data from the mozLoop API",
function() {
sandbox.stub(dispatcher, "dispatch");
store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetupWindowData({
windowData: fakeCallData
}));
});
});
});

View File

@ -438,10 +438,10 @@ describe("loop.conversationViews", function () {
loop.conversationViews.CallFailedView);
});
it("should render the PendingConversationView when the call state is 'init'",
it("should render the PendingConversationView when the call state is 'gather'",
function() {
store.set({
callState: CALL_STATES.INIT,
callState: CALL_STATES.GATHER,
contact: contact
});
@ -474,7 +474,7 @@ describe("loop.conversationViews", function () {
it("should update the rendered views when the state is changed.",
function() {
store.set({
callState: CALL_STATES.INIT,
callState: CALL_STATES.GATHER,
contact: contact
});

View File

@ -80,7 +80,7 @@ describe("loop.conversation", function() {
sandbox.stub(loop.shared.utils.Helper.prototype,
"locationData").returns({
hash: "#incoming/42",
hash: "#42",
pathname: "/"
});
@ -112,86 +112,30 @@ describe("loop.conversation", function() {
}));
});
describe("when locationHash begins with #room", function () {
// XXX must stay in sync with "test.alwaysUseRooms" pref check
// in conversation.jsx:init until we remove that code, which should
// happen in the second patch in bug 1074686, at which time this comment
// can go away as well.
var fakeRoomID = "32";
beforeEach(function() {
loop.shared.utils.Helper.prototype.locationData
.returns({
hash: "#room/" + fakeRoomID,
pathname: ""
});
sandbox.stub(loop.store, "LocalRoomStore");
});
it("should create a localRoomStore", function() {
loop.conversation.init();
sinon.assert.calledOnce(loop.store.LocalRoomStore);
sinon.assert.calledWithNew(loop.store.LocalRoomStore);
sinon.assert.calledWithExactly(loop.store.LocalRoomStore,
sinon.match({
dispatcher: sinon.match.instanceOf(loop.Dispatcher),
mozLoop: sinon.match.same(navigator.mozLoop)
}));
});
it("should dispatch SetupEmptyRoom with localRoomId from locationHash",
function() {
loop.conversation.init();
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new loop.shared.actions.SetupEmptyRoom({localRoomId: fakeRoomID}));
});
});
it("should trigger a gatherCallData action", function() {
it("should trigger a getWindowData action", function() {
loop.conversation.init();
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new loop.shared.actions.GatherCallData({
windowId: "42",
outgoing: false
new loop.shared.actions.GetWindowData({
windowId: "42"
}));
});
it("should trigger an outgoing gatherCallData action for outgoing calls",
function() {
loop.shared.utils.Helper.prototype.locationData.returns({
hash: "#outgoing/24",
pathname: "/"
});
loop.conversation.init();
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new loop.shared.actions.GatherCallData({
windowId: "24",
outgoing: true
}));
});
});
describe("ConversationControllerView", function() {
var store, conversation, client, ccView, oldTitle, dispatcher;
describe("AppControllerView", function() {
var conversationStore, conversation, client, ccView, oldTitle, dispatcher;
var conversationAppStore, localRoomStore;
function mountTestComponent(localRoomStore) {
function mountTestComponent() {
return TestUtils.renderIntoDocument(
loop.conversation.AppControllerView({
client: client,
conversation: conversation,
localRoomStore: localRoomStore,
sdk: {},
store: store
conversationStore: conversationStore,
conversationAppStore: conversationAppStore
}));
}
@ -202,7 +146,7 @@ describe("loop.conversation", function() {
sdk: {}
});
dispatcher = new loop.Dispatcher();
store = new loop.store.ConversationStore({
conversationStore = new loop.store.ConversationStore({
contact: {
name: [ "Mr Smith" ],
email: [{
@ -216,6 +160,14 @@ describe("loop.conversation", function() {
dispatcher: dispatcher,
sdkDriver: {}
});
localRoomStore = new loop.store.LocalRoomStore({
mozLoop: navigator.mozLoop,
dispatcher: dispatcher
});
conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
});
afterEach(function() {
@ -224,7 +176,7 @@ describe("loop.conversation", function() {
});
it("should display the OutgoingConversationView for outgoing calls", function() {
store.set({outgoing: true});
conversationAppStore.setStoreState({windowType: "outgoing"});
ccView = mountTestComponent();
@ -233,7 +185,7 @@ describe("loop.conversation", function() {
});
it("should display the IncomingConversationView for incoming calls", function() {
store.set({outgoing: false});
conversationAppStore.setStoreState({windowType: "incoming"});
ccView = mountTestComponent();
@ -242,20 +194,22 @@ describe("loop.conversation", function() {
});
it("should display the EmptyRoomView for rooms", function() {
navigator.mozLoop.rooms = {
addCallback: function() {},
removeCallback: function() {}
};
var localRoomStore = new loop.store.LocalRoomStore({
mozLoop: navigator.mozLoop,
dispatcher: dispatcher
});
conversationAppStore.setStoreState({windowType: "room"});
ccView = mountTestComponent(localRoomStore);
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.roomViews.EmptyRoomView);
});
it("should display the GenericFailureView for failures", function() {
conversationAppStore.setStoreState({windowType: "failed"});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.conversation.GenericFailureView);
});
});
describe("IncomingConversationView", function() {
@ -700,7 +654,7 @@ describe("loop.conversation", function() {
conversation.trigger("session:network-disconnected");
TestUtils.findRenderedComponentWithType(icView,
loop.conversation.IncomingCallFailedView);
loop.conversation.GenericFailureView);
});
it("should update the conversation window toolbar title",

View File

@ -46,6 +46,7 @@
<script src="../../content/shared/js/roomListStore.js"></script>
<script src="../../content/js/client.js"></script>
<script src="../../content/shared/js/localRoomStore.js"></script>
<script src="../../content/js/conversationAppStore.js"></script>
<script src="../../content/js/roomViews.js"></script>
<script src="../../content/js/conversationViews.js"></script>
<script src="../../content/js/conversation.js"></script>
@ -53,6 +54,7 @@
<script src="../../content/js/panel.js"></script>
<!-- Test scripts -->
<script src="conversationAppStore_test.js"></script>
<script src="client_test.js"></script>
<script src="conversation_test.js"></script>
<script src="panel_test.js"></script>

View File

@ -221,40 +221,31 @@ describe("loop.store.ConversationStore", function () {
});
});
describe("#gatherCallData", function() {
describe("#setupWindowData", function() {
var fakeSetupWindowData;
beforeEach(function() {
store.set({callState: CALL_STATES.INIT});
navigator.mozLoop = {
getCallData: function() {
return {
contact: contact,
callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
};
fakeSetupWindowData = {
windowData: {
type: "outgoing",
contact: contact,
windowId: "123456",
callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
}
};
});
afterEach(function() {
delete navigator.mozLoop;
});
it("should set the state to 'gather'", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData({
windowId: "76543218",
outgoing: true
}));
new sharedActions.SetupWindowData(fakeSetupWindowData));
expect(store.get("callState")).eql(CALL_STATES.GATHER);
});
it("should save the basic call information", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData({
windowId: "123456",
outgoing: true
}));
new sharedActions.SetupWindowData(fakeSetupWindowData));
expect(store.get("windowId")).eql("123456");
expect(store.get("outgoing")).eql(true);
@ -262,28 +253,16 @@ describe("loop.store.ConversationStore", function () {
it("should save the basic information from the mozLoop api", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData({
windowId: "123456",
outgoing: true
}));
new sharedActions.SetupWindowData(fakeSetupWindowData));
expect(store.get("contact")).eql(contact);
expect(store.get("callType")).eql(sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
describe("outgoing calls", function() {
var outgoingCallData;
beforeEach(function() {
outgoingCallData = {
windowId: "123456",
outgoing: true
};
});
it("should request the outgoing call data", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
@ -291,7 +270,7 @@ describe("loop.store.ConversationStore", function () {
});
it("should include all email addresses in the call data", function() {
contact = {
fakeSetupWindowData.windowData.contact = {
name: [ "Mr Smith" ],
email: [{
type: "home",
@ -306,7 +285,7 @@ describe("loop.store.ConversationStore", function () {
};
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
@ -314,7 +293,7 @@ describe("loop.store.ConversationStore", function () {
});
it("should include trim phone numbers for the call data", function() {
contact = {
fakeSetupWindowData.windowData.contact = {
name: [ "Mr Smith" ],
tel: [{
type: "home",
@ -324,7 +303,7 @@ describe("loop.store.ConversationStore", function () {
};
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
@ -332,7 +311,7 @@ describe("loop.store.ConversationStore", function () {
});
it("should include all email and telephone values in the call data", function() {
contact = {
fakeSetupWindowData.windowData.contact = {
name: [ "Mr Smith" ],
email: [{
type: "home",
@ -355,7 +334,7 @@ describe("loop.store.ConversationStore", function () {
};
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
@ -375,8 +354,8 @@ describe("loop.store.ConversationStore", function () {
client.setupOutgoingCall.callsArgWith(2, null, callData);
store.gatherCallData(
new sharedActions.GatherCallData(outgoingCallData));
store.setupWindowData(
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action
@ -389,8 +368,8 @@ describe("loop.store.ConversationStore", function () {
it("should dispatch a connection failure action on failure", function() {
client.setupOutgoingCall.callsArgWith(2, {});
store.gatherCallData(
new sharedActions.GatherCallData(outgoingCallData));
store.setupWindowData(
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action

View File

@ -22,31 +22,30 @@ describe("loop.Dispatcher", function () {
it("should register a store against an action name", function() {
var object = { fake: true };
dispatcher.register(object, ["gatherCallData"]);
dispatcher.register(object, ["getWindowData"]);
expect(dispatcher._eventData["gatherCallData"][0]).eql(object);
expect(dispatcher._eventData["getWindowData"][0]).eql(object);
});
it("should register multiple store against an action name", function() {
var object1 = { fake: true };
var object2 = { fake2: true };
dispatcher.register(object1, ["gatherCallData"]);
dispatcher.register(object2, ["gatherCallData"]);
dispatcher.register(object1, ["getWindowData"]);
dispatcher.register(object2, ["getWindowData"]);
expect(dispatcher._eventData["gatherCallData"][0]).eql(object1);
expect(dispatcher._eventData["gatherCallData"][1]).eql(object2);
expect(dispatcher._eventData["getWindowData"][0]).eql(object1);
expect(dispatcher._eventData["getWindowData"][1]).eql(object2);
});
});
describe("#dispatch", function() {
var gatherStore1, gatherStore2, cancelStore1, connectStore1;
var gatherAction, cancelAction, connectAction, resolveCancelStore1;
var getDataStore1, getDataStore2, cancelStore1, connectStore1;
var getDataAction, cancelAction, connectAction, resolveCancelStore1;
beforeEach(function() {
gatherAction = new sharedActions.GatherCallData({
windowId: "42",
outgoing: false
getDataAction = new sharedActions.GetWindowData({
windowId: "42"
});
cancelAction = new sharedActions.CancelCall();
@ -54,11 +53,11 @@ describe("loop.Dispatcher", function () {
sessionData: {}
});
gatherStore1 = {
gatherCallData: sinon.stub()
getDataStore1 = {
getWindowData: sinon.stub()
};
gatherStore2 = {
gatherCallData: sinon.stub()
getDataStore2 = {
getWindowData: sinon.stub()
};
cancelStore1 = {
cancelCall: sinon.stub()
@ -67,8 +66,8 @@ describe("loop.Dispatcher", function () {
connectCall: function() {}
};
dispatcher.register(gatherStore1, ["gatherCallData"]);
dispatcher.register(gatherStore2, ["gatherCallData"]);
dispatcher.register(getDataStore1, ["getWindowData"]);
dispatcher.register(getDataStore2, ["getWindowData"]);
dispatcher.register(cancelStore1, ["cancelCall"]);
dispatcher.register(connectStore1, ["connectCall"]);
});
@ -76,33 +75,33 @@ describe("loop.Dispatcher", function () {
it("should dispatch an action to the required object", function() {
dispatcher.dispatch(cancelAction);
sinon.assert.notCalled(gatherStore1.gatherCallData);
sinon.assert.notCalled(getDataStore1.getWindowData);
sinon.assert.calledOnce(cancelStore1.cancelCall);
sinon.assert.calledWithExactly(cancelStore1.cancelCall, cancelAction);
sinon.assert.notCalled(gatherStore2.gatherCallData);
sinon.assert.notCalled(getDataStore2.getWindowData);
});
it("should dispatch actions to multiple objects", function() {
dispatcher.dispatch(gatherAction);
dispatcher.dispatch(getDataAction);
sinon.assert.calledOnce(gatherStore1.gatherCallData);
sinon.assert.calledWithExactly(gatherStore1.gatherCallData, gatherAction);
sinon.assert.calledOnce(getDataStore1.getWindowData);
sinon.assert.calledWithExactly(getDataStore1.getWindowData, getDataAction);
sinon.assert.notCalled(cancelStore1.cancelCall);
sinon.assert.calledOnce(gatherStore2.gatherCallData);
sinon.assert.calledWithExactly(gatherStore2.gatherCallData, gatherAction);
sinon.assert.calledOnce(getDataStore2.getWindowData);
sinon.assert.calledWithExactly(getDataStore2.getWindowData, getDataAction);
});
it("should dispatch multiple actions", function() {
dispatcher.dispatch(cancelAction);
dispatcher.dispatch(gatherAction);
dispatcher.dispatch(getDataAction);
sinon.assert.calledOnce(cancelStore1.cancelCall);
sinon.assert.calledOnce(gatherStore1.gatherCallData);
sinon.assert.calledOnce(gatherStore2.gatherCallData);
sinon.assert.calledOnce(getDataStore1.getWindowData);
sinon.assert.calledOnce(getDataStore2.getWindowData);
});
describe("Queued actions", function() {
@ -110,10 +109,10 @@ describe("loop.Dispatcher", function () {
// Restore the stub, so that we can easily add a function to be
// returned. Unfortunately, sinon doesn't make this easy.
sandbox.stub(connectStore1, "connectCall", function() {
dispatcher.dispatch(gatherAction);
dispatcher.dispatch(getDataAction);
sinon.assert.notCalled(gatherStore1.gatherCallData);
sinon.assert.notCalled(gatherStore2.gatherCallData);
sinon.assert.notCalled(getDataStore1.getWindowData);
sinon.assert.notCalled(getDataStore2.getWindowData);
});
});
@ -132,8 +131,8 @@ describe("loop.Dispatcher", function () {
sinon.assert.calledOnce(connectStore1.connectCall);
// These should be called, because the dispatcher synchronously queues actions.
sinon.assert.calledOnce(gatherStore1.gatherCallData);
sinon.assert.calledOnce(gatherStore2.gatherCallData);
sinon.assert.calledOnce(getDataStore1.getWindowData);
sinon.assert.calledOnce(getDataStore2.getWindowData);
});
});
});

View File

@ -31,14 +31,15 @@ describe("loop.store.LocalRoomStore", function () {
});
});
describe("#setupEmptyRoom", function() {
describe("#setupWindowData", function() {
var store, fakeMozLoop, fakeRoomId, fakeRoomName;
beforeEach(function() {
fakeRoomId = "337-ff-54";
fakeRoomName = "Monkeys";
fakeMozLoop = {
rooms: { getRoomData: sandbox.stub() }
rooms: { getRoomData: sandbox.stub() },
getLoopBoolPref: function () { return false; }
};
store = new loop.store.LocalRoomStore(
@ -57,8 +58,12 @@ describe("loop.store.LocalRoomStore", function () {
done();
});
dispatcher.dispatch(new sharedActions.SetupEmptyRoom(
{localRoomId: fakeRoomId}));
dispatcher.dispatch(new sharedActions.SetupWindowData({
windowData: {
type: "room",
localRoomId: fakeRoomId
}
}));
});
it("should set localRoomId on the store from the action data",
@ -70,9 +75,13 @@ describe("loop.store.LocalRoomStore", function () {
done();
});
dispatcher.dispatch(
new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
});
dispatcher.dispatch(new sharedActions.SetupWindowData({
windowData: {
type: "room",
localRoomId: fakeRoomId
}
}));
});
it("should set serverData.roomName from the getRoomData callback",
function(done) {
@ -83,8 +92,12 @@ describe("loop.store.LocalRoomStore", function () {
done();
});
dispatcher.dispatch(
new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
dispatcher.dispatch(new sharedActions.SetupWindowData({
windowData: {
type: "room",
localRoomId: fakeRoomId
}
}));
});
it("should set error on the store when getRoomData calls back an error",
@ -102,8 +115,12 @@ describe("loop.store.LocalRoomStore", function () {
done();
});
dispatcher.dispatch(
new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
dispatcher.dispatch(new sharedActions.SetupWindowData({
windowData: {
type: "room",
localRoomId: fakeRoomId
}
}));
});
});

View File

@ -26,7 +26,7 @@ add_task(function test_startDirectCall_opens_window() {
do_check_true(!!openedUrl, "should open a chat window");
// Stop the busy kicking in for following tests.
let callId = openedUrl.match(/about:loopconversation\#outgoing\/(.*)/)[1];
let callId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
LoopCalls.releaseCallData(callId);
});
@ -38,7 +38,7 @@ add_task(function test_startDirectCall_getCallData() {
LoopCalls.startDirectCall(contact, "audio-video");
let callId = openedUrl.match(/about:loopconversation\#outgoing\/(.*)/)[1];
let callId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
let callData = LoopCalls.getCallData(callId);