Bug 1107255 - Close panel on call conversation start, r=:NiKo` a=lsblakk
authorDan Mosedale <dmose@meer.net>
Tue, 09 Dec 2014 11:22:47 -0800
changeset 234218 abdf900b595db7470c79886376e3b68bdfe7b4e8
parent 234217 9ddd28e950d02ce57ffa1177405fdb45333a1515
child 234219 6467c5966b50d15704bb01249a2d3b92ac278fac
push id4239
push userrjesup@wgate.com
push dateTue, 16 Dec 2014 14:37:13 +0000
treeherdermozilla-beta@4f0898e01551 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslsblakk
bugs1107255
milestone35.0
Bug 1107255 - Close panel on call conversation start, r=:NiKo` a=lsblakk
browser/components/loop/content/js/contacts.js
browser/components/loop/content/js/contacts.jsx
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/content/shared/js/mixins.js
browser/components/loop/test/desktop-local/contacts_test.js
browser/components/loop/test/desktop-local/panel_test.js
browser/components/loop/test/shared/mixins_test.js
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -253,17 +253,20 @@ loop.contacts = (function(_, mozL10n) {
             : null
           
         )
       );
     }
   });
 
   const ContactsList = React.createClass({displayName: 'ContactsList',
-    mixins: [React.addons.LinkedStateMixin],
+    mixins: [
+      React.addons.LinkedStateMixin,
+      loop.shared.mixins.WindowCloseMixin
+    ],
 
     /**
      * Contacts collection object
      */
     contacts: null,
 
     /**
      * User profile
@@ -430,21 +433,23 @@ loop.contacts = (function(_, mozL10n) {
             if (err) {
               throw err;
             }
           });
           break;
         case "video-call":
           if (!contact.blocked) {
             navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+            this.closeWindow();
           }
           break;
         case "audio-call":
           if (!contact.blocked) {
             navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+            this.closeWindow();
           }
           break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
 
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -253,17 +253,20 @@ loop.contacts = (function(_, mozL10n) {
             : null
           }
         </li>
       );
     }
   });
 
   const ContactsList = React.createClass({
-    mixins: [React.addons.LinkedStateMixin],
+    mixins: [
+      React.addons.LinkedStateMixin,
+      loop.shared.mixins.WindowCloseMixin
+    ],
 
     /**
      * Contacts collection object
      */
     contacts: null,
 
     /**
      * User profile
@@ -430,21 +433,23 @@ loop.contacts = (function(_, mozL10n) {
             if (err) {
               throw err;
             }
           });
           break;
         case "video-call":
           if (!contact.blocked) {
             navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+            this.closeWindow();
           }
           break;
         case "audio-call":
           if (!contact.blocked) {
             navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+            this.closeWindow();
           }
           break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
 
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -525,30 +525,33 @@ loop.panel = (function(_, mozL10n) {
    * Room list entry.
    */
   var RoomEntry = React.createClass({displayName: 'RoomEntry',
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       room:       React.PropTypes.instanceOf(loop.store.Room).isRequired
     },
 
+    mixins: [loop.shared.mixins.WindowCloseMixin],
+
     getInitialState: function() {
       return { urlCopied: false };
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {
       return (nextProps.room.ctime > this.props.room.ctime) ||
         (nextState.urlCopied !== this.state.urlCopied);
     },
 
     handleClickRoomUrl: function(event) {
       event.preventDefault();
       this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
         roomToken: this.props.room.roomToken
       }));
+      this.closeWindow();
     },
 
     handleCopyButtonClick: function(event) {
       event.preventDefault();
       this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
         roomUrl: this.props.room.roomUrl
       }));
       this.setState({urlCopied: true});
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -525,30 +525,33 @@ loop.panel = (function(_, mozL10n) {
    * Room list entry.
    */
   var RoomEntry = React.createClass({
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       room:       React.PropTypes.instanceOf(loop.store.Room).isRequired
     },
 
+    mixins: [loop.shared.mixins.WindowCloseMixin],
+
     getInitialState: function() {
       return { urlCopied: false };
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {
       return (nextProps.room.ctime > this.props.room.ctime) ||
         (nextState.urlCopied !== this.state.urlCopied);
     },
 
     handleClickRoomUrl: function(event) {
       event.preventDefault();
       this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
         roomToken: this.props.room.roomToken
       }));
+      this.closeWindow();
     },
 
     handleCopyButtonClick: function(event) {
       event.preventDefault();
       this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
         roomUrl: this.props.room.roomUrl
       }));
       this.setState({urlCopied: true});
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -11,18 +11,20 @@ loop.shared.mixins = (function() {
 
   /**
    * Root object, by default set to window.
    * @type {DOMWindow|Object}
    */
   var rootObject = window;
 
   /**
-   * Sets a new root object. This is useful for testing native DOM events so we
-   * can fake them.
+   * Sets a new root object.  This is useful for testing native DOM events so we
+   * can fake them. In beforeEach(), loop.shared.mixins.setRootObject is used to
+   * substitute a fake window, and in afterEach(), the real window object is
+   * replaced.
    *
    * @param {Object}
    */
   function setRootObject(obj) {
     console.info("loop.shared.mixins: rootObject set to " + obj);
     rootObject = obj;
   }
 
@@ -60,16 +62,31 @@ loop.shared.mixins = (function() {
    */
   var DocumentTitleMixin = {
     setTitle: function(newTitle) {
       rootObject.document.title = newTitle;
     }
   };
 
   /**
+   * Window close mixin, for more testable closing of windows.  Instead of
+   * calling window.close() directly, use this mixin and call
+   * this.closeWindow from your component.
+   *
+   * @type {Object}
+   *
+   * @see setRootObject for info on how to unit test code that uses this mixin
+   */
+  var WindowCloseMixin = {
+    closeWindow: function() {
+      rootObject.close();
+    }
+  };
+
+  /**
    * Dropdown menu mixin.
    * @type {Object}
    */
   var DropdownMenuMixin = {
     get documentBody() {
       return rootObject.document.body;
     },
 
@@ -286,11 +303,12 @@ loop.shared.mixins = (function() {
   return {
     AudioMixin: AudioMixin,
     RoomsAudioMixin: RoomsAudioMixin,
     setRootObject: setRootObject,
     DropdownMenuMixin: DropdownMenuMixin,
     DocumentVisibilityMixin: DocumentVisibilityMixin,
     DocumentLocationMixin: DocumentLocationMixin,
     DocumentTitleMixin: DocumentTitleMixin,
-    UrlHashChangeMixin: UrlHashChangeMixin
+    UrlHashChangeMixin: UrlHashChangeMixin,
+    WindowCloseMixin: WindowCloseMixin
   };
 })();
--- a/browser/components/loop/test/desktop-local/contacts_test.js
+++ b/browser/components/loop/test/desktop-local/contacts_test.js
@@ -9,37 +9,88 @@ var expect = chai.expect;
 var TestUtils = React.addons.TestUtils;
 
 describe("loop.contacts", function() {
   "use strict";
 
   var fakeAddContactButtonText = "Fake Add Contact";
   var fakeEditContactButtonText = "Fake Edit Contact";
   var fakeDoneButtonText = "Fake Done";
+  var sandbox;
+  var fakeWindow;
 
   beforeEach(function(done) {
+    sandbox = sinon.sandbox.create();
     navigator.mozLoop = {
       getStrings: function(entityName) {
         var textContentValue = "fakeText";
         if (entityName == "add_contact_button") {
           textContentValue = fakeAddContactButtonText;
         } else if (entityName == "edit_contact_title") {
           textContentValue = fakeEditContactButtonText;
         } else if (entityName == "edit_contact_done_button") {
           textContentValue = fakeDoneButtonText;
         }
         return JSON.stringify({textContent: textContentValue});
       },
     };
 
+    fakeWindow = {
+      close: sandbox.stub(),
+      //document: { addEventListener: function(){} }
+    };
+    loop.shared.mixins.setRootObject(fakeWindow);
+
     document.mozL10n.initialize(navigator.mozLoop);
     // XXX prevent a race whenever mozL10n hasn't been initialized yet
     setTimeout(done, 0);
   });
 
+  afterEach(function() {
+    loop.shared.mixins.setRootObject(window);
+    sandbox.restore();
+  });
+
+
+  describe("ContactsList", function () {
+    var listView;
+
+    beforeEach(function() {
+      navigator.mozLoop.calls = {
+        startDirectCall: sandbox.stub(),
+        clearCallInProgress: sandbox.stub()
+      };
+      navigator.mozLoop.contacts = {getAll: sandbox.stub()};
+
+      listView = TestUtils.renderIntoDocument(loop.contacts.ContactsList());
+    });
+
+    afterEach(function() {
+      listView = null;
+      delete navigator.mozLoop.calls;
+      delete navigator.mozLoop.contacts;
+    });
+
+    describe("#handleContactAction", function() {
+      it("should call window.close when called with 'video-call' action",
+        function() {
+          listView.handleContactAction({}, "video-call");
+
+          sinon.assert.calledOnce(fakeWindow.close);
+      });
+
+      it("should call window.close when called with 'audio-call' action",
+        function() {
+          listView.handleContactAction({}, "audio-call");
+
+          sinon.assert.calledOnce(fakeWindow.close);
+        });
+    });
+  });
+
   describe("ContactDetailsForm", function() {
     describe("#render", function() {
       describe("add mode", function() {
         it("should render 'add' header", function() {
           var view = TestUtils.renderIntoDocument(
             loop.contacts.ContactDetailsForm({mode: "add"}));
 
           var header = view.getDOMNode().querySelector("header");
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -8,26 +8,33 @@
 var expect = chai.expect;
 var TestUtils = React.addons.TestUtils;
 var sharedActions = loop.shared.actions;
 var sharedUtils = loop.shared.utils;
 
 describe("loop.panel", function() {
   "use strict";
 
-  var sandbox, notifications, fakeXHR, requests = [];
+  var sandbox, notifications, fakeXHR, fakeWindow, requests = [];
 
   beforeEach(function(done) {
     sandbox = sinon.sandbox.create();
     fakeXHR = sandbox.useFakeXMLHttpRequest();
     requests = [];
     // https://github.com/cjohansen/Sinon.JS/issues/393
     fakeXHR.xhr.onCreate = function (xhr) {
       requests.push(xhr);
+    }
+
+    fakeWindow = {
+      close: sandbox.stub(),
+      document: { addEventListener: function(){} }
     };
+    loop.shared.mixins.setRootObject(fakeWindow);
+
     notifications = new loop.shared.models.NotificationCollection();
 
     navigator.mozLoop = {
       doNotDisturb: true,
       fxAEnabled: true,
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
@@ -60,16 +67,17 @@ describe("loop.panel", function() {
 
     document.mozL10n.initialize(navigator.mozLoop);
     // XXX prevent a race whenever mozL10n hasn't been initialized yet
     setTimeout(done, 0);
   });
 
   afterEach(function() {
     delete navigator.mozLoop;
+    loop.shared.mixins.setRootObject(window);
     sandbox.restore();
   });
 
   describe("#init", function() {
     beforeEach(function() {
       sandbox.stub(React, "renderComponent");
       sandbox.stub(document.mozL10n, "initialize");
       sandbox.stub(document.mozL10n, "get").returns("Fake title");
@@ -799,32 +807,41 @@ describe("loop.panel", function() {
         TestUtils.Simulate.click(deleteButton);
 
         sinon.assert.calledOnce(navigator.mozLoop.confirm);
         sinon.assert.notCalled(dispatcher.dispatch);
       });
     });
 
     describe("Room URL click", function() {
-      var roomEntry;
+      var roomEntry, urlLink;
 
-      it("should dispatch an OpenRoom action", function() {
+      beforeEach(function() {
         sandbox.stub(dispatcher, "dispatch");
+
         roomEntry = mountRoomEntry({
           dispatcher: dispatcher,
           room: new loop.store.Room(roomData)
         });
-        var urlLink = roomEntry.getDOMNode().querySelector("p > a");
+        urlLink = roomEntry.getDOMNode().querySelector("p > a");
+      });
 
+      it("should dispatch an OpenRoom action", function() {
         TestUtils.Simulate.click(urlLink);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.OpenRoom({roomToken: roomData.roomToken}));
       });
+
+      it("should call window.close", function() {
+        TestUtils.Simulate.click(urlLink);
+
+        sinon.assert.calledOnce(fakeWindow.close);
+      });
     });
   });
 
   describe("loop.panel.RoomList", function() {
     var roomStore, dispatcher, fakeEmail;
 
     beforeEach(function() {
       fakeEmail = "fakeEmail@example.com";
--- a/browser/components/loop/test/shared/mixins_test.js
+++ b/browser/components/loop/test/shared/mixins_test.js
@@ -112,16 +112,44 @@ describe("loop.shared.mixins", function(
       var comp = TestUtils.renderIntoDocument(TestComp());
 
       comp.setTitle("It's a Fake!");
 
       expect(rootObject.document.title).eql("It's a Fake!");
     });
   });
 
+
+  describe("loop.shared.mixins.WindowCloseMixin", function() {
+    var TestComp, rootObject;
+
+    beforeEach(function() {
+      rootObject = {
+        close: sandbox.stub()
+      };
+      sharedMixins.setRootObject(rootObject);
+
+      TestComp = React.createClass({
+        mixins: [loop.shared.mixins.WindowCloseMixin],
+        render: function() {
+          return React.DOM.div();
+        }
+      });
+    });
+
+    it("should call window.close", function() {
+      var comp = TestUtils.renderIntoDocument(TestComp());
+
+      comp.closeWindow();
+
+      sinon.assert.calledOnce(rootObject.close);
+      sinon.assert.calledWithExactly(rootObject.close);
+    });
+  });
+
   describe("loop.shared.mixins.DocumentVisibilityMixin", function() {
     var comp, TestComp, onDocumentVisibleStub, onDocumentHiddenStub;
 
     beforeEach(function() {
       onDocumentVisibleStub = sandbox.stub();
       onDocumentHiddenStub = sandbox.stub();
 
       TestComp = React.createClass({