Bug 1121210: notify UITour when the active tab changes and don't show the get started info panel when the rooms tab is not selected. r=MattN, a=sylvestre
authorMike de Boer <mdeboer@mozilla.com>
Thu, 12 Feb 2015 16:51:31 +0100
changeset 243764 6c0ded9eb9aa
parent 243763 78815ed2e606
child 243765 eb77152f1233
push id4467
push usermdeboer@mozilla.com
push date2015-02-12 15:59 +0000
treeherdermozilla-beta@6c0ded9eb9aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, sylvestre
bugs1121210
milestone36.0
Bug 1121210: notify UITour when the active tab changes and don't show the get started info panel when the rooms tab is not selected. r=MattN, a=sylvestre
browser/base/content/browser-loop.js
browser/components/loop/MozLoopAPI.jsm
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/test/desktop-local/panel_test.js
browser/components/loop/test/mochitest/browser_toolbarbutton.js
browser/modules/UITour.jsm
browser/modules/test/browser_UITour_loop.js
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -8,48 +8,94 @@ let LoopUI;
 XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms", "resource:///modules/loop/LoopRooms.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm");
 
 
 (function() {
   LoopUI = {
+    /**
+     * @var {XULWidgetSingleWrapper} toolbarButton Getter for the Loop toolbarbutton
+     *                                             instance for this window.
+     */
     get toolbarButton() {
       delete this.toolbarButton;
       return this.toolbarButton = CustomizableUI.getWidget("loop-button").forWindow(window);
     },
 
+    /**
+     * @var {XULElement} panel Getter for the Loop panel element.
+     */
     get panel() {
       delete this.panel;
       return this.panel = document.getElementById("loop-notification-panel");
     },
 
     /**
+     * @var {XULElement|null} browser Getter for the Loop panel browser element.
+     *                                Will be NULL if the panel hasn't loaded yet.
+     */
+    get browser() {
+      let browser = document.querySelector("#loop-notification-panel > #loop");
+      if (browser) {
+        delete this.browser;
+        this.browser = browser;
+      }
+      return browser;
+    },
+
+    /**
+     * @var {String|null} selectedTab Getter for the name of the currently selected
+     *                                tab inside the Loop panel. Will be NULL if
+     *                                the panel hasn't loaded yet.
+     */
+    get selectedTab() {
+      if (!this.browser) {
+        return null;
+      }
+
+      let selectedTab = this.browser.contentDocument.querySelector(".tab-view > .selected");
+      return selectedTab && selectedTab.getAttribute("data-tab-name");
+    },
+
+    /**
      * @return {Promise}
      */
     promiseDocumentVisible(aDocument) {
       if (!aDocument.hidden) {
         return Promise.resolve();
       }
 
       return new Promise((resolve) => {
         aDocument.addEventListener("visibilitychange", function onVisibilityChanged() {
           aDocument.removeEventListener("visibilitychange", onVisibilityChanged);
           resolve();
         });
       });
     },
 
+    /**
+     * Toggle between opening or hiding the Loop panel.
+     *
+     * @param {DOMEvent} [event] Optional event that triggered the call to this
+     *                           function.
+     * @param {String}   [tabId] Optional name of the tab to select after the panel
+     *                           has opened. Does nothing when the panel is hidden.
+     * @return {Promise}
+     */
     togglePanel: function(event, tabId = null) {
       if (this.panel.state == "open") {
-        this.panel.hidePopup();
-      } else {
-        this.openCallPanel(event, tabId);
+        return new Promise(resolve => {
+          this.panel.hidePopup();
+          resolve();
+        });
       }
+
+      return this.openCallPanel(event, tabId);
     },
 
     /**
      * Opens the panel for Loop and sizes it appropriately.
      *
      * @param {event}  event   The event opening the panel, used to anchor
      *                         the panel to the button which triggers it.
      * @param {String} [tabId] Identifier of the tab to select when the panel is
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -738,23 +738,25 @@ function injectLoopAPI(targetWindow) {
         });
       }
     },
 
     /**
      * Notifies the UITour module that an event occurred that it might be
      * interested in.
      *
-     * @param {String} subject Subject of the notification
+     * @param {String} subject  Subject of the notification
+     * @param {mixed}  [params] Optional parameters, providing more details to
+     *                          the notification subject
      */
     notifyUITour: {
       enumerable: true,
       writable: true,
-      value: function(subject) {
-        UITour.notify(subject);
+      value: function(subject, params) {
+        UITour.notify(subject, params);
       }
     },
   };
 
   function onStatusChanged(aSubject, aTopic, aData) {
     let event = new targetWindow.CustomEvent("LoopStatusChanged");
     targetWindow.dispatchEvent(event);
   };
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -20,25 +20,34 @@ loop.panel = (function(_, mozL10n) {
   var ButtonGroup = sharedViews.ButtonGroup;
   var ContactsList = loop.contacts.ContactsList;
   var ContactDetailsForm = loop.contacts.ContactDetailsForm;
 
   var TabView = React.createClass({displayName: 'TabView',
     propTypes: {
       buttonsHidden: React.PropTypes.array,
       // The selectedTab prop is used by the UI showcase.
-      selectedTab: React.PropTypes.string
+      selectedTab: React.PropTypes.string,
+      mozLoop: React.PropTypes.object
     },
 
     getDefaultProps: function() {
       return {
         buttonsHidden: []
       };
     },
 
+    shouldComponentUpdate: function(nextProps, nextState) {
+      var tabChange = this.state.selectedTab !== nextState.selectedTab;
+      if (tabChange) {
+        this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab);
+      }
+      return tabChange;
+    },
+
     getInitialState: function() {
       // XXX Work around props.selectedTab being undefined initially.
       // When we don't need to rely on the pref, this can move back to
       // getDefaultProps (bug 1100258).
       return {
         selectedTab: this.props.selectedTab ||
           (navigator.mozLoop.getLoopPref("rooms.enabled") ?
             "rooms" : "call")
@@ -965,17 +974,17 @@ loop.panel = (function(_, mozL10n) {
         hideButtons.push("contacts");
       }
 
       return (
         React.DOM.div(null, 
           NotificationListView({notifications: this.props.notifications, 
                                 clearOnDocumentHidden: true}), 
           TabView({ref: "tabView", selectedTab: this.props.selectedTab, 
-            buttonsHidden: hideButtons}, 
+            buttonsHidden: hideButtons, mozLoop: this.props.mozLoop}, 
             this._renderRoomsOrCallTab(), 
             Tab({name: "contacts"}, 
               ContactsList({selectTab: this.selectTab, 
                             startForm: this.startForm})
             ), 
             Tab({name: "contacts_add", hidden: true}, 
               ContactDetailsForm({ref: "contacts_add", mode: "add", 
                                   selectTab: this.selectTab})
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -20,25 +20,34 @@ loop.panel = (function(_, mozL10n) {
   var ButtonGroup = sharedViews.ButtonGroup;
   var ContactsList = loop.contacts.ContactsList;
   var ContactDetailsForm = loop.contacts.ContactDetailsForm;
 
   var TabView = React.createClass({
     propTypes: {
       buttonsHidden: React.PropTypes.array,
       // The selectedTab prop is used by the UI showcase.
-      selectedTab: React.PropTypes.string
+      selectedTab: React.PropTypes.string,
+      mozLoop: React.PropTypes.object
     },
 
     getDefaultProps: function() {
       return {
         buttonsHidden: []
       };
     },
 
+    shouldComponentUpdate: function(nextProps, nextState) {
+      var tabChange = this.state.selectedTab !== nextState.selectedTab;
+      if (tabChange) {
+        this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab);
+      }
+      return tabChange;
+    },
+
     getInitialState: function() {
       // XXX Work around props.selectedTab being undefined initially.
       // When we don't need to rely on the pref, this can move back to
       // getDefaultProps (bug 1100258).
       return {
         selectedTab: this.props.selectedTab ||
           (navigator.mozLoop.getLoopPref("rooms.enabled") ?
             "rooms" : "call")
@@ -965,17 +974,17 @@ loop.panel = (function(_, mozL10n) {
         hideButtons.push("contacts");
       }
 
       return (
         <div>
           <NotificationListView notifications={this.props.notifications}
                                 clearOnDocumentHidden={true} />
           <TabView ref="tabView" selectedTab={this.props.selectedTab}
-            buttonsHidden={hideButtons}>
+            buttonsHidden={hideButtons} mozLoop={this.props.mozLoop}>
             {this._renderRoomsOrCallTab()}
             <Tab name="contacts">
               <ContactsList selectTab={this.selectTab}
                             startForm={this.startForm} />
             </Tab>
             <Tab name="contacts_add" hidden={true}>
               <ContactDetailsForm ref="contacts_add" mode="add"
                                   selectTab={this.selectTab} />
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -59,17 +59,18 @@ describe("loop.panel", function() {
         on: sandbox.stub()
       },
       rooms: {
         getAll: function(version, callback) {
           callback(null, []);
         },
         on: sandbox.stub()
       },
-      confirm: sandbox.stub()
+      confirm: sandbox.stub(),
+      notifyUITour: sandbox.stub()
     };
 
     document.mozL10n.initialize(navigator.mozLoop);
     // XXX prevent a race whenever mozL10n hasn't been initialized yet
     setTimeout(done, 0);
   });
 
   afterEach(function() {
--- a/browser/components/loop/test/mochitest/browser_toolbarbutton.js
+++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js
@@ -6,37 +6,84 @@
  */
 
 "use strict";
 
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
 const {LoopRoomsInternal} = Components.utils.import("resource:///modules/loop/LoopRooms.jsm", {});
 Services.prefs.setBoolPref("loop.gettingStarted.seen", true);
 
+const fxASampleToken = {
+  token_type: "bearer",
+  access_token: "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",
+  scope: "profile"
+};
+
+const fxASampleProfile = {
+  email: "test@example.com",
+  uid: "abcd1234"
+};
+
 registerCleanupFunction(function*() {
   MozLoopService.doNotDisturb = false;
   MozLoopServiceInternal.fxAOAuthProfile = null;
   yield MozLoopServiceInternal.clearError("testing");
   Services.prefs.clearUserPref("loop.gettingStarted.seen");
 });
 
+add_task(function* test_LoopUI_getters() {
+  Assert.ok(LoopUI.panel, "LoopUI panel element should be set");
+  Assert.strictEqual(LoopUI.browser, null, "Browser element should not be there yet");
+  Assert.strictEqual(LoopUI.selectedTab, null, "No tab should be selected yet");
+
+  // Load and show the Loop panel for the very first time this session.
+  yield loadLoopPanel();
+  Assert.ok(LoopUI.browser, "Browser element should be there");
+  Assert.strictEqual(LoopUI.selectedTab, "rooms", "Initially the rooms tab should be selected");
+
+  // Hide the panel.
+  yield LoopUI.togglePanel();
+  Assert.strictEqual(LoopUI.selectedTab, "rooms", "Rooms tab should still be selected");
+
+  // Make sure the contacts tab shows up by simulating a login.
+  MozLoopServiceInternal.fxAOAuthTokenData = fxASampleToken;
+  MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
+  yield MozLoopServiceInternal.notifyStatusChanged("login");
+
+  // Programmatically select the contacts tab.
+  yield LoopUI.togglePanel(null, "contacts");
+  Assert.strictEqual(LoopUI.selectedTab, "contacts", "Contacts tab should be selected now");
+
+  // Switch back to the rooms tab.
+  yield LoopUI.openCallPanel(null, "rooms");
+  Assert.strictEqual(LoopUI.selectedTab, "rooms", "Rooms tab should be selected now");
+
+  // Hide the panel.
+  yield LoopUI.togglePanel();
+
+  // Logout to prevent interfering with the tests after this one.
+  MozLoopServiceInternal.fxAOAuthTokenData =
+    MozLoopServiceInternal.fxAOAuthProfile = null;
+  yield MozLoopServiceInternal.notifyStatusChanged();
+});
+
 add_task(function* test_doNotDisturb() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopService.doNotDisturb = true;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
   yield MozLoopService.doNotDisturb = false;
   Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is not in disabled state");
 });
 
 add_task(function* test_doNotDisturb_with_login() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopService.doNotDisturb = true;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
-  MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
-  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
+  MozLoopServiceInternal.fxAOAuthTokenData = fxASampleToken;
+  MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
   yield MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
   yield loadLoopPanel();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state after opening panel");
   LoopUI.panel.hidePopup();
   yield MozLoopService.doNotDisturb = false;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   MozLoopServiceInternal.fxAOAuthTokenData = null;
@@ -51,30 +98,30 @@ add_task(function* test_error() {
   yield MozLoopServiceInternal.clearError("testing");
   Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is not in error state");
 });
 
 add_task(function* test_error_with_login() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopServiceInternal.setError("testing", {});
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
-  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
+  MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
   MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
   yield MozLoopServiceInternal.clearError("testing");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   MozLoopServiceInternal.fxAOAuthProfile = null;
   MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
 
 add_task(function* test_active() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
-  MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
-  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
+  MozLoopServiceInternal.fxAOAuthTokenData = fxASampleToken;
+  MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
   yield MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
   yield loadLoopPanel();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state after opening panel");
   LoopUI.panel.hidePopup();
   MozLoopServiceInternal.fxAOAuthTokenData = null;
   MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -129,33 +129,33 @@ this.UITour = {
     ["loop",        {
       allowAdd: true,
       query: "#loop-button",
       widgetName: "loop-button",
     }],
     ["loop-newRoom", {
       infoPanelPosition: "leftcenter topright",
       query: (aDocument) => {
-        let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop");
-        if (!loopBrowser) {
+        let loopUI = aDocument.defaultView.LoopUI;
+        if (loopUI.selectedTab != "rooms") {
           return null;
         }
         // Use the parentElement full-width container of the button so our arrow
         // doesn't overlap the panel contents much.
-        return loopBrowser.contentDocument.querySelector(".new-room-button").parentElement;
+        return loopUI.browser.contentDocument.querySelector(".new-room-button").parentElement;
       },
     }],
     ["loop-roomList", {
       infoPanelPosition: "leftcenter topright",
       query: (aDocument) => {
-        let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop");
-        if (!loopBrowser) {
+        let loopUI = aDocument.defaultView.LoopUI;
+        if (loopUI.selectedTab != "rooms") {
           return null;
         }
-        return loopBrowser.contentDocument.querySelector(".room-list");
+        return loopUI.browser.contentDocument.querySelector(".room-list");
       },
     }],
     ["loop-selectedRoomButtons", {
       infoPanelOffsetY: -20,
       infoPanelPosition: "start_after",
       query: (aDocument) => {
         let chatbox = aDocument.querySelector("chatbox[src^='about\:loopconversation'][selected]");
 
@@ -168,17 +168,17 @@ this.UITour = {
         // But anchor on the <browser> in the chatbox so the panel doesn't jump to undefined
         // positions when the copy/email buttons disappear e.g. when the feedback form opens or
         // somebody else joins the room.
         return chatbox.content;
       },
     }],
     ["loop-signInUpLink", {
       query: (aDocument) => {
-        let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop");
+        let loopBrowser = aDocument.defaultView.LoopUI.browser;
         if (!loopBrowser) {
           return null;
         }
         return loopBrowser.contentDocument.querySelector(".signin-link");
       },
     }],
     ["privateWindow",  {query: "#privatebrowsing-button"}],
     ["quit",        {query: "#PanelUI-quit"}],
--- a/browser/modules/test/browser_UITour_loop.js
+++ b/browser/modules/test/browser_UITour_loop.js
@@ -6,16 +6,17 @@
 let gTestTab;
 let gContentAPI;
 let gContentWindow;
 let loopButton;
 let loopPanel = document.getElementById("loop-notification-panel");
 
 Components.utils.import("resource:///modules/UITour.jsm");
 const { LoopRooms } = Components.utils.import("resource:///modules/loop/LoopRooms.jsm", {});
+const { MozLoopServiceInternal } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
 
 function test() {
   UITourTest();
 }
 
 let tests = [
   taskify(function* test_menu_show_hide() {
     ise(loopButton.open, false, "Menu should initially be closed");
@@ -89,16 +90,63 @@ let tests = [
           yield Promise.all(hiddenPromises);
           isnot(infoPanel.state, "open", "Info panel should have automatically hid");
           isnot(highlightPanel.state, "open", "Highlight panel should have automatically hid");
           done();
         }), "Info panel should be anchored to the new room button");
       });
     });
   },
+  taskify(function* test_panelTabChangeNotifications() {
+    // First make sure the Loop panel looks like we're logged in to have more than
+    // just one tab to switch to.
+    const fxASampleToken = {
+      token_type: "bearer",
+      access_token: "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",
+      scope: "profile"
+    };
+    const fxASampleProfile = {
+      email: "test@example.com",
+      uid: "abcd1234"
+    };
+    MozLoopServiceInternal.fxAOAuthTokenData = fxASampleToken;
+    MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
+    yield MozLoopServiceInternal.notifyStatusChanged("login");
+
+    // Show the Loop menu.
+    yield showMenuPromise("loop");
+
+    // Listen for and test the notifications that will arrive from now on.
+    let tabChangePromise = new Promise(resolve => {
+      gContentAPI.observe((event, params) => {
+        is(event, "Loop:PanelTabChanged", "Check Loop:PanelTabChanged notification");
+        is(params, "contacts", "Check the tab name param");
+
+        gContentAPI.observe((event, params) => {
+          is(event, "Loop:PanelTabChanged", "Check Loop:PanelTabChanged notification");
+          is(params, "rooms", "Check the tab name param");
+
+          gContentAPI.observe((event, params) => {
+            ok(false, "No more notifications should have arrived");
+          });
+          resolve();
+        });
+      });
+    });
+
+    // Switch to the contacts tab.
+    yield window.LoopUI.openCallPanel(null, "contacts");
+
+    // Logout. The panel tab will switch back to 'rooms'.
+    MozLoopServiceInternal.fxAOAuthTokenData =
+      MozLoopServiceInternal.fxAOAuthProfile = null;
+    yield MozLoopServiceInternal.notifyStatusChanged();
+
+    yield tabChangePromise;
+  }),
   function test_notifyLoopChatWindowOpenedClosed(done) {
     gContentAPI.observe((event, params) => {
       is(event, "Loop:ChatWindowOpened", "Check Loop:ChatWindowOpened notification");
       gContentAPI.observe((event, params) => {
         is(event, "Loop:ChatWindowShown", "Check Loop:ChatWindowShown notification");
         gContentAPI.observe((event, params) => {
           is(event, "Loop:ChatWindowClosed", "Check Loop:ChatWindowClosed notification");
           gContentAPI.observe((event, params) => {