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 225359 59a9099979f85393ab7b3fa9311e90cfcd905c8a
parent 225358 a1cd55076e81022463f261a7daa5ec5bd60ff854
child 225360 98b285915390a7d4d4974bd6655b5fd2fef832a9
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnperriault
bugs1067519
milestone34.0a2
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"
           };