From 4bb378544569bfcd8a8e1dfc7b35985115289cba Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Wed, 24 Sep 2014 14:25:30 +0100 Subject: [PATCH] Bug 1066502 Remove the backbone router from the Loop conversation window, use a react view for control. r=nperriault --- .../components/loop/content/conversation.html | 2 - .../loop/content/js/conversation.js | 336 +++++---- .../loop/content/js/conversation.jsx | 336 +++++---- .../loop/content/js/desktopRouter.js | 35 - browser/components/loop/content/js/panel.js | 6 - browser/components/loop/content/js/panel.jsx | 6 - .../loop/content/shared/js/models.js | 6 +- .../loop/content/shared/js/router.js | 153 ---- .../loop/content/shared/js/utils.js | 22 + browser/components/loop/jar.mn | 2 - .../loop/standalone/content/js/webapp.js | 31 +- .../loop/standalone/content/js/webapp.jsx | 31 +- .../test/desktop-local/conversation_test.js | 684 ++++++++---------- .../loop/test/desktop-local/index.html | 2 - .../loop/test/desktop-local/panel_test.js | 7 - .../components/loop/test/shared/index.html | 2 - .../loop/test/shared/models_test.js | 8 +- .../loop/test/shared/router_test.js | 150 ---- .../components/loop/test/shared/utils_test.js | 34 + .../loop/test/standalone/webapp_test.js | 47 +- browser/components/loop/ui/index.html | 2 - 21 files changed, 757 insertions(+), 1145 deletions(-) delete mode 100644 browser/components/loop/content/js/desktopRouter.js delete mode 100644 browser/components/loop/content/shared/js/router.js delete mode 100644 browser/components/loop/test/shared/router_test.js diff --git a/browser/components/loop/content/conversation.html b/browser/components/loop/content/conversation.html index 149ba2e04d78..d673be68c52f 100644 --- a/browser/components/loop/content/conversation.html +++ b/browser/components/loop/content/conversation.html @@ -27,13 +27,11 @@ - - diff --git a/browser/components/loop/content/js/conversation.js b/browser/components/loop/content/js/conversation.js index 19cec5ecbce6..d94325c51c78 100644 --- a/browser/components/loop/content/js/conversation.js +++ b/browser/components/loop/content/js/conversation.js @@ -8,16 +8,11 @@ /* global loop:true, React */ var loop = loop || {}; -loop.conversation = (function(OT, mozL10n) { +loop.conversation = (function(mozL10n) { "use strict"; - var sharedViews = loop.shared.views; - - /** - * App router. - * @type {loop.desktopRouter.DesktopConversationRouter} - */ - var router; + var sharedViews = loop.shared.views, + sharedModels = loop.shared.models; var IncomingCallView = React.createClass({displayName: 'IncomingCallView', @@ -200,92 +195,183 @@ loop.conversation = (function(OT, mozL10n) { }); /** - * Conversation router. + * This view manages the incoming conversation views - from + * call initiation through to the actual conversation and call end. * - * Required options: - * - {loop.shared.models.ConversationModel} conversation Conversation model. - * - {loop.shared.models.NotificationCollection} notifications - * - * @type {loop.shared.router.BaseConversationRouter} + * At the moment, it does more than that, these parts need refactoring out. */ - var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({ - routes: { - "incoming/:callId": "incoming", - "call/accept": "accept", - "call/decline": "decline", - "call/ongoing": "conversation", - "call/declineAndBlock": "declineAndBlock", - "call/shutdown": "shutdown", - "call/feedback": "feedback" + var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView', + propTypes: { + client: React.PropTypes.instanceOf(loop.Client).isRequired, + conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) + .isRequired, + sdk: React.PropTypes.object.isRequired + }, + + getInitialState: function() { + return { + callStatus: "start" + } + }, + + componentDidMount: function() { + this.props.conversation.on("accept", this.accept, this); + this.props.conversation.on("decline", this.decline, this); + this.props.conversation.on("declineAndBlock", this.declineAndBlock, this); + this.props.conversation.on("call:accepted", this.accepted, this); + this.props.conversation.on("change:publishedStream", this._checkConnected, this); + this.props.conversation.on("change:subscribedStream", this._checkConnected, this); + this.props.conversation.on("session:ended", this.endCall, this); + this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this); + this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this); + this.props.conversation.on("session:connection-error", this._notifyError, this); + + this.setupIncomingCall(); + }, + + componentDidUnmount: function() { + this.props.conversation.off(null, null, this); + }, + + render: function() { + switch (this.state.callStatus) { + case "start": { + document.title = mozL10n.get("incoming_call_title2"); + + // XXX Don't render anything initially, though this should probably + // be some sort of pending view, whilst we connect the websocket. + return null; + } + case "incoming": { + document.title = mozL10n.get("incoming_call_title2"); + + return ( + IncomingCallView({ + model: this.props.conversation, + video: this.props.conversation.hasVideoStream("incoming")} + ) + ); + } + case "connected": { + // XXX This should be the caller id (bug 1020449) + document.title = mozL10n.get("incoming_call_title2"); + + var callType = this.props.conversation.get("selectedCallType"); + + return ( + sharedViews.ConversationView({ + initiate: true, + sdk: this.props.sdk, + model: this.props.conversation, + video: {enabled: callType !== "audio"}} + ) + ); + } + case "end": { + document.title = mozL10n.get("conversation_has_ended"); + + var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref( + "feedback.baseUrl"); + + var appVersionInfo = navigator.mozLoop.appVersionInfo; + + var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, { + product: navigator.mozLoop.getLoopCharPref("feedback.product"), + platform: appVersionInfo.OS, + channel: appVersionInfo.channel, + version: appVersionInfo.version + }); + + return ( + sharedViews.FeedbackView({ + feedbackApiClient: feedbackClient, + onAfterFeedbackReceived: this.closeWindow.bind(this)} + ) + ); + } + case "close": { + window.close(); + return (React.DOM.div(null)); + } + } }, /** - * @override {loop.shared.router.BaseConversationRouter.startCall} + * Notify the user that the connection was not possible + * @param {{code: number, message: string}} error */ - startCall: function() { - this.navigate("call/ongoing", {trigger: true}); + _notifyError: function(error) { + console.error(error); + this.props.notifications.errorL10n("connection_error_see_console_notification"); + this.setState({callStatus: "end"}); }, /** - * @override {loop.shared.router.BaseConversationRouter.endCall} + * Peer hung up. Notifies the user and ends the call. + * + * Event properties: + * - {String} connectionId: OT session id */ - endCall: function() { - navigator.mozLoop.releaseCallData(this._conversation.get("callId")); - this.navigate("call/feedback", {trigger: true}); + _onPeerHungup: function() { + this.props.notifications.warnL10n("peer_ended_conversation2"); + this.setState({callStatus: "end"}); }, - shutdown: function() { - navigator.mozLoop.releaseCallData(this._conversation.get("callId")); + /** + * Network disconnected. Notifies the user and ends the call. + */ + _onNetworkDisconnected: function() { + this.props.notifications.warnL10n("network_disconnected"); + this.setState({callStatus: "end"}); }, /** * Incoming call route. - * - * @param {String} callId Identifier assigned by the LoopService - * to this incoming call. */ - incoming: function(callId) { + setupIncomingCall: function() { navigator.mozLoop.startAlerting(); - this._conversation.once("accept", function() { - this.navigate("call/accept", {trigger: true}); - }.bind(this)); - this._conversation.once("decline", function() { - this.navigate("call/decline", {trigger: true}); - }.bind(this)); - this._conversation.once("declineAndBlock", function() { - this.navigate("call/declineAndBlock", {trigger: true}); - }.bind(this)); - this._conversation.once("call:incoming", this.startCall, this); - this._conversation.once("change:publishedStream", this._checkConnected, this); - this._conversation.once("change:subscribedStream", this._checkConnected, this); - var callData = navigator.mozLoop.getCallData(callId); + var callData = navigator.mozLoop.getCallData(this.props.conversation.get("callId")); if (!callData) { console.error("Failed to get the call data"); // XXX Not the ideal response, but bug 1047410 will be replacing // this by better "call failed" UI. - this._notifications.errorL10n("cannot_start_call_session_not_ready"); + this.props.notifications.errorL10n("cannot_start_call_session_not_ready"); return; } - this._conversation.setIncomingSessionData(callData); - this._setupWebSocketAndCallView(); + this.props.conversation.setIncomingSessionData(callData); + this._setupWebSocket(); + }, + + /** + * Starts the actual conversation + */ + accepted: function() { + this.setState({callStatus: "connected"}); + }, + + /** + * Moves the call to the end state + */ + endCall: function() { + navigator.mozLoop.releaseCallData(this.props.conversation.get("callId")); + this.setState({callStatus: "end"}); }, /** * Used to set up the web socket connection and navigate to the * call view if appropriate. */ - _setupWebSocketAndCallView: function() { + _setupWebSocket: function() { this._websocket = new loop.CallConnectionWebSocket({ - url: this._conversation.get("progressURL"), - websocketToken: this._conversation.get("websocketToken"), - callId: this._conversation.get("callId"), + url: this.props.conversation.get("progressURL"), + websocketToken: this.props.conversation.get("websocketToken"), + callId: this.props.conversation.get("callId"), }); this._websocket.promiseConnect().then(function() { - this.loadReactComponent(loop.conversation.IncomingCallView({ - model: this._conversation, - video: this._conversation.hasVideoStream("incoming") - })); + this.setState({callStatus: "incoming"}); }.bind(this), function() { this._handleSessionError(); return; @@ -301,7 +387,7 @@ loop.conversation = (function(OT, mozL10n) { _checkConnected: function() { // Check we've had both local and remote streams connected before // sending the media up message. - if (this._conversation.streamsConnected()) { + if (this.props.conversation.streamsConnected()) { this._websocket.mediaUp(); } }, @@ -337,6 +423,12 @@ loop.conversation = (function(OT, mozL10n) { _abortIncomingCall: function() { navigator.mozLoop.stopAlerting(); this._websocket.close(); + // Having a timeout here lets the logging for the websocket complete and be + // displayed on the console if both are on. + setTimeout(this.closeWindow, 0); + }, + + closeWindow: function() { window.close(); }, @@ -346,7 +438,7 @@ loop.conversation = (function(OT, mozL10n) { accept: function() { navigator.mozLoop.stopAlerting(); this._websocket.accept(); - this._conversation.incoming(); + this.props.conversation.accepted(); }, /** @@ -354,13 +446,11 @@ loop.conversation = (function(OT, mozL10n) { */ _declineCall: function() { this._websocket.decline(); - navigator.mozLoop.releaseCallData(this._conversation.get("callId")); - // XXX Don't close the window straight away, but let any sends happen - // first. Ideally we'd wait to close the window until after we have a - // response from the server, to know that everything has completed - // successfully. However, that's quite difficult to ensure at the - // moment so we'll add it later. - setTimeout(window.close, 0); + navigator.mozLoop.releaseCallData(this.props.conversation.get("callId")); + this._websocket.close(); + // Having a timeout here lets the logging for the websocket complete and be + // displayed on the console if both are on. + setTimeout(this.closeWindow, 0); }, /** @@ -379,8 +469,8 @@ loop.conversation = (function(OT, mozL10n) { */ declineAndBlock: function() { navigator.mozLoop.stopAlerting(); - var token = this._conversation.get("callToken"); - this._client.deleteCallUrl(token, function(error) { + var token = this.props.conversation.get("callToken"); + this.props.client.deleteCallUrl(token, function(error) { // XXX The conversation window will be closed when this cb is triggered // figure out if there is a better way to report the error to the user // (bug 1048909). @@ -389,62 +479,14 @@ loop.conversation = (function(OT, mozL10n) { this._declineCall(); }, - /** - * conversation is the route when the conversation is active. The start - * route should be navigated to first. - */ - conversation: function() { - if (!this._conversation.isSessionReady()) { - console.error("Error: navigated to conversation route without " + - "the start route to initialise the call first"); - this._handleSessionError(); - return; - } - - var callType = this._conversation.get("selectedCallType"); - var videoStream = callType === "audio" ? false : true; - - /*jshint newcap:false*/ - this.loadReactComponent(sharedViews.ConversationView({ - initiate: true, - sdk: OT, - model: this._conversation, - video: {enabled: videoStream} - })); - }, - /** * Handles a error starting the session */ _handleSessionError: function() { // XXX Not the ideal response, but bug 1047410 will be replacing // this by better "call failed" UI. - this._notifications.errorL10n("cannot_start_call_session_not_ready"); + this.props.notifications.errorL10n("cannot_start_call_session_not_ready"); }, - - /** - * Call has ended, display a feedback form. - */ - feedback: function() { - document.title = mozL10n.get("conversation_has_ended"); - - var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref( - "feedback.baseUrl"); - - var appVersionInfo = navigator.mozLoop.appVersionInfo; - - var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, { - product: navigator.mozLoop.getLoopCharPref("feedback.product"), - platform: appVersionInfo.OS, - channel: appVersionInfo.channel, - version: appVersionInfo.version - }); - - this.loadReactComponent(sharedViews.FeedbackView({ - feedbackApiClient: feedbackClient, - onAfterFeedbackReceived: window.close.bind(window) - })); - } }); /** @@ -457,44 +499,50 @@ loop.conversation = (function(OT, mozL10n) { // Plug in an alternate client ID mechanism, as localStorage and cookies // don't work in the conversation window - if (OT && OT.hasOwnProperty("overrideGuidStorage")) { - OT.overrideGuidStorage({ - get: function(callback) { - callback(null, navigator.mozLoop.getLoopCharPref("ot.guid")); - }, - set: function(guid, callback) { - navigator.mozLoop.setLoopCharPref("ot.guid", guid); - callback(null); - } - }); - } - - document.title = mozL10n.get("incoming_call_title2"); + window.OT.overrideGuidStorage({ + get: function(callback) { + callback(null, navigator.mozLoop.getLoopCharPref("ot.guid")); + }, + set: function(guid, callback) { + navigator.mozLoop.setLoopCharPref("ot.guid", guid); + callback(null); + } + }); document.body.classList.add(loop.shared.utils.getTargetPlatform()); var client = new loop.Client(); - router = new ConversationRouter({ - client: client, - conversation: new loop.shared.models.ConversationModel( - {}, // Model attributes - {sdk: OT}), // Model dependencies - notifications: new loop.shared.models.NotificationCollection() - }); + var conversation = new sharedModels.ConversationModel( + {}, // Model attributes + {sdk: window.OT} // Model dependencies + ); + var notifications = new sharedModels.NotificationCollection(); window.addEventListener("unload", function(event) { // Handle direct close of dialog box via [x] control. - navigator.mozLoop.releaseCallData(router._conversation.get("callId")); + navigator.mozLoop.releaseCallData(conversation.get("callId")); }); - Backbone.history.start(); + // Obtain the callId and pass it to the conversation + var helper = new loop.shared.utils.Helper(); + var locationHash = helper.locationHash(); + if (locationHash) { + conversation.set("callId", locationHash.match(/\#incoming\/(.*)/)[1]); + } + + React.renderComponent(IncomingConversationView({ + client: client, + conversation: conversation, + notifications: notifications, + sdk: window.OT} + ), document.querySelector('#main')); } return { - ConversationRouter: ConversationRouter, + IncomingConversationView: IncomingConversationView, IncomingCallView: IncomingCallView, init: init }; -})(window.OT, document.mozL10n); +})(document.mozL10n); document.addEventListener('DOMContentLoaded', loop.conversation.init); diff --git a/browser/components/loop/content/js/conversation.jsx b/browser/components/loop/content/js/conversation.jsx index 27a9797ad74e..9adbae363102 100644 --- a/browser/components/loop/content/js/conversation.jsx +++ b/browser/components/loop/content/js/conversation.jsx @@ -8,16 +8,11 @@ /* global loop:true, React */ var loop = loop || {}; -loop.conversation = (function(OT, mozL10n) { +loop.conversation = (function(mozL10n) { "use strict"; - var sharedViews = loop.shared.views; - - /** - * App router. - * @type {loop.desktopRouter.DesktopConversationRouter} - */ - var router; + var sharedViews = loop.shared.views, + sharedModels = loop.shared.models; var IncomingCallView = React.createClass({ @@ -200,92 +195,183 @@ loop.conversation = (function(OT, mozL10n) { }); /** - * Conversation router. + * This view manages the incoming conversation views - from + * call initiation through to the actual conversation and call end. * - * Required options: - * - {loop.shared.models.ConversationModel} conversation Conversation model. - * - {loop.shared.models.NotificationCollection} notifications - * - * @type {loop.shared.router.BaseConversationRouter} + * At the moment, it does more than that, these parts need refactoring out. */ - var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({ - routes: { - "incoming/:callId": "incoming", - "call/accept": "accept", - "call/decline": "decline", - "call/ongoing": "conversation", - "call/declineAndBlock": "declineAndBlock", - "call/shutdown": "shutdown", - "call/feedback": "feedback" + var IncomingConversationView = React.createClass({ + propTypes: { + client: React.PropTypes.instanceOf(loop.Client).isRequired, + conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) + .isRequired, + sdk: React.PropTypes.object.isRequired + }, + + getInitialState: function() { + return { + callStatus: "start" + } + }, + + componentDidMount: function() { + this.props.conversation.on("accept", this.accept, this); + this.props.conversation.on("decline", this.decline, this); + this.props.conversation.on("declineAndBlock", this.declineAndBlock, this); + this.props.conversation.on("call:accepted", this.accepted, this); + this.props.conversation.on("change:publishedStream", this._checkConnected, this); + this.props.conversation.on("change:subscribedStream", this._checkConnected, this); + this.props.conversation.on("session:ended", this.endCall, this); + this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this); + this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this); + this.props.conversation.on("session:connection-error", this._notifyError, this); + + this.setupIncomingCall(); + }, + + componentDidUnmount: function() { + this.props.conversation.off(null, null, this); + }, + + render: function() { + switch (this.state.callStatus) { + case "start": { + document.title = mozL10n.get("incoming_call_title2"); + + // XXX Don't render anything initially, though this should probably + // be some sort of pending view, whilst we connect the websocket. + return null; + } + case "incoming": { + document.title = mozL10n.get("incoming_call_title2"); + + return ( + + ); + } + case "connected": { + // XXX This should be the caller id (bug 1020449) + document.title = mozL10n.get("incoming_call_title2"); + + var callType = this.props.conversation.get("selectedCallType"); + + return ( + + ); + } + case "end": { + document.title = mozL10n.get("conversation_has_ended"); + + var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref( + "feedback.baseUrl"); + + var appVersionInfo = navigator.mozLoop.appVersionInfo; + + var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, { + product: navigator.mozLoop.getLoopCharPref("feedback.product"), + platform: appVersionInfo.OS, + channel: appVersionInfo.channel, + version: appVersionInfo.version + }); + + return ( + + ); + } + case "close": { + window.close(); + return (
); + } + } }, /** - * @override {loop.shared.router.BaseConversationRouter.startCall} + * Notify the user that the connection was not possible + * @param {{code: number, message: string}} error */ - startCall: function() { - this.navigate("call/ongoing", {trigger: true}); + _notifyError: function(error) { + console.error(error); + this.props.notifications.errorL10n("connection_error_see_console_notification"); + this.setState({callStatus: "end"}); }, /** - * @override {loop.shared.router.BaseConversationRouter.endCall} + * Peer hung up. Notifies the user and ends the call. + * + * Event properties: + * - {String} connectionId: OT session id */ - endCall: function() { - navigator.mozLoop.releaseCallData(this._conversation.get("callId")); - this.navigate("call/feedback", {trigger: true}); + _onPeerHungup: function() { + this.props.notifications.warnL10n("peer_ended_conversation2"); + this.setState({callStatus: "end"}); }, - shutdown: function() { - navigator.mozLoop.releaseCallData(this._conversation.get("callId")); + /** + * Network disconnected. Notifies the user and ends the call. + */ + _onNetworkDisconnected: function() { + this.props.notifications.warnL10n("network_disconnected"); + this.setState({callStatus: "end"}); }, /** * Incoming call route. - * - * @param {String} callId Identifier assigned by the LoopService - * to this incoming call. */ - incoming: function(callId) { + setupIncomingCall: function() { navigator.mozLoop.startAlerting(); - this._conversation.once("accept", function() { - this.navigate("call/accept", {trigger: true}); - }.bind(this)); - this._conversation.once("decline", function() { - this.navigate("call/decline", {trigger: true}); - }.bind(this)); - this._conversation.once("declineAndBlock", function() { - this.navigate("call/declineAndBlock", {trigger: true}); - }.bind(this)); - this._conversation.once("call:incoming", this.startCall, this); - this._conversation.once("change:publishedStream", this._checkConnected, this); - this._conversation.once("change:subscribedStream", this._checkConnected, this); - var callData = navigator.mozLoop.getCallData(callId); + var callData = navigator.mozLoop.getCallData(this.props.conversation.get("callId")); if (!callData) { console.error("Failed to get the call data"); // XXX Not the ideal response, but bug 1047410 will be replacing // this by better "call failed" UI. - this._notifications.errorL10n("cannot_start_call_session_not_ready"); + this.props.notifications.errorL10n("cannot_start_call_session_not_ready"); return; } - this._conversation.setIncomingSessionData(callData); - this._setupWebSocketAndCallView(); + this.props.conversation.setIncomingSessionData(callData); + this._setupWebSocket(); + }, + + /** + * Starts the actual conversation + */ + accepted: function() { + this.setState({callStatus: "connected"}); + }, + + /** + * Moves the call to the end state + */ + endCall: function() { + navigator.mozLoop.releaseCallData(this.props.conversation.get("callId")); + this.setState({callStatus: "end"}); }, /** * Used to set up the web socket connection and navigate to the * call view if appropriate. */ - _setupWebSocketAndCallView: function() { + _setupWebSocket: function() { this._websocket = new loop.CallConnectionWebSocket({ - url: this._conversation.get("progressURL"), - websocketToken: this._conversation.get("websocketToken"), - callId: this._conversation.get("callId"), + url: this.props.conversation.get("progressURL"), + websocketToken: this.props.conversation.get("websocketToken"), + callId: this.props.conversation.get("callId"), }); this._websocket.promiseConnect().then(function() { - this.loadReactComponent(loop.conversation.IncomingCallView({ - model: this._conversation, - video: this._conversation.hasVideoStream("incoming") - })); + this.setState({callStatus: "incoming"}); }.bind(this), function() { this._handleSessionError(); return; @@ -301,7 +387,7 @@ loop.conversation = (function(OT, mozL10n) { _checkConnected: function() { // Check we've had both local and remote streams connected before // sending the media up message. - if (this._conversation.streamsConnected()) { + if (this.props.conversation.streamsConnected()) { this._websocket.mediaUp(); } }, @@ -337,6 +423,12 @@ loop.conversation = (function(OT, mozL10n) { _abortIncomingCall: function() { navigator.mozLoop.stopAlerting(); this._websocket.close(); + // Having a timeout here lets the logging for the websocket complete and be + // displayed on the console if both are on. + setTimeout(this.closeWindow, 0); + }, + + closeWindow: function() { window.close(); }, @@ -346,7 +438,7 @@ loop.conversation = (function(OT, mozL10n) { accept: function() { navigator.mozLoop.stopAlerting(); this._websocket.accept(); - this._conversation.incoming(); + this.props.conversation.accepted(); }, /** @@ -354,13 +446,11 @@ loop.conversation = (function(OT, mozL10n) { */ _declineCall: function() { this._websocket.decline(); - navigator.mozLoop.releaseCallData(this._conversation.get("callId")); - // XXX Don't close the window straight away, but let any sends happen - // first. Ideally we'd wait to close the window until after we have a - // response from the server, to know that everything has completed - // successfully. However, that's quite difficult to ensure at the - // moment so we'll add it later. - setTimeout(window.close, 0); + navigator.mozLoop.releaseCallData(this.props.conversation.get("callId")); + this._websocket.close(); + // Having a timeout here lets the logging for the websocket complete and be + // displayed on the console if both are on. + setTimeout(this.closeWindow, 0); }, /** @@ -379,8 +469,8 @@ loop.conversation = (function(OT, mozL10n) { */ declineAndBlock: function() { navigator.mozLoop.stopAlerting(); - var token = this._conversation.get("callToken"); - this._client.deleteCallUrl(token, function(error) { + var token = this.props.conversation.get("callToken"); + this.props.client.deleteCallUrl(token, function(error) { // XXX The conversation window will be closed when this cb is triggered // figure out if there is a better way to report the error to the user // (bug 1048909). @@ -389,62 +479,14 @@ loop.conversation = (function(OT, mozL10n) { this._declineCall(); }, - /** - * conversation is the route when the conversation is active. The start - * route should be navigated to first. - */ - conversation: function() { - if (!this._conversation.isSessionReady()) { - console.error("Error: navigated to conversation route without " + - "the start route to initialise the call first"); - this._handleSessionError(); - return; - } - - var callType = this._conversation.get("selectedCallType"); - var videoStream = callType === "audio" ? false : true; - - /*jshint newcap:false*/ - this.loadReactComponent(sharedViews.ConversationView({ - initiate: true, - sdk: OT, - model: this._conversation, - video: {enabled: videoStream} - })); - }, - /** * Handles a error starting the session */ _handleSessionError: function() { // XXX Not the ideal response, but bug 1047410 will be replacing // this by better "call failed" UI. - this._notifications.errorL10n("cannot_start_call_session_not_ready"); + this.props.notifications.errorL10n("cannot_start_call_session_not_ready"); }, - - /** - * Call has ended, display a feedback form. - */ - feedback: function() { - document.title = mozL10n.get("conversation_has_ended"); - - var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref( - "feedback.baseUrl"); - - var appVersionInfo = navigator.mozLoop.appVersionInfo; - - var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, { - product: navigator.mozLoop.getLoopCharPref("feedback.product"), - platform: appVersionInfo.OS, - channel: appVersionInfo.channel, - version: appVersionInfo.version - }); - - this.loadReactComponent(sharedViews.FeedbackView({ - feedbackApiClient: feedbackClient, - onAfterFeedbackReceived: window.close.bind(window) - })); - } }); /** @@ -457,44 +499,50 @@ loop.conversation = (function(OT, mozL10n) { // Plug in an alternate client ID mechanism, as localStorage and cookies // don't work in the conversation window - if (OT && OT.hasOwnProperty("overrideGuidStorage")) { - OT.overrideGuidStorage({ - get: function(callback) { - callback(null, navigator.mozLoop.getLoopCharPref("ot.guid")); - }, - set: function(guid, callback) { - navigator.mozLoop.setLoopCharPref("ot.guid", guid); - callback(null); - } - }); - } - - document.title = mozL10n.get("incoming_call_title2"); + window.OT.overrideGuidStorage({ + get: function(callback) { + callback(null, navigator.mozLoop.getLoopCharPref("ot.guid")); + }, + set: function(guid, callback) { + navigator.mozLoop.setLoopCharPref("ot.guid", guid); + callback(null); + } + }); document.body.classList.add(loop.shared.utils.getTargetPlatform()); var client = new loop.Client(); - router = new ConversationRouter({ - client: client, - conversation: new loop.shared.models.ConversationModel( - {}, // Model attributes - {sdk: OT}), // Model dependencies - notifications: new loop.shared.models.NotificationCollection() - }); + var conversation = new sharedModels.ConversationModel( + {}, // Model attributes + {sdk: window.OT} // Model dependencies + ); + var notifications = new sharedModels.NotificationCollection(); window.addEventListener("unload", function(event) { // Handle direct close of dialog box via [x] control. - navigator.mozLoop.releaseCallData(router._conversation.get("callId")); + navigator.mozLoop.releaseCallData(conversation.get("callId")); }); - Backbone.history.start(); + // Obtain the callId and pass it to the conversation + var helper = new loop.shared.utils.Helper(); + var locationHash = helper.locationHash(); + if (locationHash) { + conversation.set("callId", locationHash.match(/\#incoming\/(.*)/)[1]); + } + + React.renderComponent(, document.querySelector('#main')); } return { - ConversationRouter: ConversationRouter, + IncomingConversationView: IncomingConversationView, IncomingCallView: IncomingCallView, init: init }; -})(window.OT, document.mozL10n); +})(document.mozL10n); document.addEventListener('DOMContentLoaded', loop.conversation.init); diff --git a/browser/components/loop/content/js/desktopRouter.js b/browser/components/loop/content/js/desktopRouter.js deleted file mode 100644 index 8adc7da2d877..000000000000 --- a/browser/components/loop/content/js/desktopRouter.js +++ /dev/null @@ -1,35 +0,0 @@ -/* 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/. */ - -/* jshint esnext:true */ -/* global loop:true */ - -var loop = loop || {}; -loop.desktopRouter = (function() { - "use strict"; - - /** - * On the desktop app, the use of about: uris prevents us from changing the - * url of the location. As a result, we change the navigate function to simply - * activate the new routes, and not try changing the url. - * - * XXX It is conceivable we might be able to remove this in future, if we - * can either swap to resource uris or remove the limitation on the about uris. - */ - var extendedRouter = { - navigate: function(to) { - this[this.routes[to]](); - } - }; - - var DesktopRouter = loop.shared.router.BaseRouter.extend(extendedRouter); - - var DesktopConversationRouter = - loop.shared.router.BaseConversationRouter.extend(extendedRouter); - - return { - DesktopRouter: DesktopRouter, - DesktopConversationRouter: DesktopConversationRouter - }; -})(); diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index b5c0ebebe5be..e732858c9e00 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -17,12 +17,6 @@ loop.panel = (function(_, mozL10n) { var ContactsList = loop.contacts.ContactsList; var __ = mozL10n.get; // aliasing translation function as __ for concision - /** - * Panel router. - * @type {loop.desktopRouter.DesktopRouter} - */ - var router; - var TabView = React.createClass({displayName: 'TabView', getInitialState: function() { return { diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx index 4a8f364c3fda..52224a31ac24 100644 --- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -17,12 +17,6 @@ loop.panel = (function(_, mozL10n) { var ContactsList = loop.contacts.ContactsList; var __ = mozL10n.get; // aliasing translation function as __ for concision - /** - * Panel router. - * @type {loop.desktopRouter.DesktopRouter} - */ - var router; - var TabView = React.createClass({ getInitialState: function() { return { diff --git a/browser/components/loop/content/shared/js/models.js b/browser/components/loop/content/shared/js/models.js index 63092d9f83c6..66258d04248c 100644 --- a/browser/components/loop/content/shared/js/models.js +++ b/browser/components/loop/content/shared/js/models.js @@ -77,10 +77,10 @@ loop.shared.models = (function(l10n) { }, /** - * Starts an incoming conversation. + * Indicates an incoming conversation has been accepted. */ - incoming: function() { - this.trigger("call:incoming"); + accepted: function() { + this.trigger("call:accepted"); }, /** diff --git a/browser/components/loop/content/shared/js/router.js b/browser/components/loop/content/shared/js/router.js deleted file mode 100644 index 6329ea31ab81..000000000000 --- a/browser/components/loop/content/shared/js/router.js +++ /dev/null @@ -1,153 +0,0 @@ -/* 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.shared = loop.shared || {}; -loop.shared.router = (function() { - "use strict"; - - /** - * Base Router. Allows defining a main active view and ease toggling it when - * the active route changes. - * - * @link http://mikeygee.com/blog/backbone.html - */ - var BaseRouter = Backbone.Router.extend({ - /** - * Notifications collection. - * @type {loop.shared.models.NotificationCollection} - */ - _notifications: undefined, - - /** - * Constructor. - * - * Required options: - * - {loop.shared.models.NotificationCollection} notifications - * - * @param {Object} options Options object. - */ - constructor: function(options) { - options = options || {}; - if (!options.notifications) { - throw new Error("missing required notifications"); - } - this._notifications = options.notifications; - - Backbone.Router.apply(this, arguments); - }, - - /** - * Renders a React component as current active view. - * - * @param {React} reactComponent React component. - */ - loadReactComponent: function(reactComponent) { - this.clearActiveView(); - React.renderComponent(reactComponent, - document.querySelector("#main")); - }, - - /** - * Clears current active view. - */ - clearActiveView: function() { - React.unmountComponentAtNode(document.querySelector("#main")); - } - }); - - /** - * Base conversation router, implementing common behaviors when handling - * a conversation. - */ - var BaseConversationRouter = BaseRouter.extend({ - /** - * Current conversation. - * @type {loop.shared.models.ConversationModel} - */ - _conversation: undefined, - - /** - * Constructor. Defining it as `constructor` allows implementing an - * `initialize` method in child classes without needing calling this parent - * one. See http://backbonejs.org/#Model-constructor (same for Router) - * - * Required options: - * - {loop.shared.model.ConversationModel} model Conversation model. - * - * @param {Object} options Options object. - */ - constructor: function(options) { - options = options || {}; - if (!options.conversation) { - throw new Error("missing required conversation"); - } - if (!options.client) { - throw new Error("missing required client"); - } - this._conversation = options.conversation; - this._client = options.client; - - this.listenTo(this._conversation, "session:ended", this._onSessionEnded); - this.listenTo(this._conversation, "session:peer-hungup", - this._onPeerHungup); - this.listenTo(this._conversation, "session:network-disconnected", - this._onNetworkDisconnected); - this.listenTo(this._conversation, "session:connection-error", - this._notifyError); - - BaseRouter.apply(this, arguments); - }, - - /** - * Notify the user that the connection was not possible - * @param {{code: number, message: string}} error - */ - _notifyError: function(error) { - console.log(error); - this._notifications.errorL10n("connection_error_see_console_notification"); - this.endCall(); - }, - - /** - * Ends the call. This method should be overriden. - */ - endCall: function() {}, - - /** - * Session has ended. Notifies the user and ends the call. - */ - _onSessionEnded: function() { - this.endCall(); - }, - - /** - * Peer hung up. Notifies the user and ends the call. - * - * Event properties: - * - {String} connectionId: OT session id - * - * @param {Object} event - */ - _onPeerHungup: function() { - this._notifications.warnL10n("peer_ended_conversation2"); - this.endCall(); - }, - - /** - * Network disconnected. Notifies the user and ends the call. - */ - _onNetworkDisconnected: function() { - this._notifications.warnL10n("network_disconnected"); - this.endCall(); - } - }); - - return { - BaseRouter: BaseRouter, - BaseConversationRouter: BaseConversationRouter - }; -})(); diff --git a/browser/components/loop/content/shared/js/utils.js b/browser/components/loop/content/shared/js/utils.js index 84186bba0848..80478f0d13e8 100644 --- a/browser/components/loop/content/shared/js/utils.js +++ b/browser/components/loop/content/shared/js/utils.js @@ -46,7 +46,29 @@ loop.shared.utils = (function() { return !!localStorage.getItem(prefName); } + /** + * Helper for general things + */ + function Helper() { + this._iOSRegex = /^(iPad|iPhone|iPod)/; + } + + Helper.prototype = { + isFirefox: function(platform) { + return platform.indexOf("Firefox") !== -1; + }, + + isIOS: function(platform) { + return this._iOSRegex.test(platform); + }, + + locationHash: function() { + return window.location.hash; + } + }; + return { + Helper: Helper, getTargetPlatform: getTargetPlatform, getBoolPreference: getBoolPreference }; diff --git a/browser/components/loop/jar.mn b/browser/components/loop/jar.mn index 464379e145e8..3efd65675742 100644 --- a/browser/components/loop/jar.mn +++ b/browser/components/loop/jar.mn @@ -12,7 +12,6 @@ browser.jar: # Desktop script content/browser/loop/js/client.js (content/js/client.js) - content/browser/loop/js/desktopRouter.js (content/js/desktopRouter.js) content/browser/loop/js/conversation.js (content/js/conversation.js) content/browser/loop/js/otconfig.js (content/js/otconfig.js) content/browser/loop/js/panel.js (content/js/panel.js) @@ -55,7 +54,6 @@ browser.jar: # Shared scripts content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js) content/browser/loop/shared/js/models.js (content/shared/js/models.js) - content/browser/loop/shared/js/router.js (content/shared/js/router.js) content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js) content/browser/loop/shared/js/views.js (content/shared/js/views.js) content/browser/loop/shared/js/utils.js (content/shared/js/utils.js) diff --git a/browser/components/loop/standalone/content/js/webapp.js b/browser/components/loop/standalone/content/js/webapp.js index 4fcb9b51806d..732aaca3dc80 100644 --- a/browser/components/loop/standalone/content/js/webapp.js +++ b/browser/components/loop/standalone/content/js/webapp.js @@ -15,7 +15,8 @@ loop.webapp = (function($, _, OT, mozL10n) { loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000"; var sharedModels = loop.shared.models, - sharedViews = loop.shared.views; + sharedViews = loop.shared.views, + sharedUtils = loop.shared.utils; /** * Homepage view. @@ -435,7 +436,7 @@ loop.webapp = (function($, _, OT, mozL10n) { client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired, conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) .isRequired, - helper: React.PropTypes.instanceOf(WebappHelper).isRequired, + helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, sdk: React.PropTypes.object.isRequired, @@ -690,7 +691,7 @@ loop.webapp = (function($, _, OT, mozL10n) { client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired, conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) .isRequired, - helper: React.PropTypes.instanceOf(WebappHelper).isRequired, + helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, sdk: React.PropTypes.object.isRequired, @@ -726,32 +727,11 @@ loop.webapp = (function($, _, OT, mozL10n) { } }); - /** - * Local helpers. - */ - function WebappHelper() { - this._iOSRegex = /^(iPad|iPhone|iPod)/; - } - - WebappHelper.prototype = { - isFirefox: function(platform) { - return platform.indexOf("Firefox") !== -1; - }, - - isIOS: function(platform) { - return this._iOSRegex.test(platform); - }, - - locationHash: function() { - return window.location.hash; - } - }; - /** * App initialization. */ function init() { - var helper = new WebappHelper(); + var helper = new sharedUtils.Helper(); var client = new loop.StandaloneClient({ baseServerUrl: loop.config.serverUrl }); @@ -797,7 +777,6 @@ loop.webapp = (function($, _, OT, mozL10n) { UnsupportedDeviceView: UnsupportedDeviceView, init: init, PromoteFirefoxView: PromoteFirefoxView, - WebappHelper: WebappHelper, WebappRootView: WebappRootView }; })(jQuery, _, window.OT, navigator.mozL10n); diff --git a/browser/components/loop/standalone/content/js/webapp.jsx b/browser/components/loop/standalone/content/js/webapp.jsx index 4f49c6714ed6..a681d2f1f221 100644 --- a/browser/components/loop/standalone/content/js/webapp.jsx +++ b/browser/components/loop/standalone/content/js/webapp.jsx @@ -15,7 +15,8 @@ loop.webapp = (function($, _, OT, mozL10n) { loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000"; var sharedModels = loop.shared.models, - sharedViews = loop.shared.views; + sharedViews = loop.shared.views, + sharedUtils = loop.shared.utils; /** * Homepage view. @@ -435,7 +436,7 @@ loop.webapp = (function($, _, OT, mozL10n) { client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired, conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) .isRequired, - helper: React.PropTypes.instanceOf(WebappHelper).isRequired, + helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, sdk: React.PropTypes.object.isRequired, @@ -690,7 +691,7 @@ loop.webapp = (function($, _, OT, mozL10n) { client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired, conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) .isRequired, - helper: React.PropTypes.instanceOf(WebappHelper).isRequired, + helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, sdk: React.PropTypes.object.isRequired, @@ -726,32 +727,11 @@ loop.webapp = (function($, _, OT, mozL10n) { } }); - /** - * Local helpers. - */ - function WebappHelper() { - this._iOSRegex = /^(iPad|iPhone|iPod)/; - } - - WebappHelper.prototype = { - isFirefox: function(platform) { - return platform.indexOf("Firefox") !== -1; - }, - - isIOS: function(platform) { - return this._iOSRegex.test(platform); - }, - - locationHash: function() { - return window.location.hash; - } - }; - /** * App initialization. */ function init() { - var helper = new WebappHelper(); + var helper = new sharedUtils.Helper(); var client = new loop.StandaloneClient({ baseServerUrl: loop.config.serverUrl }); @@ -797,7 +777,6 @@ loop.webapp = (function($, _, OT, mozL10n) { UnsupportedDeviceView: UnsupportedDeviceView, init: init, PromoteFirefoxView: PromoteFirefoxView, - WebappHelper: WebappHelper, WebappRootView: WebappRootView }; })(jQuery, _, window.OT, navigator.mozL10n); diff --git a/browser/components/loop/test/desktop-local/conversation_test.js b/browser/components/loop/test/desktop-local/conversation_test.js index c4ba9e7f8995..218d3b1848ef 100644 --- a/browser/components/loop/test/desktop-local/conversation_test.js +++ b/browser/components/loop/test/desktop-local/conversation_test.js @@ -9,10 +9,24 @@ var expect = chai.expect; describe("loop.conversation", function() { "use strict"; - var ConversationRouter = loop.conversation.ConversationRouter, + var sharedModels = loop.shared.models, + sharedView = loop.shared.views, sandbox, notifications; + // XXX refactor to Just Work with "sandbox.stubComponent" or else + // just pass in the sandbox and put somewhere generally usable + + function stubComponent(obj, component, mockTagName){ + var reactClass = React.createClass({ + render: function() { + var mockTagName = mockTagName || "div"; + return React.DOM[mockTagName](null, this.props.children); + } + }); + return sandbox.stub(obj, component, reactClass); + } + beforeEach(function() { sandbox = sinon.sandbox.create(); sandbox.useFakeTimers(); @@ -26,14 +40,14 @@ describe("loop.conversation", function() { get locale() { return "en-US"; }, - setLoopCharPref: sandbox.stub(), - getLoopCharPref: sandbox.stub(), - getLoopBoolPref: sandbox.stub(), - getCallData: sandbox.stub(), - releaseCallData: function() {}, - startAlerting: function() {}, - stopAlerting: function() {}, - ensureRegistered: function() {}, + setLoopCharPref: sinon.stub(), + getLoopCharPref: sinon.stub(), + getLoopBoolPref: sinon.stub(), + getCallData: sinon.stub(), + releaseCallData: sinon.stub(), + startAlerting: sinon.stub(), + stopAlerting: sinon.stub(), + ensureRegistered: sinon.stub(), get appVersionInfo() { return { version: "42", @@ -57,21 +71,19 @@ describe("loop.conversation", function() { var oldTitle; beforeEach(function() { - oldTitle = document.title; - + sandbox.stub(React, "renderComponent"); sandbox.stub(document.mozL10n, "initialize"); - sandbox.stub(document.mozL10n, "get").returns("Fake title"); - sandbox.stub(loop.conversation.ConversationRouter.prototype, - "initialize"); sandbox.stub(loop.shared.models.ConversationModel.prototype, "initialize"); - sandbox.stub(Backbone.history, "start"); + window.OT = { + overrideGuidStorage: sinon.stub() + }; }); afterEach(function() { - document.title = oldTitle; + delete window.OT; }); it("should initalize L10n", function() { @@ -82,300 +94,256 @@ describe("loop.conversation", function() { navigator.mozLoop); }); - it("should set the document title", function() { + it("should create the IncomingConversationView", function() { loop.conversation.init(); - expect(document.title).to.be.equal("Fake title"); + sinon.assert.calledOnce(React.renderComponent); + sinon.assert.calledWith(React.renderComponent, + sinon.match(function(value) { + return TestUtils.isDescriptorOfType(value, + loop.conversation.IncomingConversationView); + })); }); - it("should create the router", function() { - loop.conversation.init(); - - sinon.assert.calledOnce( - loop.conversation.ConversationRouter.prototype.initialize); - }); - - it("should start Backbone history", function() { - loop.conversation.init(); - - sinon.assert.calledOnce(Backbone.history.start); - }); }); - describe("ConversationRouter", function() { - var conversation, client; + describe("IncomingConversationView", function() { + var conversation, client, icView, oldTitle; + + function mountTestComponent() { + return TestUtils.renderIntoDocument( + loop.conversation.IncomingConversationView({ + client: client, + conversation: conversation, + notifications: notifications, + sdk: {} + })); + } beforeEach(function() { + oldTitle = document.title; client = new loop.Client(); conversation = new loop.shared.models.ConversationModel({}, { sdk: {} }); - sandbox.spy(conversation, "setIncomingSessionData"); + conversation.set({callId: 42}); sandbox.stub(conversation, "setOutgoingSessionData"); }); - describe("Routes", function() { - var router; + afterEach(function() { + icView = undefined; + document.title = oldTitle; + }); + + describe("start", function() { + it("should set the title to incoming_call_title2", function() { + sandbox.stub(document.mozL10n, "get", function(x) { + return x; + }); + + icView = mountTestComponent(); + + expect(document.title).eql("incoming_call_title2"); + }); + }); + + describe("componentDidMount", function() { + var fakeSessionData; beforeEach(function() { - router = new ConversationRouter({ - client: client, - conversation: conversation, - notifications: notifications - }); - sandbox.stub(conversation, "incoming"); + fakeSessionData = { + sessionId: "sessionId", + sessionToken: "sessionToken", + apiKey: "apiKey", + callType: "callType", + callId: "Hello", + progressURL: "http://progress.example.com", + websocketToken: "7b" + }; + + navigator.mozLoop.getCallData.returns(fakeSessionData); + stubComponent(loop.conversation, "IncomingCallView"); + stubComponent(sharedView, "ConversationView"); }); - describe("#incoming", function() { + it("should start alerting", function() { + icView = mountTestComponent(); - // XXX refactor to Just Work with "sandbox.stubComponent" or else - // just pass in the sandbox and put somewhere generally usable + sinon.assert.calledOnce(navigator.mozLoop.startAlerting); + }); - function stubComponent(obj, component, mockTagName){ - var reactClass = React.createClass({ - render: function() { - var mockTagName = mockTagName || "div"; - return React.DOM[mockTagName](null, this.props.children); - } - }); - return sandbox.stub(obj, component, reactClass); - } + it("should call getCallData on navigator.mozLoop", function() { + icView = mountTestComponent(); - beforeEach(function() { - sandbox.stub(router, "loadReactComponent"); - stubComponent(loop.conversation, "IncomingCallView"); - }); + sinon.assert.calledOnce(navigator.mozLoop.getCallData); + sinon.assert.calledWith(navigator.mozLoop.getCallData, 42); + }); - it("should start alerting", function() { - sandbox.stub(navigator.mozLoop, "startAlerting"); - router.incoming("fakeVersion"); - - sinon.assert.calledOnce(navigator.mozLoop.startAlerting); - }); - - it("should call getCallData on navigator.mozLoop", - function() { - router.incoming(42); - - sinon.assert.calledOnce(navigator.mozLoop.getCallData); - sinon.assert.calledWith(navigator.mozLoop.getCallData, 42); - }); - - describe("getCallData successful", function() { - var fakeSessionData, resolvePromise, rejectPromise; + describe("getCallData successful", function() { + var promise, resolveWebSocketConnect, + rejectWebSocketConnect; + describe("Session Data setup", function() { beforeEach(function() { - fakeSessionData = { - sessionId: "sessionId", - sessionToken: "sessionToken", - apiKey: "apiKey", - callType: "callType", - callId: "Hello", - progressURL: "http://progress.example.com", - websocketToken: 123 - }; - - sandbox.stub(router, "_setupWebSocketAndCallView"); - - navigator.mozLoop.getCallData.returns(fakeSessionData); + sandbox.stub(loop, "CallConnectionWebSocket").returns({ + promiseConnect: function () { + promise = new Promise(function(resolve, reject) { + resolveWebSocketConnect = resolve; + rejectWebSocketConnect = reject; + }); + return promise; + }, + on: sinon.stub() + }); }); it("should store the session data", function() { - router.incoming("fakeVersion"); + sandbox.stub(conversation, "setIncomingSessionData"); + + icView = mountTestComponent(); sinon.assert.calledOnce(conversation.setIncomingSessionData); sinon.assert.calledWithExactly(conversation.setIncomingSessionData, fakeSessionData); }); - it("should call #_setupWebSocketAndCallView", function() { + it("should setup the websocket connection", function() { + icView = mountTestComponent(); - router.incoming("fakeVersion"); - - sinon.assert.calledOnce(router._setupWebSocketAndCallView); - sinon.assert.calledWithExactly(router._setupWebSocketAndCallView); + sinon.assert.calledOnce(loop.CallConnectionWebSocket); + sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, { + callId: "Hello", + url: "http://progress.example.com", + websocketToken: "7b" + }); }); }); - describe("#_setupWebSocketAndCallView", function() { + describe("WebSocket Handling", function() { beforeEach(function() { - conversation.setIncomingSessionData({ - sessionId: "sessionId", - sessionToken: "sessionToken", - apiKey: "apiKey", - callType: "callType", - callId: "Hello", - progressURL: "http://progress.example.com", - websocketToken: 123 + promise = new Promise(function(resolve, reject) { + resolveWebSocketConnect = resolve; + rejectWebSocketConnect = reject; + }); + + sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise); + }); + + it("should set the state to incoming on success", function(done) { + icView = mountTestComponent(); + resolveWebSocketConnect(); + + promise.then(function () { + expect(icView.state.callStatus).eql("incoming"); + done(); }); }); - describe("Websocket connection successful", function() { - var promise; + it("should display an error if the websocket failed to connect", function(done) { + sandbox.stub(notifications, "errorL10n"); + icView = mountTestComponent(); + rejectWebSocketConnect(); + + promise.then(function() { + }, function () { + sinon.assert.calledOnce(notifications.errorL10n); + sinon.assert.calledWithExactly(notifications.errorL10n, + "cannot_start_call_session_not_ready"); + done(); + }); + }); + }); + + describe("WebSocket Events", function() { + describe("Call cancelled or timed out before acceptance", function() { beforeEach(function() { - sandbox.stub(loop, "CallConnectionWebSocket").returns({ - promiseConnect: function() { - promise = new Promise(function(resolve, reject) { - resolve(); + icView = mountTestComponent(); + promise = new Promise(function(resolve, reject) { + resolve(); + }); + + sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise); + sandbox.stub(loop.CallConnectionWebSocket.prototype, "close"); + sandbox.stub(window, "close"); + }); + + describe("progress - terminated - cancel", function() { + it("should stop alerting", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "cancel" }); - return promise; - }, - on: sinon.spy() + + sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); + done(); + }); + }); + + it("should close the websocket", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "cancel" + }); + + sinon.assert.calledOnce(icView._websocket.close); + done(); + }); + }); + + it("should close the window", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "cancel" + }); + + sandbox.clock.tick(1); + + sinon.assert.calledOnce(window.close); + done(); + }); }); }); - it("should create a CallConnectionWebSocket", function(done) { - router._setupWebSocketAndCallView(); + describe("progress - terminated - timeout (previousState = alerting)", function() { + it("should stop alerting", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "timeout" + }, "alerting"); - promise.then(function () { - sinon.assert.calledOnce(loop.CallConnectionWebSocket); - sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, { - callId: "Hello", - url: "http://progress.example.com", - // The websocket token is converted to a hex string. - websocketToken: "7b" - }); - done(); - }); - }); - - it("should create the view with video=false", function(done) { - sandbox.stub(conversation, "get").withArgs("callType").returns("audio"); - - router._setupWebSocketAndCallView(); - - promise.then(function () { - sinon.assert.called(conversation.get); - sinon.assert.calledOnce(loop.conversation.IncomingCallView); - sinon.assert.calledWithExactly(loop.conversation.IncomingCallView, - {model: conversation, - video: false}); - done(); - }); - }); - }); - - describe("Websocket connection failed", function() { - var promise; - - beforeEach(function() { - sandbox.stub(loop, "CallConnectionWebSocket").returns({ - promiseConnect: function() { - promise = new Promise(function(resolve, reject) { - reject(); - }); - return promise; - }, - on: sinon.spy() - }); - }); - - it("should display an error", function(done) { - sandbox.stub(notifications, "errorL10n"); - router._setupWebSocketAndCallView(); - - promise.then(function() { - }, function () { - sinon.assert.calledOnce(router._notifications.errorL10n); - sinon.assert.calledWithExactly(router._notifications.errorL10n, - "cannot_start_call_session_not_ready"); - done(); - }); - }); - }); - - describe("Events", function() { - describe("Call cancelled or timed out before acceptance", function() { - var promise; - - beforeEach(function() { - sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect", function() { - promise = new Promise(function(resolve, reject) { - resolve(); - }); - return promise; - }); - sandbox.stub(loop.CallConnectionWebSocket.prototype, "close"); - sandbox.stub(navigator.mozLoop, "stopAlerting"); - sandbox.stub(window, "close"); - - router._setupWebSocketAndCallView(); - }); - - describe("progress - terminated - cancel", function() { - it("should stop alerting", function(done) { - promise.then(function() { - router._websocket.trigger("progress", { - state: "terminated", - reason: "cancel" - }); - - sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); - done(); - }); - }); - - it("should close the websocket", function(done) { - promise.then(function() { - router._websocket.trigger("progress", { - state: "terminated", - reason: "cancel" - }); - - sinon.assert.calledOnce(router._websocket.close); - done(); - }); - }); - - it("should close the window", function(done) { - promise.then(function() { - router._websocket.trigger("progress", { - state: "terminated", - reason: "cancel" - }); - - sinon.assert.calledOnce(window.close); - done(); - }); + sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); + done(); }); }); - describe("progress - terminated - timeout (previousState = alerting)", function() { - it("should stop alerting", function(done) { - promise.then(function() { - router._websocket.trigger("progress", { - state: "terminated", - reason: "timeout" - }, "alerting"); + it("should close the websocket", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "timeout" + }, "alerting"); - sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); - done(); - }); + sinon.assert.calledOnce(icView._websocket.close); + done(); }); + }); - it("should close the websocket", function(done) { - promise.then(function() { - router._websocket.trigger("progress", { - state: "terminated", - reason: "timeout" - }, "alerting"); + it("should close the window", function(done) { + promise.then(function() { + icView._websocket.trigger("progress", { + state: "terminated", + reason: "timeout" + }, "alerting"); - sinon.assert.calledOnce(router._websocket.close); - done(); - }); - }); + sandbox.clock.tick(1); - it("should close the window", function(done) { - promise.then(function() { - router._websocket.trigger("progress", { - state: "terminated", - reason: "timeout" - }, "alerting"); - - sinon.assert.calledOnce(window.close); - done(); - }); + sinon.assert.calledOnce(window.close); + done(); }); }); }); @@ -385,6 +353,7 @@ describe("loop.conversation", function() { describe("#accept", function() { beforeEach(function() { + icView = mountTestComponent(); conversation.setIncomingSessionData({ sessionId: "sessionId", sessionToken: "sessionToken", @@ -394,72 +363,38 @@ describe("loop.conversation", function() { progressURL: "http://progress.example.com", websocketToken: 123 }); - router._setupWebSocketAndCallView(); - sandbox.stub(router._websocket, "accept"); - sandbox.stub(navigator.mozLoop, "stopAlerting"); + sandbox.stub(icView._websocket, "accept"); + sandbox.stub(icView.props.conversation, "accepted"); }); it("should initiate the conversation", function() { - router.accept(); + icView.accept(); - sinon.assert.calledOnce(conversation.incoming); + sinon.assert.calledOnce(icView.props.conversation.accepted); }); it("should notify the websocket of the user acceptance", function() { - router.accept(); + icView.accept(); - sinon.assert.calledOnce(router._websocket.accept); + sinon.assert.calledOnce(icView._websocket.accept); }); it("should stop alerting", function() { - router.accept(); + icView.accept(); sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); }); }); - describe("#conversation", function() { - beforeEach(function() { - sandbox.stub(router, "loadReactComponent"); - }); - - it("should load the ConversationView if session is set", function() { - conversation.set("sessionId", "fakeSessionId"); - - router.conversation(); - - sinon.assert.calledOnce(router.loadReactComponent); - sinon.assert.calledWith(router.loadReactComponent, - sinon.match(function(value) { - return TestUtils.isDescriptorOfType(value, - loop.shared.views.ConversationView); - })); - }); - - it("should not load the ConversationView if session is not set", - function() { - router.conversation(); - - sinon.assert.notCalled(router.loadReactComponent); - }); - - it("should notify the user when session is not set", - function() { - sandbox.stub(notifications, "errorL10n"); - router.conversation(); - - sinon.assert.calledOnce(router._notifications.errorL10n); - sinon.assert.calledWithExactly(router._notifications.errorL10n, - "cannot_start_call_session_not_ready"); - }); - }); - describe("#decline", function() { beforeEach(function() { + icView = mountTestComponent(); + sandbox.stub(window, "close"); - router._websocket = { - decline: sandbox.spy() + icView._websocket = { + decline: sinon.stub(), + close: sinon.stub() }; conversation.setIncomingSessionData({ callId: 8699, @@ -468,76 +403,40 @@ describe("loop.conversation", function() { }); it("should close the window", function() { - router.decline(); + icView.decline(); + sandbox.clock.tick(1); sinon.assert.calledOnce(window.close); }); it("should stop alerting", function() { - sandbox.stub(navigator.mozLoop, "stopAlerting"); - router.decline(); + icView.decline(); sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); }); it("should release callData", function() { - sandbox.stub(navigator.mozLoop, "releaseCallData"); - router.decline(); + icView.decline(); sinon.assert.calledOnce(navigator.mozLoop.releaseCallData); sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, 8699); }); }); - describe("#feedback", function() { - var oldTitle; - - beforeEach(function() { - oldTitle = document.title; - sandbox.stub(document.mozL10n, "get").returns("Call ended"); - }); - - beforeEach(function() { - sandbox.stub(loop, "FeedbackAPIClient"); - sandbox.stub(router, "loadReactComponent"); - }); - - afterEach(function() { - document.title = oldTitle; - }); - - // XXX When the call is ended gracefully, we should check that we - // close connections nicely (see bug 1046744) - it("should display a feedback form view", function() { - router.feedback(); - - sinon.assert.calledOnce(router.loadReactComponent); - sinon.assert.calledWith(router.loadReactComponent, - sinon.match(function(value) { - return TestUtils.isDescriptorOfType(value, - loop.shared.views.FeedbackView); - })); - }); - - it("should update the conversation window title", function() { - router.feedback(); - - expect(document.title).eql("Call ended"); - }); - }); - describe("#blocked", function() { beforeEach(function() { - router._websocket = { - decline: sandbox.spy() + icView = mountTestComponent(); + + icView._websocket = { + decline: sinon.spy(), + close: sinon.stub() }; sandbox.stub(window, "close"); }); it("should call mozLoop.stopAlerting", function() { - sandbox.stub(navigator.mozLoop, "stopAlerting"); - router.declineAndBlock(); + icView.declineAndBlock(); sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); }); @@ -547,7 +446,7 @@ describe("loop.conversation", function() { .returns("fakeToken"); var deleteCallUrl = sandbox.stub(loop.Client.prototype, "deleteCallUrl"); - router.declineAndBlock(); + icView.declineAndBlock(); sinon.assert.calledOnce(deleteCallUrl); sinon.assert.calledWithExactly(deleteCallUrl, "fakeToken", @@ -556,7 +455,7 @@ describe("loop.conversation", function() { it("should get callToken from conversation model", function() { sandbox.stub(conversation, "get"); - router.declineAndBlock(); + icView.declineAndBlock(); sinon.assert.calledTwice(conversation.get); sinon.assert.calledWithExactly(conversation.get, "callToken"); @@ -572,14 +471,14 @@ describe("loop.conversation", function() { sandbox.stub(loop.Client.prototype, "deleteCallUrl", function(_, cb) { cb(fakeError); }); - router.declineAndBlock(); + icView.declineAndBlock(); sinon.assert.calledOnce(log); sinon.assert.calledWithExactly(log, fakeError); }); it("should close the window", function() { - router.declineAndBlock(); + icView.declineAndBlock(); sandbox.clock.tick(1); @@ -589,63 +488,66 @@ describe("loop.conversation", function() { }); describe("Events", function() { - var router, fakeSessionData; + var fakeSessionData; beforeEach(function() { + icView = mountTestComponent(); + fakeSessionData = { sessionId: "sessionId", sessionToken: "sessionToken", apiKey: "apiKey" }; - sandbox.stub(loop.conversation.ConversationRouter.prototype, - "navigate"); conversation.set("loopToken", "fakeToken"); - router = new loop.conversation.ConversationRouter({ - client: client, - conversation: conversation, - notifications: notifications - }); + navigator.mozLoop.getLoopCharPref.returns("http://fake"); + stubComponent(sharedView, "ConversationView"); }); - it("should navigate to call/ongoing once the call is ready", - function() { - router.incoming(42); + describe("call:accepted", function() { + it("should display the ConversationView", + function() { + conversation.accepted(); - conversation.incoming(); - - sinon.assert.calledOnce(router.navigate); - sinon.assert.calledWith(router.navigate, "call/ongoing"); - }); - - it("should navigate to call/feedback when the call session ends", - function() { - conversation.trigger("session:ended"); - - sinon.assert.calledOnce(router.navigate); - sinon.assert.calledWith(router.navigate, "call/feedback"); - }); - - it("should navigate to call/feedback when peer hangs up", function() { - conversation.trigger("session:peer-hungup"); - - sinon.assert.calledOnce(router.navigate); - sinon.assert.calledWith(router.navigate, "call/feedback"); + TestUtils.findRenderedComponentWithType(icView, + sharedView.ConversationView); + }); }); - it("should navigate to call/feedback when network disconnects", - function() { - conversation.trigger("session:network-disconnected"); + describe("session:ended", function() { + it("should display the feedback view when the call session ends", + function() { + conversation.trigger("session:ended"); - sinon.assert.calledOnce(router.navigate); - sinon.assert.calledWith(router.navigate, "call/feedback"); - }); + TestUtils.findRenderedComponentWithType(icView, + sharedView.FeedbackView); + }); + }); + + describe("session:peer-hungup", function() { + it("should display the feedback view when the peer hangs up", + function() { + conversation.trigger("session:peer-hungup"); + + TestUtils.findRenderedComponentWithType(icView, + sharedView.FeedbackView); + }); + }); + + describe("session:peer-hungup", function() { + it("should navigate to call/feedback when network disconnects", + function() { + conversation.trigger("session:network-disconnected"); + + TestUtils.findRenderedComponentWithType(icView, + sharedView.FeedbackView); + }); + }); describe("Published and Subscribed Streams", function() { beforeEach(function() { - router._websocket = { + icView._websocket = { mediaUp: sinon.spy() }; - router.incoming("fakeVersion"); }); describe("publishStream", function() { @@ -653,7 +555,7 @@ describe("loop.conversation", function() { function() { conversation.set("publishedStream", true); - sinon.assert.notCalled(router._websocket.mediaUp); + sinon.assert.notCalled(icView._websocket.mediaUp); }); it("should notify the websocket that media is up if both streams" + @@ -661,7 +563,7 @@ describe("loop.conversation", function() { conversation.set("subscribedStream", true); conversation.set("publishedStream", true); - sinon.assert.calledOnce(router._websocket.mediaUp); + sinon.assert.calledOnce(icView._websocket.mediaUp); }); }); @@ -670,7 +572,7 @@ describe("loop.conversation", function() { function() { conversation.set("subscribedStream", true); - sinon.assert.notCalled(router._websocket.mediaUp); + sinon.assert.notCalled(icView._websocket.mediaUp); }); it("should notify the websocket that media is up if both streams" + @@ -678,7 +580,7 @@ describe("loop.conversation", function() { conversation.set("publishedStream", true); conversation.set("subscribedStream", true); - sinon.assert.calledOnce(router._websocket.mediaUp); + sinon.assert.calledOnce(icView._websocket.mediaUp); }); }); }); diff --git a/browser/components/loop/test/desktop-local/index.html b/browser/components/loop/test/desktop-local/index.html index ec7dc942e68d..477a47a701f4 100644 --- a/browser/components/loop/test/desktop-local/index.html +++ b/browser/components/loop/test/desktop-local/index.html @@ -35,12 +35,10 @@ - - diff --git a/browser/components/loop/test/desktop-local/panel_test.js b/browser/components/loop/test/desktop-local/panel_test.js index 4e49057c4b43..4f1ab8f5efdc 100644 --- a/browser/components/loop/test/desktop-local/panel_test.js +++ b/browser/components/loop/test/desktop-local/panel_test.js @@ -13,13 +13,6 @@ describe("loop.panel", function() { var sandbox, notifications, fakeXHR, requests = []; - function createTestRouter(fakeDocument) { - return new loop.panel.PanelRouter({ - notifications: notifications, - document: fakeDocument - }); - } - beforeEach(function() { sandbox = sinon.sandbox.create(); fakeXHR = sandbox.useFakeXMLHttpRequest(); diff --git a/browser/components/loop/test/shared/index.html b/browser/components/loop/test/shared/index.html index 8261dcb2b240..908b910276be 100644 --- a/browser/components/loop/test/shared/index.html +++ b/browser/components/loop/test/shared/index.html @@ -37,7 +37,6 @@ - @@ -46,7 +45,6 @@ - - -