Bug 1067519 Loop desktop client should close the conversation window if the caller chooses to cancel the call. r=nperriault
authorMark Banner <standard8@mozilla.com>
Tue, 16 Sep 2014 16:03:59 +0100
changeset 228678 93a2036a377e1019f92c802aba84fbc32809db67
parent 228677 6f609219f77a60d6c31c1cfecd747bd98562ba9a
child 228679 07d5640694f24670fd339ba75abfa14938ddc6c0
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnperriault
bugs1067519
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1067519 Loop desktop client should close the conversation window if the caller chooses to cancel the call. r=nperriault
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/shared/js/websocket.js
browser/components/loop/test/desktop-local/conversation_test.js
browser/components/loop/test/shared/websocket_test.js
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -279,31 +279,67 @@ loop.conversation = (function(OT, mozL10
         this.loadReactComponent(loop.conversation.IncomingCallView({
           model: this._conversation,
           video: this._conversation.hasVideoStream("incoming")
         }));
       }.bind(this), function() {
         this._handleSessionError();
         return;
       }.bind(this));
+
+      this._websocket.on("progress", this._handleWebSocketProgress, this);
     },
 
     /**
      * Checks if the streams have been connected, and notifies the
      * websocket that the media is now connected.
      */
     _checkConnected: function() {
       // Check we've had both local and remote streams connected before
       // sending the media up message.
       if (this._conversation.streamsConnected()) {
         this._websocket.mediaUp();
       }
     },
 
     /**
+     * Used to receive websocket progress and to determine how to handle
+     * it if appropraite.
+     * If we add more cases here, then we should refactor this function.
+     *
+     * @param {Object} progressData The progress data from the websocket.
+     * @param {String} previousState The previous state from the websocket.
+     */
+    _handleWebSocketProgress: function(progressData, previousState) {
+      // We only care about the terminated state at the moment.
+      if (progressData.state !== "terminated")
+        return;
+
+      if (progressData.reason === "cancel") {
+        this._abortIncomingCall();
+        return;
+      }
+
+      if (progressData.reason === "timeout" &&
+          (previousState === "init" || previousState === "alerting")) {
+        this._abortIncomingCall();
+      }
+    },
+
+    /**
+     * Silently aborts an incoming call - stops the alerting, and
+     * closes the websocket.
+     */
+    _abortIncomingCall: function() {
+      navigator.mozLoop.stopAlerting();
+      this._websocket.close();
+      window.close();
+    },
+
+    /**
      * Accepts an incoming call.
      */
     accept: function() {
       navigator.mozLoop.stopAlerting();
       this._websocket.accept();
       this._conversation.incoming();
     },
 
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -279,31 +279,67 @@ loop.conversation = (function(OT, mozL10
         this.loadReactComponent(loop.conversation.IncomingCallView({
           model: this._conversation,
           video: this._conversation.hasVideoStream("incoming")
         }));
       }.bind(this), function() {
         this._handleSessionError();
         return;
       }.bind(this));
+
+      this._websocket.on("progress", this._handleWebSocketProgress, this);
     },
 
     /**
      * Checks if the streams have been connected, and notifies the
      * websocket that the media is now connected.
      */
     _checkConnected: function() {
       // Check we've had both local and remote streams connected before
       // sending the media up message.
       if (this._conversation.streamsConnected()) {
         this._websocket.mediaUp();
       }
     },
 
     /**
+     * Used to receive websocket progress and to determine how to handle
+     * it if appropraite.
+     * If we add more cases here, then we should refactor this function.
+     *
+     * @param {Object} progressData The progress data from the websocket.
+     * @param {String} previousState The previous state from the websocket.
+     */
+    _handleWebSocketProgress: function(progressData, previousState) {
+      // We only care about the terminated state at the moment.
+      if (progressData.state !== "terminated")
+        return;
+
+      if (progressData.reason === "cancel") {
+        this._abortIncomingCall();
+        return;
+      }
+
+      if (progressData.reason === "timeout" &&
+          (previousState === "init" || previousState === "alerting")) {
+        this._abortIncomingCall();
+      }
+    },
+
+    /**
+     * Silently aborts an incoming call - stops the alerting, and
+     * closes the websocket.
+     */
+    _abortIncomingCall: function() {
+      navigator.mozLoop.stopAlerting();
+      this._websocket.close();
+      window.close();
+    },
+
+    /**
      * Accepts an incoming call.
      */
     accept: function() {
       navigator.mozLoop.stopAlerting();
       this._websocket.accept();
       this._conversation.incoming();
     },
 
--- a/browser/components/loop/content/shared/js/websocket.js
+++ b/browser/components/loop/content/shared/js/websocket.js
@@ -31,16 +31,18 @@ loop.CallConnectionWebSocket = (function
     }
     if (!this.options.callId) {
       throw new Error("No callId in options");
     }
     if (!this.options.websocketToken) {
       throw new Error("No websocketToken in options");
     }
 
+    this._lastServerState = "init";
+
     // Set loop.debug.sdk to true in the browser, or standalone:
     // localStorage.setItem("debug.websocket", true);
     this._debugWebSocket =
       loop.shared.utils.getBoolPreference("debug.websocket");
 
     _.extend(this, Backbone.Events);
   };
 
@@ -73,16 +75,26 @@ loop.CallConnectionWebSocket = (function
             reject: reject,
             timeout: timeout
           };
         }.bind(this));
 
       return promise;
     },
 
+    /**
+     * Closes the websocket. This shouldn't be the normal action as the server
+     * will normally close the socket. Only in bad error cases, or where we need
+     * to close the socket just before closing the window (to avoid an error)
+     * should we call this.
+     */
+    close: function() {
+      this.socket.close();
+    },
+
     _clearConnectionFlags: function() {
       clearTimeout(this.connectDetails.timeout);
       delete this.connectDetails;
     },
 
     /**
      * Internal function called to resolve the connection promise.
      *
@@ -205,25 +217,26 @@ loop.CallConnectionWebSocket = (function
         msg = JSON.parse(event.data);
       } catch (x) {
         console.error("Error parsing received message:", x);
         return;
       }
 
       this._log("WS Receiving", event.data);
 
+      var previousState = this._lastServerState;
       this._lastServerState = msg.state;
 
       switch(msg.messageType) {
         case "hello":
           this._completeConnection();
           break;
         case "progress":
           this.trigger("progress:" + msg.state);
-          this.trigger("progress", msg);
+          this.trigger("progress", msg, previousState);
           break;
       }
     },
 
     /**
      * Called when there is an error on the websocket.
      *
      * @param {Object} event A simple error event.
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -218,17 +218,18 @@ describe("loop.conversation", function()
 
             beforeEach(function() {
               sandbox.stub(loop, "CallConnectionWebSocket").returns({
                 promiseConnect: function() {
                   promise = new Promise(function(resolve, reject) {
                     resolve();
                   });
                   return promise;
-                }
+                },
+                on: sinon.spy()
               });
             });
 
             it("should create a CallConnectionWebSocket", function(done) {
               router._setupWebSocketAndCallView();
 
               promise.then(function () {
                 sinon.assert.calledOnce(loop.CallConnectionWebSocket);
@@ -263,33 +264,130 @@ describe("loop.conversation", function()
 
             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 stop alerting", function(done) {
+                  promise.then(function() {
+                    router._websocket.trigger("progress", {
+                      state: "terminated",
+                      reason: "timeout"
+                    }, "alerting");
+
+                    sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
+                    done();
+                  });
+                });
+
+                it("should close the websocket", function(done) {
+                  promise.then(function() {
+                    router._websocket.trigger("progress", {
+                      state: "terminated",
+                      reason: "timeout"
+                    }, "alerting");
+
+                    sinon.assert.calledOnce(router._websocket.close);
+                    done();
+                  });
+                });
+
+                it("should close the window", function(done) {
+                  promise.then(function() {
+                    router._websocket.trigger("progress", {
+                      state: "terminated",
+                      reason: "timeout"
+                    }, "alerting");
+
+                    sinon.assert.calledOnce(window.close);
+                    done();
+                  });
+                });
+              });
+            });
+          });
         });
       });
 
       describe("#accept", function() {
         beforeEach(function() {
           conversation.setIncomingSessionData({
             sessionId:      "sessionId",
             sessionToken:   "sessionToken",
--- a/browser/components/loop/test/shared/websocket_test.js
+++ b/browser/components/loop/test/shared/websocket_test.js
@@ -12,16 +12,17 @@ describe("loop.CallConnectionWebSocket",
   var sandbox,
       dummySocket;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     sandbox.useFakeTimers();
 
     dummySocket = {
+      close: sinon.spy(),
       send: sinon.spy()
     };
     sandbox.stub(window, 'WebSocket').returns(dummySocket);
   });
 
   afterEach(function() {
     sandbox.restore();
   });
@@ -128,16 +129,26 @@ describe("loop.CallConnectionWebSocket",
           });
 
           promise.then(function() {
             done();
           });
         });
     });
 
+    describe("#close", function() {
+      it("should close the socket", function() {
+        callWebSocket.promiseConnect();
+
+        callWebSocket.close();
+
+        sinon.assert.calledOnce(dummySocket.close);
+      });
+    });
+
     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({
@@ -207,17 +218,45 @@ describe("loop.CallConnectionWebSocket",
             reason: "reject"
           };
 
           dummySocket.onmessage({
             data: JSON.stringify(eventData)
           });
 
           sinon.assert.called(callWebSocket.trigger);
-          sinon.assert.calledWithExactly(callWebSocket.trigger, "progress", eventData);
+          sinon.assert.calledWithExactly(callWebSocket.trigger, "progress",
+                                         eventData, "init");
+        });
+
+        it("should trigger a progress event with the previous state", function() {
+          var previousEventData = {
+            messageType: "progress",
+            state: "alerting"
+          };
+
+          // This first call is to set the previous state of the object
+          // ready for the main test below.
+          dummySocket.onmessage({
+            data: JSON.stringify(previousEventData)
+          });
+
+          var currentEventData = {
+            messageType: "progress",
+            state: "terminate",
+            reason: "reject"
+          };
+
+          dummySocket.onmessage({
+            data: JSON.stringify(currentEventData)
+          });
+
+          sinon.assert.called(callWebSocket.trigger);
+          sinon.assert.calledWithExactly(callWebSocket.trigger, "progress",
+                                         currentEventData, "alerting");
         });
 
         it("should trigger a progress:<state> event on the callWebSocket", function() {
           var eventData = {
             messageType: "progress",
             state: "terminate",
             reason: "reject"
           };