Bug 1066502 Remove the backbone router from the Loop conversation window, use a react view for control. r=nperriault

This commit is contained in:
Mark Banner 2014-09-24 14:25:30 +01:00
Родитель 839ba64784
Коммит 4bb3785445
21 изменённых файлов: 757 добавлений и 1145 удалений

Просмотреть файл

@ -27,13 +27,11 @@
<script type="text/javascript" src="loop/shared/js/utils.js"></script> <script type="text/javascript" src="loop/shared/js/utils.js"></script>
<script type="text/javascript" src="loop/shared/js/models.js"></script> <script type="text/javascript" src="loop/shared/js/models.js"></script>
<script type="text/javascript" src="loop/shared/js/router.js"></script>
<script type="text/javascript" src="loop/shared/js/mixins.js"></script> <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script> <script type="text/javascript" src="loop/shared/js/views.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script> <script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script> <script type="text/javascript" src="loop/shared/js/websocket.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script> <script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript" src="loop/js/desktopRouter.js"></script>
<script type="text/javascript" src="loop/js/conversation.js"></script> <script type="text/javascript" src="loop/js/conversation.js"></script>
</body> </body>
</html> </html>

Просмотреть файл

@ -8,16 +8,11 @@
/* global loop:true, React */ /* global loop:true, React */
var loop = loop || {}; var loop = loop || {};
loop.conversation = (function(OT, mozL10n) { loop.conversation = (function(mozL10n) {
"use strict"; "use strict";
var sharedViews = loop.shared.views; var sharedViews = loop.shared.views,
sharedModels = loop.shared.models;
/**
* App router.
* @type {loop.desktopRouter.DesktopConversationRouter}
*/
var router;
var IncomingCallView = React.createClass({displayName: 'IncomingCallView', 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: * At the moment, it does more than that, these parts need refactoring out.
* - {loop.shared.models.ConversationModel} conversation Conversation model.
* - {loop.shared.models.NotificationCollection} notifications
*
* @type {loop.shared.router.BaseConversationRouter}
*/ */
var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({ var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView',
routes: { propTypes: {
"incoming/:callId": "incoming", client: React.PropTypes.instanceOf(loop.Client).isRequired,
"call/accept": "accept", conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
"call/decline": "decline", .isRequired,
"call/ongoing": "conversation", notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
"call/declineAndBlock": "declineAndBlock", .isRequired,
"call/shutdown": "shutdown", sdk: React.PropTypes.object.isRequired
"call/feedback": "feedback" },
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() { _notifyError: function(error) {
this.navigate("call/ongoing", {trigger: true}); 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() { _onPeerHungup: function() {
navigator.mozLoop.releaseCallData(this._conversation.get("callId")); this.props.notifications.warnL10n("peer_ended_conversation2");
this.navigate("call/feedback", {trigger: true}); 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. * Incoming call route.
*
* @param {String} callId Identifier assigned by the LoopService
* to this incoming call.
*/ */
incoming: function(callId) { setupIncomingCall: function() {
navigator.mozLoop.startAlerting(); 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) { if (!callData) {
console.error("Failed to get the call data"); console.error("Failed to get the call data");
// XXX Not the ideal response, but bug 1047410 will be replacing // XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI. // 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; return;
} }
this._conversation.setIncomingSessionData(callData); this.props.conversation.setIncomingSessionData(callData);
this._setupWebSocketAndCallView(); 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 * Used to set up the web socket connection and navigate to the
* call view if appropriate. * call view if appropriate.
*/ */
_setupWebSocketAndCallView: function() { _setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({ this._websocket = new loop.CallConnectionWebSocket({
url: this._conversation.get("progressURL"), url: this.props.conversation.get("progressURL"),
websocketToken: this._conversation.get("websocketToken"), websocketToken: this.props.conversation.get("websocketToken"),
callId: this._conversation.get("callId"), callId: this.props.conversation.get("callId"),
}); });
this._websocket.promiseConnect().then(function() { this._websocket.promiseConnect().then(function() {
this.loadReactComponent(loop.conversation.IncomingCallView({ this.setState({callStatus: "incoming"});
model: this._conversation,
video: this._conversation.hasVideoStream("incoming")
}));
}.bind(this), function() { }.bind(this), function() {
this._handleSessionError(); this._handleSessionError();
return; return;
@ -301,7 +387,7 @@ loop.conversation = (function(OT, mozL10n) {
_checkConnected: function() { _checkConnected: function() {
// Check we've had both local and remote streams connected before // Check we've had both local and remote streams connected before
// sending the media up message. // sending the media up message.
if (this._conversation.streamsConnected()) { if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp(); this._websocket.mediaUp();
} }
}, },
@ -337,6 +423,12 @@ loop.conversation = (function(OT, mozL10n) {
_abortIncomingCall: function() { _abortIncomingCall: function() {
navigator.mozLoop.stopAlerting(); navigator.mozLoop.stopAlerting();
this._websocket.close(); 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(); window.close();
}, },
@ -346,7 +438,7 @@ loop.conversation = (function(OT, mozL10n) {
accept: function() { accept: function() {
navigator.mozLoop.stopAlerting(); navigator.mozLoop.stopAlerting();
this._websocket.accept(); this._websocket.accept();
this._conversation.incoming(); this.props.conversation.accepted();
}, },
/** /**
@ -354,13 +446,11 @@ loop.conversation = (function(OT, mozL10n) {
*/ */
_declineCall: function() { _declineCall: function() {
this._websocket.decline(); this._websocket.decline();
navigator.mozLoop.releaseCallData(this._conversation.get("callId")); navigator.mozLoop.releaseCallData(this.props.conversation.get("callId"));
// XXX Don't close the window straight away, but let any sends happen this._websocket.close();
// first. Ideally we'd wait to close the window until after we have a // Having a timeout here lets the logging for the websocket complete and be
// response from the server, to know that everything has completed // displayed on the console if both are on.
// successfully. However, that's quite difficult to ensure at the setTimeout(this.closeWindow, 0);
// moment so we'll add it later.
setTimeout(window.close, 0);
}, },
/** /**
@ -379,8 +469,8 @@ loop.conversation = (function(OT, mozL10n) {
*/ */
declineAndBlock: function() { declineAndBlock: function() {
navigator.mozLoop.stopAlerting(); navigator.mozLoop.stopAlerting();
var token = this._conversation.get("callToken"); var token = this.props.conversation.get("callToken");
this._client.deleteCallUrl(token, function(error) { this.props.client.deleteCallUrl(token, function(error) {
// XXX The conversation window will be closed when this cb is triggered // 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 // figure out if there is a better way to report the error to the user
// (bug 1048909). // (bug 1048909).
@ -389,62 +479,14 @@ loop.conversation = (function(OT, mozL10n) {
this._declineCall(); 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 * Handles a error starting the session
*/ */
_handleSessionError: function() { _handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing // XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI. // 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 // Plug in an alternate client ID mechanism, as localStorage and cookies
// don't work in the conversation window // don't work in the conversation window
if (OT && OT.hasOwnProperty("overrideGuidStorage")) { window.OT.overrideGuidStorage({
OT.overrideGuidStorage({ get: function(callback) {
get: function(callback) { callback(null, navigator.mozLoop.getLoopCharPref("ot.guid"));
callback(null, navigator.mozLoop.getLoopCharPref("ot.guid")); },
}, set: function(guid, callback) {
set: function(guid, callback) { navigator.mozLoop.setLoopCharPref("ot.guid", guid);
navigator.mozLoop.setLoopCharPref("ot.guid", guid); callback(null);
callback(null); }
} });
});
}
document.title = mozL10n.get("incoming_call_title2");
document.body.classList.add(loop.shared.utils.getTargetPlatform()); document.body.classList.add(loop.shared.utils.getTargetPlatform());
var client = new loop.Client(); var client = new loop.Client();
router = new ConversationRouter({ var conversation = new sharedModels.ConversationModel(
client: client, {}, // Model attributes
conversation: new loop.shared.models.ConversationModel( {sdk: window.OT} // Model dependencies
{}, // Model attributes );
{sdk: OT}), // Model dependencies var notifications = new sharedModels.NotificationCollection();
notifications: new loop.shared.models.NotificationCollection()
});
window.addEventListener("unload", function(event) { window.addEventListener("unload", function(event) {
// Handle direct close of dialog box via [x] control. // 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 { return {
ConversationRouter: ConversationRouter, IncomingConversationView: IncomingConversationView,
IncomingCallView: IncomingCallView, IncomingCallView: IncomingCallView,
init: init init: init
}; };
})(window.OT, document.mozL10n); })(document.mozL10n);
document.addEventListener('DOMContentLoaded', loop.conversation.init); document.addEventListener('DOMContentLoaded', loop.conversation.init);

Просмотреть файл

@ -8,16 +8,11 @@
/* global loop:true, React */ /* global loop:true, React */
var loop = loop || {}; var loop = loop || {};
loop.conversation = (function(OT, mozL10n) { loop.conversation = (function(mozL10n) {
"use strict"; "use strict";
var sharedViews = loop.shared.views; var sharedViews = loop.shared.views,
sharedModels = loop.shared.models;
/**
* App router.
* @type {loop.desktopRouter.DesktopConversationRouter}
*/
var router;
var IncomingCallView = React.createClass({ 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: * At the moment, it does more than that, these parts need refactoring out.
* - {loop.shared.models.ConversationModel} conversation Conversation model.
* - {loop.shared.models.NotificationCollection} notifications
*
* @type {loop.shared.router.BaseConversationRouter}
*/ */
var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({ var IncomingConversationView = React.createClass({
routes: { propTypes: {
"incoming/:callId": "incoming", client: React.PropTypes.instanceOf(loop.Client).isRequired,
"call/accept": "accept", conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
"call/decline": "decline", .isRequired,
"call/ongoing": "conversation", notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
"call/declineAndBlock": "declineAndBlock", .isRequired,
"call/shutdown": "shutdown", sdk: React.PropTypes.object.isRequired
"call/feedback": "feedback" },
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 (<div/>);
}
}
}, },
/** /**
* @override {loop.shared.router.BaseConversationRouter.startCall} * Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/ */
startCall: function() { _notifyError: function(error) {
this.navigate("call/ongoing", {trigger: true}); 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() { _onPeerHungup: function() {
navigator.mozLoop.releaseCallData(this._conversation.get("callId")); this.props.notifications.warnL10n("peer_ended_conversation2");
this.navigate("call/feedback", {trigger: true}); 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. * Incoming call route.
*
* @param {String} callId Identifier assigned by the LoopService
* to this incoming call.
*/ */
incoming: function(callId) { setupIncomingCall: function() {
navigator.mozLoop.startAlerting(); 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) { if (!callData) {
console.error("Failed to get the call data"); console.error("Failed to get the call data");
// XXX Not the ideal response, but bug 1047410 will be replacing // XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI. // 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; return;
} }
this._conversation.setIncomingSessionData(callData); this.props.conversation.setIncomingSessionData(callData);
this._setupWebSocketAndCallView(); 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 * Used to set up the web socket connection and navigate to the
* call view if appropriate. * call view if appropriate.
*/ */
_setupWebSocketAndCallView: function() { _setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({ this._websocket = new loop.CallConnectionWebSocket({
url: this._conversation.get("progressURL"), url: this.props.conversation.get("progressURL"),
websocketToken: this._conversation.get("websocketToken"), websocketToken: this.props.conversation.get("websocketToken"),
callId: this._conversation.get("callId"), callId: this.props.conversation.get("callId"),
}); });
this._websocket.promiseConnect().then(function() { this._websocket.promiseConnect().then(function() {
this.loadReactComponent(loop.conversation.IncomingCallView({ this.setState({callStatus: "incoming"});
model: this._conversation,
video: this._conversation.hasVideoStream("incoming")
}));
}.bind(this), function() { }.bind(this), function() {
this._handleSessionError(); this._handleSessionError();
return; return;
@ -301,7 +387,7 @@ loop.conversation = (function(OT, mozL10n) {
_checkConnected: function() { _checkConnected: function() {
// Check we've had both local and remote streams connected before // Check we've had both local and remote streams connected before
// sending the media up message. // sending the media up message.
if (this._conversation.streamsConnected()) { if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp(); this._websocket.mediaUp();
} }
}, },
@ -337,6 +423,12 @@ loop.conversation = (function(OT, mozL10n) {
_abortIncomingCall: function() { _abortIncomingCall: function() {
navigator.mozLoop.stopAlerting(); navigator.mozLoop.stopAlerting();
this._websocket.close(); 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(); window.close();
}, },
@ -346,7 +438,7 @@ loop.conversation = (function(OT, mozL10n) {
accept: function() { accept: function() {
navigator.mozLoop.stopAlerting(); navigator.mozLoop.stopAlerting();
this._websocket.accept(); this._websocket.accept();
this._conversation.incoming(); this.props.conversation.accepted();
}, },
/** /**
@ -354,13 +446,11 @@ loop.conversation = (function(OT, mozL10n) {
*/ */
_declineCall: function() { _declineCall: function() {
this._websocket.decline(); this._websocket.decline();
navigator.mozLoop.releaseCallData(this._conversation.get("callId")); navigator.mozLoop.releaseCallData(this.props.conversation.get("callId"));
// XXX Don't close the window straight away, but let any sends happen this._websocket.close();
// first. Ideally we'd wait to close the window until after we have a // Having a timeout here lets the logging for the websocket complete and be
// response from the server, to know that everything has completed // displayed on the console if both are on.
// successfully. However, that's quite difficult to ensure at the setTimeout(this.closeWindow, 0);
// moment so we'll add it later.
setTimeout(window.close, 0);
}, },
/** /**
@ -379,8 +469,8 @@ loop.conversation = (function(OT, mozL10n) {
*/ */
declineAndBlock: function() { declineAndBlock: function() {
navigator.mozLoop.stopAlerting(); navigator.mozLoop.stopAlerting();
var token = this._conversation.get("callToken"); var token = this.props.conversation.get("callToken");
this._client.deleteCallUrl(token, function(error) { this.props.client.deleteCallUrl(token, function(error) {
// XXX The conversation window will be closed when this cb is triggered // 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 // figure out if there is a better way to report the error to the user
// (bug 1048909). // (bug 1048909).
@ -389,62 +479,14 @@ loop.conversation = (function(OT, mozL10n) {
this._declineCall(); 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 * Handles a error starting the session
*/ */
_handleSessionError: function() { _handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing // XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI. // 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 // Plug in an alternate client ID mechanism, as localStorage and cookies
// don't work in the conversation window // don't work in the conversation window
if (OT && OT.hasOwnProperty("overrideGuidStorage")) { window.OT.overrideGuidStorage({
OT.overrideGuidStorage({ get: function(callback) {
get: function(callback) { callback(null, navigator.mozLoop.getLoopCharPref("ot.guid"));
callback(null, navigator.mozLoop.getLoopCharPref("ot.guid")); },
}, set: function(guid, callback) {
set: function(guid, callback) { navigator.mozLoop.setLoopCharPref("ot.guid", guid);
navigator.mozLoop.setLoopCharPref("ot.guid", guid); callback(null);
callback(null); }
} });
});
}
document.title = mozL10n.get("incoming_call_title2");
document.body.classList.add(loop.shared.utils.getTargetPlatform()); document.body.classList.add(loop.shared.utils.getTargetPlatform());
var client = new loop.Client(); var client = new loop.Client();
router = new ConversationRouter({ var conversation = new sharedModels.ConversationModel(
client: client, {}, // Model attributes
conversation: new loop.shared.models.ConversationModel( {sdk: window.OT} // Model dependencies
{}, // Model attributes );
{sdk: OT}), // Model dependencies var notifications = new sharedModels.NotificationCollection();
notifications: new loop.shared.models.NotificationCollection()
});
window.addEventListener("unload", function(event) { window.addEventListener("unload", function(event) {
// Handle direct close of dialog box via [x] control. // 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 { return {
ConversationRouter: ConversationRouter, IncomingConversationView: IncomingConversationView,
IncomingCallView: IncomingCallView, IncomingCallView: IncomingCallView,
init: init init: init
}; };
})(window.OT, document.mozL10n); })(document.mozL10n);
document.addEventListener('DOMContentLoaded', loop.conversation.init); document.addEventListener('DOMContentLoaded', loop.conversation.init);

Просмотреть файл

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

Просмотреть файл

@ -17,12 +17,6 @@ loop.panel = (function(_, mozL10n) {
var ContactsList = loop.contacts.ContactsList; var ContactsList = loop.contacts.ContactsList;
var __ = mozL10n.get; // aliasing translation function as __ for concision var __ = mozL10n.get; // aliasing translation function as __ for concision
/**
* Panel router.
* @type {loop.desktopRouter.DesktopRouter}
*/
var router;
var TabView = React.createClass({displayName: 'TabView', var TabView = React.createClass({displayName: 'TabView',
getInitialState: function() { getInitialState: function() {
return { return {

Просмотреть файл

@ -17,12 +17,6 @@ loop.panel = (function(_, mozL10n) {
var ContactsList = loop.contacts.ContactsList; var ContactsList = loop.contacts.ContactsList;
var __ = mozL10n.get; // aliasing translation function as __ for concision var __ = mozL10n.get; // aliasing translation function as __ for concision
/**
* Panel router.
* @type {loop.desktopRouter.DesktopRouter}
*/
var router;
var TabView = React.createClass({ var TabView = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {

Просмотреть файл

@ -77,10 +77,10 @@ loop.shared.models = (function(l10n) {
}, },
/** /**
* Starts an incoming conversation. * Indicates an incoming conversation has been accepted.
*/ */
incoming: function() { accepted: function() {
this.trigger("call:incoming"); this.trigger("call:accepted");
}, },
/** /**

Просмотреть файл

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

Просмотреть файл

@ -46,7 +46,29 @@ loop.shared.utils = (function() {
return !!localStorage.getItem(prefName); 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 { return {
Helper: Helper,
getTargetPlatform: getTargetPlatform, getTargetPlatform: getTargetPlatform,
getBoolPreference: getBoolPreference getBoolPreference: getBoolPreference
}; };

Просмотреть файл

@ -12,7 +12,6 @@ browser.jar:
# Desktop script # Desktop script
content/browser/loop/js/client.js (content/js/client.js) 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/conversation.js (content/js/conversation.js)
content/browser/loop/js/otconfig.js (content/js/otconfig.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/panel.js (content/js/panel.js)
@ -55,7 +54,6 @@ browser.jar:
# Shared scripts # Shared scripts
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js) 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/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/mixins.js (content/shared/js/mixins.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.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) content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)

Просмотреть файл

@ -15,7 +15,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000"; loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
var sharedModels = loop.shared.models, var sharedModels = loop.shared.models,
sharedViews = loop.shared.views; sharedViews = loop.shared.views,
sharedUtils = loop.shared.utils;
/** /**
* Homepage view. * Homepage view.
@ -435,7 +436,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired, client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired, .isRequired,
helper: React.PropTypes.instanceOf(WebappHelper).isRequired, helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired, .isRequired,
sdk: React.PropTypes.object.isRequired, sdk: React.PropTypes.object.isRequired,
@ -690,7 +691,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired, client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired, .isRequired,
helper: React.PropTypes.instanceOf(WebappHelper).isRequired, helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired, .isRequired,
sdk: React.PropTypes.object.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. * App initialization.
*/ */
function init() { function init() {
var helper = new WebappHelper(); var helper = new sharedUtils.Helper();
var client = new loop.StandaloneClient({ var client = new loop.StandaloneClient({
baseServerUrl: loop.config.serverUrl baseServerUrl: loop.config.serverUrl
}); });
@ -797,7 +777,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
UnsupportedDeviceView: UnsupportedDeviceView, UnsupportedDeviceView: UnsupportedDeviceView,
init: init, init: init,
PromoteFirefoxView: PromoteFirefoxView, PromoteFirefoxView: PromoteFirefoxView,
WebappHelper: WebappHelper,
WebappRootView: WebappRootView WebappRootView: WebappRootView
}; };
})(jQuery, _, window.OT, navigator.mozL10n); })(jQuery, _, window.OT, navigator.mozL10n);

Просмотреть файл

@ -15,7 +15,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000"; loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
var sharedModels = loop.shared.models, var sharedModels = loop.shared.models,
sharedViews = loop.shared.views; sharedViews = loop.shared.views,
sharedUtils = loop.shared.utils;
/** /**
* Homepage view. * Homepage view.
@ -435,7 +436,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired, client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired, .isRequired,
helper: React.PropTypes.instanceOf(WebappHelper).isRequired, helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired, .isRequired,
sdk: React.PropTypes.object.isRequired, sdk: React.PropTypes.object.isRequired,
@ -690,7 +691,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired, client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired, .isRequired,
helper: React.PropTypes.instanceOf(WebappHelper).isRequired, helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired, .isRequired,
sdk: React.PropTypes.object.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. * App initialization.
*/ */
function init() { function init() {
var helper = new WebappHelper(); var helper = new sharedUtils.Helper();
var client = new loop.StandaloneClient({ var client = new loop.StandaloneClient({
baseServerUrl: loop.config.serverUrl baseServerUrl: loop.config.serverUrl
}); });
@ -797,7 +777,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
UnsupportedDeviceView: UnsupportedDeviceView, UnsupportedDeviceView: UnsupportedDeviceView,
init: init, init: init,
PromoteFirefoxView: PromoteFirefoxView, PromoteFirefoxView: PromoteFirefoxView,
WebappHelper: WebappHelper,
WebappRootView: WebappRootView WebappRootView: WebappRootView
}; };
})(jQuery, _, window.OT, navigator.mozL10n); })(jQuery, _, window.OT, navigator.mozL10n);

Просмотреть файл

@ -9,10 +9,24 @@ var expect = chai.expect;
describe("loop.conversation", function() { describe("loop.conversation", function() {
"use strict"; "use strict";
var ConversationRouter = loop.conversation.ConversationRouter, var sharedModels = loop.shared.models,
sharedView = loop.shared.views,
sandbox, sandbox,
notifications; 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() { beforeEach(function() {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
sandbox.useFakeTimers(); sandbox.useFakeTimers();
@ -26,14 +40,14 @@ describe("loop.conversation", function() {
get locale() { get locale() {
return "en-US"; return "en-US";
}, },
setLoopCharPref: sandbox.stub(), setLoopCharPref: sinon.stub(),
getLoopCharPref: sandbox.stub(), getLoopCharPref: sinon.stub(),
getLoopBoolPref: sandbox.stub(), getLoopBoolPref: sinon.stub(),
getCallData: sandbox.stub(), getCallData: sinon.stub(),
releaseCallData: function() {}, releaseCallData: sinon.stub(),
startAlerting: function() {}, startAlerting: sinon.stub(),
stopAlerting: function() {}, stopAlerting: sinon.stub(),
ensureRegistered: function() {}, ensureRegistered: sinon.stub(),
get appVersionInfo() { get appVersionInfo() {
return { return {
version: "42", version: "42",
@ -57,21 +71,19 @@ describe("loop.conversation", function() {
var oldTitle; var oldTitle;
beforeEach(function() { beforeEach(function() {
oldTitle = document.title; sandbox.stub(React, "renderComponent");
sandbox.stub(document.mozL10n, "initialize"); 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, sandbox.stub(loop.shared.models.ConversationModel.prototype,
"initialize"); "initialize");
sandbox.stub(Backbone.history, "start"); window.OT = {
overrideGuidStorage: sinon.stub()
};
}); });
afterEach(function() { afterEach(function() {
document.title = oldTitle; delete window.OT;
}); });
it("should initalize L10n", function() { it("should initalize L10n", function() {
@ -82,300 +94,256 @@ describe("loop.conversation", function() {
navigator.mozLoop); navigator.mozLoop);
}); });
it("should set the document title", function() { it("should create the IncomingConversationView", function() {
loop.conversation.init(); 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() { describe("IncomingConversationView", function() {
var conversation, client; var conversation, client, icView, oldTitle;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
loop.conversation.IncomingConversationView({
client: client,
conversation: conversation,
notifications: notifications,
sdk: {}
}));
}
beforeEach(function() { beforeEach(function() {
oldTitle = document.title;
client = new loop.Client(); client = new loop.Client();
conversation = new loop.shared.models.ConversationModel({}, { conversation = new loop.shared.models.ConversationModel({}, {
sdk: {} sdk: {}
}); });
sandbox.spy(conversation, "setIncomingSessionData"); conversation.set({callId: 42});
sandbox.stub(conversation, "setOutgoingSessionData"); sandbox.stub(conversation, "setOutgoingSessionData");
}); });
describe("Routes", function() { afterEach(function() {
var router; 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() { beforeEach(function() {
router = new ConversationRouter({ fakeSessionData = {
client: client, sessionId: "sessionId",
conversation: conversation, sessionToken: "sessionToken",
notifications: notifications apiKey: "apiKey",
}); callType: "callType",
sandbox.stub(conversation, "incoming"); 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 sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
// just pass in the sandbox and put somewhere generally usable });
function stubComponent(obj, component, mockTagName){ it("should call getCallData on navigator.mozLoop", function() {
var reactClass = React.createClass({ icView = mountTestComponent();
render: function() {
var mockTagName = mockTagName || "div";
return React.DOM[mockTagName](null, this.props.children);
}
});
return sandbox.stub(obj, component, reactClass);
}
beforeEach(function() { sinon.assert.calledOnce(navigator.mozLoop.getCallData);
sandbox.stub(router, "loadReactComponent"); sinon.assert.calledWith(navigator.mozLoop.getCallData, 42);
stubComponent(loop.conversation, "IncomingCallView"); });
});
it("should start alerting", function() { describe("getCallData successful", function() {
sandbox.stub(navigator.mozLoop, "startAlerting"); var promise, resolveWebSocketConnect,
router.incoming("fakeVersion"); rejectWebSocketConnect;
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("Session Data setup", function() {
beforeEach(function() { beforeEach(function() {
fakeSessionData = { sandbox.stub(loop, "CallConnectionWebSocket").returns({
sessionId: "sessionId", promiseConnect: function () {
sessionToken: "sessionToken", promise = new Promise(function(resolve, reject) {
apiKey: "apiKey", resolveWebSocketConnect = resolve;
callType: "callType", rejectWebSocketConnect = reject;
callId: "Hello", });
progressURL: "http://progress.example.com", return promise;
websocketToken: 123 },
}; on: sinon.stub()
});
sandbox.stub(router, "_setupWebSocketAndCallView");
navigator.mozLoop.getCallData.returns(fakeSessionData);
}); });
it("should store the session data", function() { it("should store the session data", function() {
router.incoming("fakeVersion"); sandbox.stub(conversation, "setIncomingSessionData");
icView = mountTestComponent();
sinon.assert.calledOnce(conversation.setIncomingSessionData); sinon.assert.calledOnce(conversation.setIncomingSessionData);
sinon.assert.calledWithExactly(conversation.setIncomingSessionData, sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
fakeSessionData); fakeSessionData);
}); });
it("should call #_setupWebSocketAndCallView", function() { it("should setup the websocket connection", function() {
icView = mountTestComponent();
router.incoming("fakeVersion"); sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
sinon.assert.calledOnce(router._setupWebSocketAndCallView); callId: "Hello",
sinon.assert.calledWithExactly(router._setupWebSocketAndCallView); url: "http://progress.example.com",
websocketToken: "7b"
});
}); });
}); });
describe("#_setupWebSocketAndCallView", function() { describe("WebSocket Handling", function() {
beforeEach(function() { beforeEach(function() {
conversation.setIncomingSessionData({ promise = new Promise(function(resolve, reject) {
sessionId: "sessionId", resolveWebSocketConnect = resolve;
sessionToken: "sessionToken", rejectWebSocketConnect = reject;
apiKey: "apiKey", });
callType: "callType",
callId: "Hello", sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
progressURL: "http://progress.example.com", });
websocketToken: 123
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() { it("should display an error if the websocket failed to connect", function(done) {
var promise; 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() { beforeEach(function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({ icView = mountTestComponent();
promiseConnect: function() { promise = new Promise(function(resolve, reject) {
promise = new Promise(function(resolve, reject) { resolve();
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;
}, sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
on: sinon.spy() 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) { describe("progress - terminated - timeout (previousState = alerting)", function() {
router._setupWebSocketAndCallView(); it("should stop alerting", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "timeout"
}, "alerting");
promise.then(function () { sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
sinon.assert.calledOnce(loop.CallConnectionWebSocket); done();
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();
});
}); });
}); });
describe("progress - terminated - timeout (previousState = alerting)", function() { it("should close the websocket", function(done) {
it("should stop alerting", function(done) { promise.then(function() {
promise.then(function() { icView._websocket.trigger("progress", {
router._websocket.trigger("progress", { state: "terminated",
state: "terminated", reason: "timeout"
reason: "timeout" }, "alerting");
}, "alerting");
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); sinon.assert.calledOnce(icView._websocket.close);
done(); done();
});
}); });
});
it("should close the websocket", function(done) { it("should close the window", function(done) {
promise.then(function() { promise.then(function() {
router._websocket.trigger("progress", { icView._websocket.trigger("progress", {
state: "terminated", state: "terminated",
reason: "timeout" reason: "timeout"
}, "alerting"); }, "alerting");
sinon.assert.calledOnce(router._websocket.close); sandbox.clock.tick(1);
done();
});
});
it("should close the window", function(done) { sinon.assert.calledOnce(window.close);
promise.then(function() { done();
router._websocket.trigger("progress", {
state: "terminated",
reason: "timeout"
}, "alerting");
sinon.assert.calledOnce(window.close);
done();
});
}); });
}); });
}); });
@ -385,6 +353,7 @@ describe("loop.conversation", function() {
describe("#accept", function() { describe("#accept", function() {
beforeEach(function() { beforeEach(function() {
icView = mountTestComponent();
conversation.setIncomingSessionData({ conversation.setIncomingSessionData({
sessionId: "sessionId", sessionId: "sessionId",
sessionToken: "sessionToken", sessionToken: "sessionToken",
@ -394,72 +363,38 @@ describe("loop.conversation", function() {
progressURL: "http://progress.example.com", progressURL: "http://progress.example.com",
websocketToken: 123 websocketToken: 123
}); });
router._setupWebSocketAndCallView();
sandbox.stub(router._websocket, "accept"); sandbox.stub(icView._websocket, "accept");
sandbox.stub(navigator.mozLoop, "stopAlerting"); sandbox.stub(icView.props.conversation, "accepted");
}); });
it("should initiate the conversation", function() { 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() { 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() { it("should stop alerting", function() {
router.accept(); icView.accept();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); 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() { describe("#decline", function() {
beforeEach(function() { beforeEach(function() {
icView = mountTestComponent();
sandbox.stub(window, "close"); sandbox.stub(window, "close");
router._websocket = { icView._websocket = {
decline: sandbox.spy() decline: sinon.stub(),
close: sinon.stub()
}; };
conversation.setIncomingSessionData({ conversation.setIncomingSessionData({
callId: 8699, callId: 8699,
@ -468,76 +403,40 @@ describe("loop.conversation", function() {
}); });
it("should close the window", function() { it("should close the window", function() {
router.decline(); icView.decline();
sandbox.clock.tick(1); sandbox.clock.tick(1);
sinon.assert.calledOnce(window.close); sinon.assert.calledOnce(window.close);
}); });
it("should stop alerting", function() { it("should stop alerting", function() {
sandbox.stub(navigator.mozLoop, "stopAlerting"); icView.decline();
router.decline();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
}); });
it("should release callData", function() { it("should release callData", function() {
sandbox.stub(navigator.mozLoop, "releaseCallData"); icView.decline();
router.decline();
sinon.assert.calledOnce(navigator.mozLoop.releaseCallData); sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, 8699); 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() { describe("#blocked", function() {
beforeEach(function() { beforeEach(function() {
router._websocket = { icView = mountTestComponent();
decline: sandbox.spy()
icView._websocket = {
decline: sinon.spy(),
close: sinon.stub()
}; };
sandbox.stub(window, "close"); sandbox.stub(window, "close");
}); });
it("should call mozLoop.stopAlerting", function() { it("should call mozLoop.stopAlerting", function() {
sandbox.stub(navigator.mozLoop, "stopAlerting"); icView.declineAndBlock();
router.declineAndBlock();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
}); });
@ -547,7 +446,7 @@ describe("loop.conversation", function() {
.returns("fakeToken"); .returns("fakeToken");
var deleteCallUrl = sandbox.stub(loop.Client.prototype, var deleteCallUrl = sandbox.stub(loop.Client.prototype,
"deleteCallUrl"); "deleteCallUrl");
router.declineAndBlock(); icView.declineAndBlock();
sinon.assert.calledOnce(deleteCallUrl); sinon.assert.calledOnce(deleteCallUrl);
sinon.assert.calledWithExactly(deleteCallUrl, "fakeToken", sinon.assert.calledWithExactly(deleteCallUrl, "fakeToken",
@ -556,7 +455,7 @@ describe("loop.conversation", function() {
it("should get callToken from conversation model", function() { it("should get callToken from conversation model", function() {
sandbox.stub(conversation, "get"); sandbox.stub(conversation, "get");
router.declineAndBlock(); icView.declineAndBlock();
sinon.assert.calledTwice(conversation.get); sinon.assert.calledTwice(conversation.get);
sinon.assert.calledWithExactly(conversation.get, "callToken"); sinon.assert.calledWithExactly(conversation.get, "callToken");
@ -572,14 +471,14 @@ describe("loop.conversation", function() {
sandbox.stub(loop.Client.prototype, "deleteCallUrl", function(_, cb) { sandbox.stub(loop.Client.prototype, "deleteCallUrl", function(_, cb) {
cb(fakeError); cb(fakeError);
}); });
router.declineAndBlock(); icView.declineAndBlock();
sinon.assert.calledOnce(log); sinon.assert.calledOnce(log);
sinon.assert.calledWithExactly(log, fakeError); sinon.assert.calledWithExactly(log, fakeError);
}); });
it("should close the window", function() { it("should close the window", function() {
router.declineAndBlock(); icView.declineAndBlock();
sandbox.clock.tick(1); sandbox.clock.tick(1);
@ -589,63 +488,66 @@ describe("loop.conversation", function() {
}); });
describe("Events", function() { describe("Events", function() {
var router, fakeSessionData; var fakeSessionData;
beforeEach(function() { beforeEach(function() {
icView = mountTestComponent();
fakeSessionData = { fakeSessionData = {
sessionId: "sessionId", sessionId: "sessionId",
sessionToken: "sessionToken", sessionToken: "sessionToken",
apiKey: "apiKey" apiKey: "apiKey"
}; };
sandbox.stub(loop.conversation.ConversationRouter.prototype,
"navigate");
conversation.set("loopToken", "fakeToken"); conversation.set("loopToken", "fakeToken");
router = new loop.conversation.ConversationRouter({ navigator.mozLoop.getLoopCharPref.returns("http://fake");
client: client, stubComponent(sharedView, "ConversationView");
conversation: conversation,
notifications: notifications
});
}); });
it("should navigate to call/ongoing once the call is ready", describe("call:accepted", function() {
function() { it("should display the ConversationView",
router.incoming(42); function() {
conversation.accepted();
conversation.incoming(); TestUtils.findRenderedComponentWithType(icView,
sharedView.ConversationView);
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");
}); });
it("should navigate to call/feedback when network disconnects", describe("session:ended", function() {
function() { it("should display the feedback view when the call session ends",
conversation.trigger("session:network-disconnected"); function() {
conversation.trigger("session:ended");
sinon.assert.calledOnce(router.navigate); TestUtils.findRenderedComponentWithType(icView,
sinon.assert.calledWith(router.navigate, "call/feedback"); 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() { describe("Published and Subscribed Streams", function() {
beforeEach(function() { beforeEach(function() {
router._websocket = { icView._websocket = {
mediaUp: sinon.spy() mediaUp: sinon.spy()
}; };
router.incoming("fakeVersion");
}); });
describe("publishStream", function() { describe("publishStream", function() {
@ -653,7 +555,7 @@ describe("loop.conversation", function() {
function() { function() {
conversation.set("publishedStream", true); 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" + 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("subscribedStream", true);
conversation.set("publishedStream", 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() { function() {
conversation.set("subscribedStream", true); 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" + 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("publishedStream", true);
conversation.set("subscribedStream", true); conversation.set("subscribedStream", true);
sinon.assert.calledOnce(router._websocket.mediaUp); sinon.assert.calledOnce(icView._websocket.mediaUp);
}); });
}); });
}); });

Просмотреть файл

@ -35,12 +35,10 @@
<script src="../../content/shared/js/utils.js"></script> <script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script> <script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/models.js"></script> <script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/router.js"></script>
<script src="../../content/shared/js/mixins.js"></script> <script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/views.js"></script> <script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script> <script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/js/client.js"></script> <script src="../../content/js/client.js"></script>
<script src="../../content/js/desktopRouter.js"></script>
<script src="../../content/js/conversation.js"></script> <script src="../../content/js/conversation.js"></script>
<script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script> <script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
<script src="../../content/js/panel.js"></script> <script src="../../content/js/panel.js"></script>

Просмотреть файл

@ -13,13 +13,6 @@ describe("loop.panel", function() {
var sandbox, notifications, fakeXHR, requests = []; var sandbox, notifications, fakeXHR, requests = [];
function createTestRouter(fakeDocument) {
return new loop.panel.PanelRouter({
notifications: notifications,
document: fakeDocument
});
}
beforeEach(function() { beforeEach(function() {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
fakeXHR = sandbox.useFakeXMLHttpRequest(); fakeXHR = sandbox.useFakeXMLHttpRequest();

Просмотреть файл

@ -37,7 +37,6 @@
<script src="../../content/shared/js/models.js"></script> <script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script> <script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/views.js"></script> <script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/router.js"></script>
<script src="../../content/shared/js/websocket.js"></script> <script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script> <script src="../../content/shared/js/feedbackApiClient.js"></script>
@ -46,7 +45,6 @@
<script src="mixins_test.js"></script> <script src="mixins_test.js"></script>
<script src="utils_test.js"></script> <script src="utils_test.js"></script>
<script src="views_test.js"></script> <script src="views_test.js"></script>
<script src="router_test.js"></script>
<script src="websocket_test.js"></script> <script src="websocket_test.js"></script>
<script src="feedbackApiClient_test.js"></script> <script src="feedbackApiClient_test.js"></script>
<script> <script>

Просмотреть файл

@ -65,13 +65,13 @@ describe("loop.shared.models", function() {
conversation.set("loopToken", "fakeToken"); conversation.set("loopToken", "fakeToken");
}); });
describe("#incoming", function() { describe("#accepted", function() {
it("should trigger a `call:incoming` event", function(done) { it("should trigger a `call:accepted` event", function(done) {
conversation.once("call:incoming", function() { conversation.once("call:accepted", function() {
done(); done();
}); });
conversation.incoming(); conversation.accepted();
}); });
}); });

Просмотреть файл

@ -1,150 +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, sinon */
var expect = chai.expect;
describe("loop.shared.router", function() {
"use strict";
var sandbox, notifications;
beforeEach(function() {
sandbox = sinon.sandbox.create();
notifications = new loop.shared.models.NotificationCollection();
sandbox.stub(notifications, "errorL10n");
sandbox.stub(notifications, "warnL10n");
});
afterEach(function() {
sandbox.restore();
});
describe("BaseRouter", function() {
beforeEach(function() {
$("#fixtures").html('<div id="main"></div>');
});
afterEach(function() {
$("#fixtures").empty();
});
describe("#constructor", function() {
it("should require a notifications collection", function() {
expect(function() {
new loop.shared.router.BaseRouter();
}).to.Throw(Error, /missing required notifications/);
});
describe("inherited", function() {
var ExtendedRouter = loop.shared.router.BaseRouter.extend({});
it("should require a notifications collection", function() {
expect(function() {
new ExtendedRouter();
}).to.Throw(Error, /missing required notifications/);
});
});
});
});
describe("BaseConversationRouter", function() {
var conversation, TestRouter;
beforeEach(function() {
TestRouter = loop.shared.router.BaseConversationRouter.extend({
endCall: sandbox.spy()
});
conversation = new loop.shared.models.ConversationModel({
loopToken: "fakeToken"
}, {
sdk: {}
});
});
describe("#constructor", function() {
it("should require a ConversationModel instance", function() {
expect(function() {
new TestRouter({ client: {} });
}).to.Throw(Error, /missing required conversation/);
});
it("should require a Client instance", function() {
expect(function() {
new TestRouter({ conversation: {} });
}).to.Throw(Error, /missing required client/);
});
});
describe("Events", function() {
var router, fakeSessionData;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
router = new TestRouter({
conversation: conversation,
notifications: notifications,
client: {}
});
});
describe("session:connection-error", function() {
it("should warn the user when .connect() call fails", function() {
conversation.trigger("session:connection-error");
sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifications.errorL10n, sinon.match.string);
});
it("should invoke endCall()", function() {
conversation.trigger("session:connection-error");
sinon.assert.calledOnce(router.endCall);
sinon.assert.calledWithExactly(router.endCall);
});
});
it("should call endCall() when conversation ended", function() {
conversation.trigger("session:ended");
sinon.assert.calledOnce(router.endCall);
});
it("should warn the user when peer hangs up", function() {
conversation.trigger("session:peer-hungup");
sinon.assert.calledOnce(notifications.warnL10n);
sinon.assert.calledWithExactly(notifications.warnL10n,
"peer_ended_conversation2");
});
it("should call endCall() when peer hangs up", function() {
conversation.trigger("session:peer-hungup");
sinon.assert.calledOnce(router.endCall);
});
it("should warn the user when network disconnects", function() {
conversation.trigger("session:network-disconnected");
sinon.assert.calledOnce(notifications.warnL10n);
sinon.assert.calledWithExactly(notifications.warnL10n,
"network_disconnected");
});
it("should call endCall() when network disconnects", function() {
conversation.trigger("session:network-disconnected");
sinon.assert.calledOnce(router.endCall);
});
});
});
});

Просмотреть файл

@ -21,6 +21,40 @@ describe("loop.shared.utils", function() {
sandbox.restore(); sandbox.restore();
}); });
describe("Helper", function() {
var helper;
beforeEach(function() {
helper = new sharedUtils.Helper();
});
describe("#isIOS", function() {
it("should detect iOS", function() {
expect(helper.isIOS("iPad")).eql(true);
expect(helper.isIOS("iPod")).eql(true);
expect(helper.isIOS("iPhone")).eql(true);
expect(helper.isIOS("iPhone Simulator")).eql(true);
});
it("shouldn't detect iOS with other platforms", function() {
expect(helper.isIOS("MacIntel")).eql(false);
});
});
describe("#isFirefox", function() {
it("should detect Firefox", function() {
expect(helper.isFirefox("Firefox")).eql(true);
expect(helper.isFirefox("Gecko/Firefox")).eql(true);
expect(helper.isFirefox("Firefox/Gecko")).eql(true);
expect(helper.isFirefox("Gecko/Firefox/Chuck Norris")).eql(true);
});
it("shouldn't detect Firefox with other platforms", function() {
expect(helper.isFirefox("Opera")).eql(false);
});
});
});
describe("#getBoolPreference", function() { describe("#getBoolPreference", function() {
afterEach(function() { afterEach(function() {
navigator.mozLoop = undefined; navigator.mozLoop = undefined;

Просмотреть файл

@ -12,6 +12,7 @@ describe("loop.webapp", function() {
var sharedModels = loop.shared.models, var sharedModels = loop.shared.models,
sharedViews = loop.shared.views, sharedViews = loop.shared.views,
sharedUtils = loop.shared.utils,
sandbox, sandbox,
notifications, notifications,
feedbackApiClient; feedbackApiClient;
@ -33,7 +34,7 @@ describe("loop.webapp", function() {
beforeEach(function() { beforeEach(function() {
sandbox.stub(React, "renderComponent"); sandbox.stub(React, "renderComponent");
sandbox.stub(loop.webapp.WebappHelper.prototype, sandbox.stub(sharedUtils.Helper.prototype,
"locationHash").returns("#call/fake-Token"); "locationHash").returns("#call/fake-Token");
loop.config.feedbackApiUrl = "http://fake.invalid"; loop.config.feedbackApiUrl = "http://fake.invalid";
conversationSetStub = conversationSetStub =
@ -78,7 +79,7 @@ describe("loop.webapp", function() {
}); });
conversation.set("loopToken", "fakeToken"); conversation.set("loopToken", "fakeToken");
ocView = mountTestComponent({ ocView = mountTestComponent({
helper: new loop.webapp.WebappHelper(), helper: new sharedUtils.Helper(),
client: client, client: client,
conversation: conversation, conversation: conversation,
notifications: notifications, notifications: notifications,
@ -473,13 +474,13 @@ describe("loop.webapp", function() {
}); });
describe("WebappRootView", function() { describe("WebappRootView", function() {
var webappHelper, sdk, conversationModel, client, props; var helper, sdk, conversationModel, client, props;
function mountTestComponent() { function mountTestComponent() {
return TestUtils.renderIntoDocument( return TestUtils.renderIntoDocument(
loop.webapp.WebappRootView({ loop.webapp.WebappRootView({
client: client, client: client,
helper: webappHelper, helper: helper,
notifications: notifications, notifications: notifications,
sdk: sdk, sdk: sdk,
conversation: conversationModel, conversation: conversationModel,
@ -488,7 +489,7 @@ describe("loop.webapp", function() {
} }
beforeEach(function() { beforeEach(function() {
webappHelper = new loop.webapp.WebappHelper(); helper = new sharedUtils.Helper();
sdk = { sdk = {
checkSystemRequirements: function() { return true; } checkSystemRequirements: function() { return true; }
}; };
@ -505,7 +506,7 @@ describe("loop.webapp", function() {
it("should mount the unsupportedDevice view if the device is running iOS", it("should mount the unsupportedDevice view if the device is running iOS",
function() { function() {
sandbox.stub(webappHelper, "isIOS").returns(true); sandbox.stub(helper, "isIOS").returns(true);
var webappRootView = mountTestComponent(); var webappRootView = mountTestComponent();
@ -825,38 +826,4 @@ describe("loop.webapp", function() {
}); });
}); });
}); });
describe("WebappHelper", function() {
var helper;
beforeEach(function() {
helper = new loop.webapp.WebappHelper();
});
describe("#isIOS", function() {
it("should detect iOS", function() {
expect(helper.isIOS("iPad")).eql(true);
expect(helper.isIOS("iPod")).eql(true);
expect(helper.isIOS("iPhone")).eql(true);
expect(helper.isIOS("iPhone Simulator")).eql(true);
});
it("shouldn't detect iOS with other platforms", function() {
expect(helper.isIOS("MacIntel")).eql(false);
});
});
describe("#isFirefox", function() {
it("should detect Firefox", function() {
expect(helper.isFirefox("Firefox")).eql(true);
expect(helper.isFirefox("Gecko/Firefox")).eql(true);
expect(helper.isFirefox("Firefox/Gecko")).eql(true);
expect(helper.isFirefox("Gecko/Firefox/Chuck Norris")).eql(true);
});
it("shouldn't detect Firefox with other platforms", function() {
expect(helper.isFirefox("Opera")).eql(false);
});
});
});
}); });

Просмотреть файл

@ -34,11 +34,9 @@
<script src="../content/shared/js/feedbackApiClient.js"></script> <script src="../content/shared/js/feedbackApiClient.js"></script>
<script src="../content/shared/js/utils.js"></script> <script src="../content/shared/js/utils.js"></script>
<script src="../content/shared/js/models.js"></script> <script src="../content/shared/js/models.js"></script>
<script src="../content/shared/js/router.js"></script>
<script src="../content/shared/js/mixins.js"></script> <script src="../content/shared/js/mixins.js"></script>
<script src="../content/shared/js/views.js"></script> <script src="../content/shared/js/views.js"></script>
<script src="../content/js/client.js"></script> <script src="../content/js/client.js"></script>
<script src="../content/js/desktopRouter.js"></script>
<script src="../standalone/content/js/webapp.js"></script> <script src="../standalone/content/js/webapp.js"></script>
<script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script> <script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
<script> <script>