--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1577,16 +1577,17 @@ pref("loop.seenToS", "unseen");
pref("loop.legal.ToS_url", "https://accounts.firefox.com/legal/terms");
pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/");
pref("loop.do_not_disturb", false);
pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/Firefox-Long.ogg");
pref("loop.retry_delay.start", 60000);
pref("loop.retry_delay.limit", 300000);
pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
pref("loop.feedback.product", "Loop");
+pref("loop.debug.websocket", false);
// serverURL to be assigned by services team
pref("services.push.serverURL", "wss://push.services.mozilla.com/");
pref("social.sidebar.unload_timeout_ms", 10000);
// activation from inside of share panel is possible if activationPanelEnabled
// is true. Pref'd off for release while usage testing is done through beta.
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -31,13 +31,14 @@
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
<script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.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/router.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/websocket.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>
</body>
</html>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -184,48 +184,80 @@ loop.conversation = (function(OT, mozL10
this._conversation.once("declineAndBlock", () => {
this.navigate("call/declineAndBlock", {trigger: true});
});
this._conversation.once("call:incoming", this.startCall, this);
this._client.requestCallsInfo(loopVersion, (err, sessionData) => {
if (err) {
console.error("Failed to get the sessionData", err);
// XXX Not the ideal response, but bug 1047410 will be replacing
- //this by better "call failed" UI.
+ // this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready");
return;
}
+
// XXX For incoming calls we might have more than one call queued.
// For now, we'll just assume the first call is the right information.
// We'll probably really want to be getting this data from the
// background worker on the desktop client.
// Bug 1032700 should fix this.
this._conversation.setIncomingSessionData(sessionData[0]);
+
+ this._setupWebSocketAndCallView();
+ });
+ },
+
+ /**
+ * Used to set up the web socket connection and navigate to the
+ * call view if appropriate.
+ */
+ _setupWebSocketAndCallView: function() {
+ this._websocket = new loop.CallConnectionWebSocket({
+ url: this._conversation.get("progressURL"),
+ websocketToken: this._conversation.get("websocketToken"),
+ callId: this._conversation.get("callId"),
+ });
+ this._websocket.promiseConnect().then(function() {
this.loadReactComponent(loop.conversation.IncomingCallView({
model: this._conversation,
video: {enabled: this._conversation.hasVideoStream("incoming")}
}));
- });
+ }.bind(this), function() {
+ this._handleSessionError();
+ return;
+ }.bind(this));
},
/**
* Accepts an incoming call.
*/
accept: function() {
navigator.mozLoop.stopAlerting();
this._conversation.incoming();
},
/**
+ * Declines a call and handles closing of the window.
+ */
+ _declineCall: function() {
+ this._websocket.decline();
+ // 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);
+ },
+
+ /**
* Declines an incoming call.
*/
decline: function() {
navigator.mozLoop.stopAlerting();
- // XXX For now, we just close the window
- window.close();
+ this._declineCall();
},
/**
* Decline and block an incoming call
* @note:
* - loopToken is the callUrl identifier. It gets set in the panel
* after a callUrl is received
*/
@@ -233,43 +265,52 @@ loop.conversation = (function(OT, mozL10
navigator.mozLoop.stopAlerting();
var token = navigator.mozLoop.getLoopCharPref("loopToken");
this._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).
console.log(error);
});
- window.close();
+ 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._notifier.errorL10n("cannot_start_call_session_not_ready");
+ this._handleSessionError();
return;
}
var callType = this._conversation.get("selectedCallType");
var videoStream = callType === "audio" ? false : true;
/*jshint newcap:false*/
this.loadReactComponent(sharedViews.ConversationView({
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._notifier.errorL10n("cannot_start_call_session_not_ready");
+ },
+
+ /**
* Call has ended, display a feedback form.
*/
feedback: function() {
document.title = mozL10n.get("call_has_ended");
var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
"feedback.baseUrl");
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -184,48 +184,80 @@ loop.conversation = (function(OT, mozL10
this._conversation.once("declineAndBlock", () => {
this.navigate("call/declineAndBlock", {trigger: true});
});
this._conversation.once("call:incoming", this.startCall, this);
this._client.requestCallsInfo(loopVersion, (err, sessionData) => {
if (err) {
console.error("Failed to get the sessionData", err);
// XXX Not the ideal response, but bug 1047410 will be replacing
- //this by better "call failed" UI.
+ // this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready");
return;
}
+
// XXX For incoming calls we might have more than one call queued.
// For now, we'll just assume the first call is the right information.
// We'll probably really want to be getting this data from the
// background worker on the desktop client.
// Bug 1032700 should fix this.
this._conversation.setIncomingSessionData(sessionData[0]);
+
+ this._setupWebSocketAndCallView();
+ });
+ },
+
+ /**
+ * Used to set up the web socket connection and navigate to the
+ * call view if appropriate.
+ */
+ _setupWebSocketAndCallView: function() {
+ this._websocket = new loop.CallConnectionWebSocket({
+ url: this._conversation.get("progressURL"),
+ websocketToken: this._conversation.get("websocketToken"),
+ callId: this._conversation.get("callId"),
+ });
+ this._websocket.promiseConnect().then(function() {
this.loadReactComponent(loop.conversation.IncomingCallView({
model: this._conversation,
video: {enabled: this._conversation.hasVideoStream("incoming")}
}));
- });
+ }.bind(this), function() {
+ this._handleSessionError();
+ return;
+ }.bind(this));
},
/**
* Accepts an incoming call.
*/
accept: function() {
navigator.mozLoop.stopAlerting();
this._conversation.incoming();
},
/**
+ * Declines a call and handles closing of the window.
+ */
+ _declineCall: function() {
+ this._websocket.decline();
+ // 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);
+ },
+
+ /**
* Declines an incoming call.
*/
decline: function() {
navigator.mozLoop.stopAlerting();
- // XXX For now, we just close the window
- window.close();
+ this._declineCall();
},
/**
* Decline and block an incoming call
* @note:
* - loopToken is the callUrl identifier. It gets set in the panel
* after a callUrl is received
*/
@@ -233,43 +265,52 @@ loop.conversation = (function(OT, mozL10
navigator.mozLoop.stopAlerting();
var token = navigator.mozLoop.getLoopCharPref("loopToken");
this._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).
console.log(error);
});
- window.close();
+ 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._notifier.errorL10n("cannot_start_call_session_not_ready");
+ this._handleSessionError();
return;
}
var callType = this._conversation.get("selectedCallType");
var videoStream = callType === "audio" ? false : true;
/*jshint newcap:false*/
this.loadReactComponent(sharedViews.ConversationView({
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._notifier.errorL10n("cannot_start_call_session_not_ready");
+ },
+
+ /**
* Call has ended, display a feedback form.
*/
feedback: function() {
document.title = mozL10n.get("call_has_ended");
var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
"feedback.baseUrl");
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -20,16 +20,21 @@ loop.shared.models = (function() {
loopToken: undefined, // Loop conversation token
loopVersion: undefined, // Loop version for /calls/ information. This
// is the version received from the push
// notification and is used by the server to
// determine the pending calls
sessionId: undefined, // OT session id
sessionToken: undefined, // OT session token
apiKey: undefined, // OT api key
+ callId: undefined, // The callId on the server
+ progressURL: undefined, // The websocket url to use for progress
+ websocketToken: undefined, // The token to use for websocket auth, this is
+ // stored as a hex string which is what the server
+ // requires.
callType: undefined, // The type of incoming call selected by
// other peer ("audio" or "audio-video")
selectedCallType: undefined // The selected type for the call that was
// initiated ("audio" or "audio-video")
},
/**
* SDK object.
@@ -135,34 +140,40 @@ loop.shared.models = (function() {
* Sets session information.
* Session data received by creating an outgoing call.
*
* @param {Object} sessionData Conversation session information.
*/
setOutgoingSessionData: function(sessionData) {
// Explicit property assignment to prevent later "surprises"
this.set({
- sessionId: sessionData.sessionId,
- sessionToken: sessionData.sessionToken,
- apiKey: sessionData.apiKey
+ sessionId: sessionData.sessionId,
+ sessionToken: sessionData.sessionToken,
+ apiKey: sessionData.apiKey,
+ callId: sessionData.callId,
+ progressURL: sessionData.progressURL,
+ websocketToken: sessionData.websocketToken.toString(16)
});
},
/**
* Sets session information about the incoming call.
*
* @param {Object} sessionData Conversation session information.
*/
setIncomingSessionData: function(sessionData) {
// Explicit property assignment to prevent later "surprises"
this.set({
- sessionId: sessionData.sessionId,
- sessionToken: sessionData.sessionToken,
- apiKey: sessionData.apiKey,
- callType: sessionData.callType || "audio-video"
+ sessionId: sessionData.sessionId,
+ sessionToken: sessionData.sessionToken,
+ apiKey: sessionData.apiKey,
+ callId: sessionData.callId,
+ progressURL: sessionData.progressURL,
+ websocketToken: sessionData.websocketToken.toString(16),
+ callType: sessionData.callType || "audio-video"
});
},
/**
* Starts a SDK session and subscribe to call events.
*/
startSession: function() {
if (!this.isSessionReady()) {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/websocket.js
@@ -0,0 +1,237 @@
+/* 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.CallConnectionWebSocket = (function() {
+ "use strict";
+
+ // Response timeout is 5 seconds as per API.
+ var kResponseTimeout = 5000;
+
+ /**
+ * Handles a websocket specifically for a call connection.
+ *
+ * There should be one of these created for each call connection.
+ *
+ * options items:
+ * - url The url of the websocket to connect to.
+ * - callId The call id for the call
+ * - websocketToken The authentication token for the websocket
+ *
+ * @param {Object} options The options for this websocket.
+ */
+ function CallConnectionWebSocket(options) {
+ this.options = options || {};
+
+ if (!this.options.url) {
+ throw new Error("No url in options");
+ }
+ if (!this.options.callId) {
+ throw new Error("No callId in options");
+ }
+ if (!this.options.websocketToken) {
+ throw new Error("No websocketToken in options");
+ }
+
+ // Save the debug pref now, to avoid getting it each time.
+ if (navigator.mozLoop) {
+ this._debugWebSocket =
+ navigator.mozLoop.getLoopBoolPref("debug.websocket");
+ }
+
+ _.extend(this, Backbone.Events);
+ };
+
+ CallConnectionWebSocket.prototype = {
+ /**
+ * Start the connection to the websocket.
+ *
+ * @return {Promise} A promise that resolves when the websocket
+ * server connection is open and "hello"s have been
+ * exchanged. It is rejected if there is a failure in
+ * connection or the initial exchange of "hello"s.
+ */
+ promiseConnect: function() {
+ var promise = new Promise(
+ function(resolve, reject) {
+ this.socket = new WebSocket(this.options.url);
+ this.socket.onopen = this._onopen.bind(this);
+ this.socket.onmessage = this._onmessage.bind(this);
+ this.socket.onerror = this._onerror.bind(this);
+ this.socket.onclose = this._onclose.bind(this);
+
+ var timeout = setTimeout(function() {
+ if (this.connectDetails && this.connectDetails.reject) {
+ this.connectDetails.reject("timeout");
+ this._clearConnectionFlags();
+ }
+ }.bind(this), kResponseTimeout);
+ this.connectDetails = {
+ resolve: resolve,
+ reject: reject,
+ timeout: timeout
+ };
+ }.bind(this));
+
+ return promise;
+ },
+
+ _clearConnectionFlags: function() {
+ clearTimeout(this.connectDetails.timeout);
+ delete this.connectDetails;
+ },
+
+ /**
+ * Internal function called to resolve the connection promise.
+ *
+ * It will log an error if no promise is found.
+ */
+ _completeConnection: function() {
+ if (this.connectDetails && this.connectDetails.resolve) {
+ this.connectDetails.resolve();
+ this._clearConnectionFlags();
+ return;
+ }
+
+ console.error("Failed to complete connection promise - no promise available");
+ },
+
+ /**
+ * Checks if the websocket is connecting, and rejects the connection
+ * promise if appropriate.
+ *
+ * @param {Object} event The event to reject the promise with if
+ * appropriate.
+ */
+ _checkConnectionFailed: function(event) {
+ if (this.connectDetails && this.connectDetails.reject) {
+ this.connectDetails.reject(event);
+ this._clearConnectionFlags();
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Notifies the server that the user has declined the call.
+ */
+ decline: function() {
+ this._send({
+ messageType: "action",
+ event: "terminate",
+ reason: "reject"
+ });
+ },
+
+ /**
+ * Sends data on the websocket.
+ *
+ * @param {Object} data The data to send.
+ */
+ _send: function(data) {
+ this._log("WS Sending", data);
+
+ this.socket.send(JSON.stringify(data));
+ },
+
+ /**
+ * Used to determine if the server state is in a completed state, i.e.
+ * the server has determined the connection is terminated or connected.
+ *
+ * @return True if the last received state is terminated or connected.
+ */
+ get _stateIsCompleted() {
+ return this._lastServerState === "terminated" ||
+ this._lastServerState === "connected";
+ },
+
+ /**
+ * Called when the socket is open. Automatically sends a "hello"
+ * message to the server.
+ */
+ _onopen: function() {
+ // Auto-register with the server.
+ this._send({
+ messageType: "hello",
+ callId: this.options.callId,
+ auth: this.options.websocketToken
+ });
+ },
+
+ /**
+ * Called when a message is received from the server.
+ *
+ * @param {Object} event The websocket onmessage event.
+ */
+ _onmessage: function(event) {
+ var msg;
+ try {
+ msg = JSON.parse(event.data);
+ } catch (x) {
+ console.error("Error parsing received message:", x);
+ return;
+ }
+
+ this._log("WS Receiving", event.data);
+
+ this._lastServerState = msg.state;
+
+ switch(msg.messageType) {
+ case "hello":
+ this._completeConnection();
+ break;
+ case "progress":
+ this.trigger("progress", msg);
+ break;
+ }
+ },
+
+ /**
+ * Called when there is an error on the websocket.
+ *
+ * @param {Object} event A simple error event.
+ */
+ _onerror: function(event) {
+ this._log("WS Error", event);
+
+ if (!this._stateIsCompleted &&
+ !this._checkConnectionFailed(event)) {
+ this.trigger("error", event);
+ }
+ },
+
+ /**
+ * Called when the websocket is closed.
+ *
+ * @param {CloseEvent} event The details of the websocket closing.
+ */
+ _onclose: function(event) {
+ this._log("WS Close", event);
+
+ // If the websocket goes away when we're not in a completed state
+ // then its an error. So we either pass it back via the connection
+ // promise, or trigger the closed event.
+ if (!this._stateIsCompleted &&
+ !this._checkConnectionFailed(event)) {
+ this.trigger("closed", event);
+ }
+ },
+
+ /**
+ * Logs debug to the console.
+ *
+ * Parameters: same as console.log
+ */
+ _log: function() {
+ if (this._debugWebSocket) {
+ console.log.apply(console, arguments);
+ }
+ }
+ };
+
+ return CallConnectionWebSocket;
+})();
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -41,16 +41,17 @@ browser.jar:
content/browser/loop/shared/img/dropdown-inverse@2x.png (content/shared/img/dropdown-inverse@2x.png)
# 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/views.js (content/shared/js/views.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
+ content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
# Shared libs
content/browser/loop/shared/libs/react-0.11.1.js (content/shared/libs/react-0.11.1.js)
content/browser/loop/shared/libs/lodash-2.4.1.js (content/shared/libs/lodash-2.4.1.js)
content/browser/loop/shared/libs/jquery-2.1.0.js (content/shared/libs/jquery-2.1.0.js)
content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js)
# Shared sounds
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -32,16 +32,17 @@
<script type="text/javascript" src="shared/libs/backbone-1.1.2.js"></script>
<!-- app scripts -->
<script type="text/javascript" src="config.js"></script>
<script type="text/javascript" src="shared/js/utils.js"></script>
<script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/router.js"></script>
+ <script type="text/javascript" src="shared/js/websocket.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/webapp.js"></script>
<script>
// Wait for all the localization notes to load
window.addEventListener('localized', function() {
loop.webapp.init();
}, false);
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -369,23 +369,78 @@ loop.webapp = (function($, _, OT, webL10
* Actually starts the call.
*/
startCall: function() {
var loopToken = this._conversation.get("loopToken");
if (!loopToken) {
this._notifier.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
} else {
+ this._setupWebSocketAndCallView(loopToken);
+ }
+ },
+
+ /**
+ * Used to set up the web socket connection and navigate to the
+ * call view if appropriate.
+ *
+ * @param {string} loopToken The session token to use.
+ */
+ _setupWebSocketAndCallView: function(loopToken) {
+ this._websocket = new loop.CallConnectionWebSocket({
+ url: this._conversation.get("progressURL"),
+ websocketToken: this._conversation.get("websocketToken"),
+ callId: this._conversation.get("callId"),
+ });
+ this._websocket.promiseConnect().then(function() {
this.navigate("call/ongoing/" + loopToken, {
trigger: true
});
+ }.bind(this), function() {
+ // XXX Not the ideal response, but bug 1047410 will be replacing
+ // this by better "call failed" UI.
+ this._notifier.errorL10n("cannot_start_call_session_not_ready");
+ return;
+ }.bind(this));
+
+ this._websocket.on("progress", this._handleWebSocketProgress, this);
+ },
+
+ /**
+ * Used to receive websocket progress and to determine how to handle
+ * it if appropraite.
+ */
+ _handleWebSocketProgress: function(progressData) {
+ if (progressData.state === "terminated") {
+ // XXX Before adding more states here, the basic protocol messages to the
+ // server need implementing on both the standalone and desktop side.
+ // These are covered by bug 1045643, but also check the dependencies on
+ // bug 1034041.
+ //
+ // Failure to do this will break desktop - standalone call setup. We're
+ // ok to handle reject, as that is a specific message from the destkop via
+ // the server.
+ switch (progressData.reason) {
+ case "reject":
+ this._handleCallRejected();
+ }
}
},
/**
+ * Handles call rejection.
+ * XXX This should really display the call failed view - bug 1046959
+ * will implement this.
+ */
+ _handleCallRejected: function() {
+ this.endCall();
+ this._notifier.errorL10n("call_timeout_notification_text");
+ },
+
+ /**
* @override {loop.shared.router.BaseConversationRouter.endCall}
*/
endCall: function() {
var route = "home";
if (this._conversation.get("loopToken")) {
route = "call/" + this._conversation.get("loopToken");
}
this.navigate(route, {trigger: true});
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -369,23 +369,78 @@ loop.webapp = (function($, _, OT, webL10
* Actually starts the call.
*/
startCall: function() {
var loopToken = this._conversation.get("loopToken");
if (!loopToken) {
this._notifier.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
} else {
+ this._setupWebSocketAndCallView(loopToken);
+ }
+ },
+
+ /**
+ * Used to set up the web socket connection and navigate to the
+ * call view if appropriate.
+ *
+ * @param {string} loopToken The session token to use.
+ */
+ _setupWebSocketAndCallView: function(loopToken) {
+ this._websocket = new loop.CallConnectionWebSocket({
+ url: this._conversation.get("progressURL"),
+ websocketToken: this._conversation.get("websocketToken"),
+ callId: this._conversation.get("callId"),
+ });
+ this._websocket.promiseConnect().then(function() {
this.navigate("call/ongoing/" + loopToken, {
trigger: true
});
+ }.bind(this), function() {
+ // XXX Not the ideal response, but bug 1047410 will be replacing
+ // this by better "call failed" UI.
+ this._notifier.errorL10n("cannot_start_call_session_not_ready");
+ return;
+ }.bind(this));
+
+ this._websocket.on("progress", this._handleWebSocketProgress, this);
+ },
+
+ /**
+ * Used to receive websocket progress and to determine how to handle
+ * it if appropraite.
+ */
+ _handleWebSocketProgress: function(progressData) {
+ if (progressData.state === "terminated") {
+ // XXX Before adding more states here, the basic protocol messages to the
+ // server need implementing on both the standalone and desktop side.
+ // These are covered by bug 1045643, but also check the dependencies on
+ // bug 1034041.
+ //
+ // Failure to do this will break desktop - standalone call setup. We're
+ // ok to handle reject, as that is a specific message from the destkop via
+ // the server.
+ switch (progressData.reason) {
+ case "reject":
+ this._handleCallRejected();
+ }
}
},
/**
+ * Handles call rejection.
+ * XXX This should really display the call failed view - bug 1046959
+ * will implement this.
+ */
+ _handleCallRejected: function() {
+ this.endCall();
+ this._notifier.errorL10n("call_timeout_notification_text");
+ },
+
+ /**
* @override {loop.shared.router.BaseConversationRouter.endCall}
*/
endCall: function() {
var route = "home";
if (this._conversation.get("loopToken")) {
route = "call/" + this._conversation.get("loopToken");
}
this.navigate(route, {trigger: true});
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -10,16 +10,17 @@ describe("loop.conversation", function()
"use strict";
var ConversationRouter = loop.conversation.ConversationRouter,
sandbox,
notifier;
beforeEach(function() {
sandbox = sinon.sandbox.create();
+ sandbox.useFakeTimers();
notifier = {
notify: sandbox.spy(),
warn: sandbox.spy(),
warnL10n: sandbox.spy(),
error: sandbox.spy(),
errorL10n: sandbox.spy()
};
@@ -114,17 +115,16 @@ describe("loop.conversation", function()
beforeEach(function() {
client = new loop.Client();
conversation = new loop.shared.models.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000,
});
sandbox.stub(client, "requestCallsInfo");
- sandbox.stub(conversation, "setIncomingSessionData");
sandbox.stub(conversation, "setOutgoingSessionData");
});
describe("Routes", function() {
var router;
beforeEach(function() {
router = new ConversationRouter({
@@ -182,63 +182,135 @@ describe("loop.conversation", function()
client.requestCallsInfo.callsArgWith(1, "failed");
router.incoming(42);
sinon.assert.calledOnce(notifier.errorL10n);
});
describe("requestCallsInfo successful", function() {
- var fakeSessionData;
+ var fakeSessionData, resolvePromise, rejectPromise;
beforeEach(function() {
fakeSessionData = {
- sessionId: "sessionId",
- sessionToken: "sessionToken",
- apiKey: "apiKey",
- callType: "callType"
+ sessionId: "sessionId",
+ sessionToken: "sessionToken",
+ apiKey: "apiKey",
+ callType: "callType",
+ callId: "Hello",
+ progressURL: "http://progress.example.com",
+ websocketToken: 123
};
+ sandbox.stub(router, "_setupWebSocketAndCallView");
+ sandbox.stub(conversation, "setIncomingSessionData");
+
client.requestCallsInfo.callsArgWith(1, null, [fakeSessionData]);
});
it("should store the session data", function() {
- router.incoming(42);
+ router.incoming("fakeVersion");
sinon.assert.calledOnce(conversation.setIncomingSessionData);
sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
fakeSessionData);
});
- it("should call the view with video.enabled=false", function() {
- sandbox.stub(conversation, "get").withArgs("callType").returns("audio");
+ it("should call #_setupWebSocketAndCallView", function() {
+
router.incoming("fakeVersion");
- sinon.assert.calledOnce(conversation.get);
- sinon.assert.calledOnce(loop.conversation.IncomingCallView);
- sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
- {model: conversation,
- video: {enabled: false}});
+ sinon.assert.calledOnce(router._setupWebSocketAndCallView);
+ sinon.assert.calledWithExactly(router._setupWebSocketAndCallView);
+ });
+ });
+
+ describe("#_setupWebSocketAndCallView", function() {
+ beforeEach(function() {
+ conversation.setIncomingSessionData({
+ sessionId: "sessionId",
+ sessionToken: "sessionToken",
+ apiKey: "apiKey",
+ callType: "callType",
+ callId: "Hello",
+ progressURL: "http://progress.example.com",
+ websocketToken: 123
+ });
});
- it("should display the incoming call view", function() {
- sandbox.stub(conversation, "get").withArgs("callType")
- .returns("audio-video");
- router.incoming("fakeVersion");
+ describe("Websocket connection successful", function() {
+ var promise;
+
+ beforeEach(function() {
+ sandbox.stub(loop, "CallConnectionWebSocket").returns({
+ promiseConnect: function() {
+ promise = new Promise(function(resolve, reject) {
+ resolve();
+ });
+ return promise;
+ }
+ });
+ });
+
+ it("should create a CallConnectionWebSocket", function(done) {
+ router._setupWebSocketAndCallView();
+
+ 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.enabled=false", function(done) {
+ sandbox.stub(conversation, "get").withArgs("callType").returns("audio");
+
+ router._setupWebSocketAndCallView();
- sinon.assert.calledOnce(loop.conversation.IncomingCallView);
- sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
- {model: conversation,
- video: {enabled: true}});
- sinon.assert.calledOnce(router.loadReactComponent);
- sinon.assert.calledWith(router.loadReactComponent,
- sinon.match(function(value) {
- return TestUtils.isDescriptorOfType(value,
- loop.conversation.IncomingCallView);
- }));
+ promise.then(function () {
+ sinon.assert.called(conversation.get);
+ sinon.assert.calledOnce(loop.conversation.IncomingCallView);
+ sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
+ {model: conversation,
+ video: {enabled: 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;
+ }
+ });
+ });
+
+ it("should display an error", function(done) {
+ router._setupWebSocketAndCallView();
+
+ promise.then(function() {
+ }, function () {
+ sinon.assert.calledOnce(router._notifier.errorL10n);
+ sinon.assert.calledWithExactly(router._notifier.errorL10n,
+ "cannot_start_call_session_not_ready");
+ done();
+ });
+ });
});
});
});
describe("#accept", function() {
it("should initiate the conversation", function() {
router.accept();
@@ -286,20 +358,24 @@ describe("loop.conversation", function()
sinon.assert.calledWithExactly(router._notifier.errorL10n,
"cannot_start_call_session_not_ready");
});
});
describe("#decline", function() {
beforeEach(function() {
sandbox.stub(window, "close");
+ router._websocket = {
+ decline: sandbox.spy()
+ };
});
it("should close the window", function() {
router.decline();
+ sandbox.clock.tick(1);
sinon.assert.calledOnce(window.close);
});
it("should stop alerting", function() {
sandbox.stub(navigator.mozLoop, "stopAlerting");
router.decline();
@@ -340,16 +416,23 @@ describe("loop.conversation", function()
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()
+ };
+ sandbox.stub(window, "close");
+ });
+
it("should call mozLoop.stopAlerting", function() {
sandbox.stub(navigator.mozLoop, "stopAlerting");
router.declineAndBlock();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
});
it("should call delete call", function() {
@@ -370,19 +453,20 @@ describe("loop.conversation", function()
});
router.declineAndBlock();
sinon.assert.calledOnce(log);
sinon.assert.calledWithExactly(log, fakeError);
});
it("should close the window", function() {
- sandbox.stub(window, "close");
router.declineAndBlock();
+ sandbox.clock.tick(1);
+
sinon.assert.calledOnce(window.close);
});
});
});
describe("Events", function() {
var router, fakeSessionData;
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -32,16 +32,17 @@
</script>
<!-- App scripts -->
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/router.js"></script>
<script src="../../content/shared/js/views.js"></script>
+ <script src="../../content/shared/js/websocket.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/panel.js"></script>
<!-- Test scripts -->
<script src="client_test.js"></script>
<script src="conversation_test.js"></script>
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -31,22 +31,24 @@
chai.Assertion.includeStack = true;
mocha.setup('bdd');
</script>
<!-- App scripts -->
<script src="../../content/shared/js/models.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/feedbackApiClient.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>
<script src="views_test.js"></script>
<script src="router_test.js"></script>
+ <script src="websocket_test.js"></script>
<script src="feedbackApiClient_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
});
</script>
</body>
</html>
--- a/browser/components/loop/test/shared/models_test.js
+++ b/browser/components/loop/test/shared/models_test.js
@@ -17,20 +17,21 @@ describe("loop.shared.models", function(
sandbox.useFakeTimers();
fakeXHR = sandbox.useFakeXMLHttpRequest();
requests = [];
// https://github.com/cjohansen/Sinon.JS/issues/393
fakeXHR.xhr.onCreate = function(xhr) {
requests.push(xhr);
};
fakeSessionData = {
- sessionId: "sessionId",
- sessionToken: "sessionToken",
- apiKey: "apiKey",
- callType: "callType"
+ sessionId: "sessionId",
+ sessionToken: "sessionToken",
+ apiKey: "apiKey",
+ callType: "callType",
+ websocketToken: 123
};
fakeSession = _.extend({
connect: function () {},
endSession: sandbox.stub(),
set: sandbox.stub(),
disconnect: sandbox.spy(),
unpublish: sandbox.spy()
}, Backbone.Events);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/shared/websocket_test.js
@@ -0,0 +1,224 @@
+/* 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, it, beforeEach, afterEach, describe */
+
+var expect = chai.expect;
+
+describe("loop.CallConnectionWebSocket", function() {
+ "use strict";
+
+ var sandbox,
+ dummySocket;
+
+ beforeEach(function() {
+ sandbox = sinon.sandbox.create();
+ sandbox.useFakeTimers();
+
+ dummySocket = {
+ send: sinon.spy()
+ };
+ sandbox.stub(window, 'WebSocket').returns(dummySocket);
+ });
+
+ afterEach(function() {
+ sandbox.restore();
+ });
+
+ describe("#constructor", function() {
+ it("should require a url option", function() {
+ expect(function() {
+ return new loop.CallConnectionWebSocket();
+ }).to.Throw(/No url/);
+ });
+
+ it("should require a callId setting", function() {
+ expect(function() {
+ return new loop.CallConnectionWebSocket({url: "wss://fake/"});
+ }).to.Throw(/No callId/);
+ });
+
+ it("should require a websocketToken setting", function() {
+ expect(function() {
+ return new loop.CallConnectionWebSocket({
+ url: "http://fake/",
+ callId: "hello"
+ });
+ }).to.Throw(/No websocketToken/);
+ });
+ });
+
+ describe("constructed", function() {
+ var callWebSocket, fakeUrl, fakeCallId, fakeWebSocketToken;
+
+ beforeEach(function() {
+ fakeUrl = "wss://fake/";
+ fakeCallId = "callId";
+ fakeWebSocketToken = "7b";
+
+ callWebSocket = new loop.CallConnectionWebSocket({
+ url: fakeUrl,
+ callId: fakeCallId,
+ websocketToken: fakeWebSocketToken
+ });
+ });
+
+ describe("#promiseConnect", function() {
+ it("should create a new websocket connection", function() {
+ callWebSocket.promiseConnect();
+
+ sinon.assert.calledOnce(window.WebSocket);
+ sinon.assert.calledWithExactly(window.WebSocket, fakeUrl);
+ });
+
+ it("should reject the promise if connection is not completed in " +
+ "5 seconds", function(done) {
+ var promise = callWebSocket.promiseConnect();
+
+ sandbox.clock.tick(5101);
+
+ promise.then(function() {}, function(error) {
+ expect(error).to.be.equal("timeout");
+ done();
+ });
+ });
+
+ it("should reject the promise if the connection errors", function(done) {
+ var promise = callWebSocket.promiseConnect();
+
+ dummySocket.onerror("error");
+
+ promise.then(function() {}, function(error) {
+ expect(error).to.be.equal("error");
+ done();
+ });
+ });
+
+ it("should reject the promise if the connection closes", function(done) {
+ var promise = callWebSocket.promiseConnect();
+
+ dummySocket.onclose("close");
+
+ promise.then(function() {}, function(error) {
+ expect(error).to.be.equal("close");
+ done();
+ });
+ });
+
+ it("should send hello when the socket is opened", function() {
+ callWebSocket.promiseConnect();
+
+ dummySocket.onopen();
+
+ sinon.assert.calledOnce(dummySocket.send);
+ sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
+ messageType: "hello",
+ callId: fakeCallId,
+ auth: fakeWebSocketToken
+ }));
+ });
+
+ it("should resolve the promise when the 'hello' is received",
+ function(done) {
+ var promise = callWebSocket.promiseConnect();
+
+ dummySocket.onmessage({
+ data: '{"messageType":"hello", "state":"init"}'
+ });
+
+ promise.then(function() {
+ done();
+ });
+ });
+ });
+
+ describe("#decline", function() {
+ it("should send a terminate message to the server", function() {
+ callWebSocket.promiseConnect();
+
+ callWebSocket.decline();
+
+ sinon.assert.calledOnce(dummySocket.send);
+ sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
+ messageType: "action",
+ event: "terminate",
+ reason: "reject"
+ }));
+ });
+ });
+
+ describe("Events", function() {
+ beforeEach(function() {
+ sandbox.stub(callWebSocket, "trigger");
+
+ callWebSocket.promiseConnect();
+ });
+
+ describe("Progress", function() {
+ it("should trigger a progress event on the callWebSocket", function() {
+ var eventData = {
+ messageType: "progress",
+ state: "terminate",
+ reason: "reject"
+ };
+
+ dummySocket.onmessage({
+ data: JSON.stringify(eventData)
+ });
+
+ sinon.assert.calledOnce(callWebSocket.trigger);
+ sinon.assert.calledWithExactly(callWebSocket.trigger, "progress", eventData);
+ });
+ });
+
+ describe("Error", function() {
+ // Handled in constructed -> #promiseConnect:
+ // should reject the promise if the connection errors
+
+ it("should trigger an error if state is not completed", function() {
+ callWebSocket._clearConnectionFlags();
+
+ dummySocket.onerror("Error");
+
+ sinon.assert.calledOnce(callWebSocket.trigger);
+ sinon.assert.calledWithExactly(callWebSocket.trigger,
+ "error", "Error");
+ });
+
+ it("should not trigger an error if state is completed", function() {
+ callWebSocket._clearConnectionFlags();
+ callWebSocket._lastServerState = "connected";
+
+ dummySocket.onerror("Error");
+
+ sinon.assert.notCalled(callWebSocket.trigger);
+ });
+ });
+
+ describe("Close", function() {
+ // Handled in constructed -> #promiseConnect:
+ // should reject the promise if the connection closes
+
+ it("should trigger a close event if state is not completed", function() {
+ callWebSocket._clearConnectionFlags();
+
+ dummySocket.onclose("Error");
+
+ sinon.assert.calledOnce(callWebSocket.trigger);
+ sinon.assert.calledWithExactly(callWebSocket.trigger,
+ "closed", "Error");
+ });
+
+ it("should not trigger an error if state is completed", function() {
+ callWebSocket._clearConnectionFlags();
+ callWebSocket._lastServerState = "terminated";
+
+ dummySocket.onclose("Error");
+
+ sinon.assert.notCalled(callWebSocket.trigger);
+ });
+ });
+ });
+ });
+});
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -30,16 +30,17 @@
chai.Assertion.includeStack = true;
mocha.setup('bdd');
</script>
<!-- App scripts -->
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/models.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="../../standalone/content/js/standaloneClient.js"></script>
<script src="../../standalone/content/js/webapp.js"></script>
<!-- Test scripts -->
<script src="standalone_client_test.js"></script>
<script src="webapp_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -90,40 +90,175 @@ describe("loop.webapp", function() {
conversation: conversation,
notifier: notifier
});
sandbox.stub(router, "loadView");
sandbox.stub(router, "navigate");
});
describe("#startCall", function() {
+ beforeEach(function() {
+ sandbox.stub(router, "_setupWebSocketAndCallView");
+ });
+
it("should navigate back home if session token is missing", function() {
router.startCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "home");
});
it("should notify the user if session token is missing", function() {
router.startCall();
sinon.assert.calledOnce(notifier.errorL10n);
sinon.assert.calledWithExactly(notifier.errorL10n,
"missing_conversation_info");
});
- it("should navigate to call/ongoing/:token if session token is available",
- function() {
- conversation.set("loopToken", "fake");
+ it("should setup the websocket if session token is available", function() {
+ conversation.set("loopToken", "fake");
+
+ router.startCall();
+
+ sinon.assert.calledOnce(router._setupWebSocketAndCallView);
+ sinon.assert.calledWithExactly(router._setupWebSocketAndCallView, "fake");
+ });
+ });
+
+ describe("#_setupWebSocketAndCallView", function() {
+ beforeEach(function() {
+ conversation.setOutgoingSessionData({
+ sessionId: "sessionId",
+ sessionToken: "sessionToken",
+ apiKey: "apiKey",
+ callId: "Hello",
+ progressURL: "http://progress.example.com",
+ websocketToken: 123
+ });
+ });
+
+ describe("Websocket connection successful", function() {
+ var promise;
+
+ beforeEach(function() {
+ sandbox.stub(loop, "CallConnectionWebSocket").returns({
+ promiseConnect: function() {
+ promise = new Promise(function(resolve, reject) {
+ resolve();
+ });
+ return promise;
+ },
+
+ on: sandbox.spy()
+ });
+ });
+
+ it("should create a CallConnectionWebSocket", function(done) {
+ router._setupWebSocketAndCallView("fake");
+
+ 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 navigate to call/ongoing/:token", function(done) {
+ router._setupWebSocketAndCallView("fake");
+
+ promise.then(function () {
+ sinon.assert.calledOnce(router.navigate);
+ sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
+ done();
+ });
+ });
+ });
+
+ describe("Websocket connection failed", function() {
+ var promise;
- router.startCall();
+ beforeEach(function() {
+ sandbox.stub(loop, "CallConnectionWebSocket").returns({
+ promiseConnect: function() {
+ promise = new Promise(function(resolve, reject) {
+ reject();
+ });
+ return promise;
+ },
+
+ on: sandbox.spy()
+ });
+ });
+
+ it("should display an error", function() {
+ router._setupWebSocketAndCallView();
+
+ promise.then(function() {
+ }, function () {
+ sinon.assert.calledOnce(router._notifier.errorL10n);
+ sinon.assert.calledWithExactly(router._notifier.errorL10n,
+ "cannot_start_call_session_not_ready");
+ done();
+ });
+ });
+ });
+
+ describe("Websocket Events", function() {
+ beforeEach(function() {
+ conversation.setOutgoingSessionData({
+ sessionId: "sessionId",
+ sessionToken: "sessionToken",
+ apiKey: "apiKey",
+ callId: "Hello",
+ progressURL: "http://progress.example.com",
+ websocketToken: 123
+ });
- sinon.assert.calledOnce(router.navigate);
- sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
+ sandbox.stub(loop.CallConnectionWebSocket.prototype,
+ "promiseConnect").returns({
+ then: sandbox.spy()
+ });
+
+ router._setupWebSocketAndCallView();
});
+
+ describe("Progress", function() {
+ describe("state: terminate, reason: reject", function() {
+ beforeEach(function() {
+ sandbox.stub(router, "endCall");
+ });
+
+ it("should end the call", function() {
+ router._websocket.trigger("progress", {
+ state: "terminated",
+ reason: "reject"
+ });
+
+ sinon.assert.calledOnce(router.endCall);
+ });
+
+ it("should display an error message", function() {
+ router._websocket.trigger("progress", {
+ state: "terminated",
+ reason: "reject"
+ });
+
+ sinon.assert.calledOnce(router._notifier.errorL10n);
+ sinon.assert.calledWithExactly(router._notifier.errorL10n,
+ "call_timeout_notification_text");
+ });
+ });
+ });
+ });
});
describe("#endCall", function() {
it("should navigate to home if session token is unset", function() {
router.endCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "home");
@@ -236,30 +371,31 @@ describe("loop.webapp", function() {
});
});
describe("Events", function() {
var fakeSessionData;
beforeEach(function() {
fakeSessionData = {
- sessionId: "sessionId",
- sessionToken: "sessionToken",
- apiKey: "apiKey"
+ sessionId: "sessionId",
+ sessionToken: "sessionToken",
+ apiKey: "apiKey",
+ websocketToken: 123
};
conversation.set("loopToken", "fakeToken");
+ sandbox.stub(router, "startCall");
});
- it("should navigate to call/ongoing/:token once call session is ready",
+ it("should attempt to start the call once call session is ready",
function() {
router.setupOutgoingCall();
conversation.outgoing(fakeSessionData);
- sinon.assert.calledOnce(router.navigate);
- sinon.assert.calledWith(router.navigate, "call/ongoing/fakeToken");
+ sinon.assert.calledOnce(router.startCall);
});
it("should navigate to call/{token} when conversation ended", function() {
conversation.trigger("session:ended");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
});