Bug 1110602 - Don't remove tour tabs from originTabs when switching tabs so they can continue to get notifications. r=Unfocused, a=sledru
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Wed, 21 Jan 2015 13:21:41 -0800
changeset 243685 ba8e83fca8f2
parent 243684 32f28bbf1fb4
child 243686 fe2ce463edba
push id4436
push usermozilla@noorenberghe.ca
push date2015-02-05 02:33 +0000
treeherdermozilla-beta@5690b3943d75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersUnfocused, sledru
bugs1110602
milestone36.0
Bug 1110602 - Don't remove tour tabs from originTabs when switching tabs so they can continue to get notifications. r=Unfocused, a=sledru
browser/base/content/browser-fxaccounts.js
browser/base/content/browser-syncui.js
browser/components/customizableui/test/browser_967000_button_sync.js
browser/modules/UITour.jsm
browser/modules/test/browser_UITour_detach_tab.js
browser/modules/test/browser_UITour_observe.js
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -220,26 +220,26 @@ let gFxAccounts = {
   },
 
   openPreferences: function () {
     openPreferences("paneSync");
   },
 
   openAccountsPage: function () {
     let entryPoint = "menupanel";
-    if (UITour.originTabs.get(window) && UITour.originTabs.get(window).has(gBrowser.selectedTab)) {
+    if (UITour.tourBrowsersByWindow.get(window) && UITour.tourBrowsersByWindow.get(window).has(gBrowser.selectedBrowser)) {
       entryPoint = "uitour";
     }
     switchToTabHavingURI("about:accounts?entrypoint=" + entryPoint, true, {
       replaceQueryString: true
     });
   },
 
   openSignInAgainPage: function () {
     let entryPoint = "menupanel";
-    if (UITour.originTabs.get(window) && UITour.originTabs.get(window).has(gBrowser.selectedTab)) {
+    if (UITour.tourBrowsersByWindow.get(window) && UITour.tourBrowsersByWindow.get(window).has(gBrowser.selectedBrowser)) {
       entryPoint = "uitour";
     }
     switchToTabHavingURI("about:accounts?action=reauth&entrypoint=" + entryPoint, true, {
       replaceQueryString: true
     });
   }
 };
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -322,17 +322,18 @@ let gSyncUI = {
                                 .getService(Components.interfaces.nsISupports)
                                 .wrappedJSObject;
     if (xps.fxAccountsEnabled) {
       fxAccounts.getSignedInUser().then(userData => {
         if (userData) {
           this.openPrefs();
         } else {
           // If the user is also in an uitour, set the entrypoint to `uitour`
-          if (UITour.originTabs.get(window) && UITour.originTabs.get(window).has(gBrowser.selectedTab)) {
+          if (UITour.tourBrowsersByWindow.get(window) &&
+              UITour.tourBrowsersByWindow.get(window).has(gBrowser.selectedBrowser)) {
             entryPoint = "uitour";
           }
           switchToTabHavingURI("about:accounts?entrypoint=" + entryPoint, true, {
             replaceQueryString: true
           });
         }
       });
     } else {
--- a/browser/components/customizableui/test/browser_967000_button_sync.js
+++ b/browser/components/customizableui/test/browser_967000_button_sync.js
@@ -17,18 +17,18 @@ function openAboutAccountsFromMenuPanel(
 
   // add the Sync button to the panel
   CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
 
   // check the button's functionality
   yield PanelUI.show();
 
   if (entryPoint == "uitour") {
-    UITour.originTabs.set(window, new Set());
-    UITour.originTabs.get(window).add(gBrowser.selectedTab);
+    UITour.tourBrowsersByWindow.set(window, new Set());
+    UITour.tourBrowsersByWindow.get(window).add(gBrowser.selectedBrowser);
   }
 
   let syncButton = document.getElementById("sync-button");
   ok(syncButton, "The Sync button was added to the Panel Menu");
 
   let deferred = Promise.defer();
   let handler = (e) => {
     if (e.originalTarget != gBrowser.selectedTab.linkedBrowser.contentDocument ||
@@ -60,16 +60,16 @@ function asyncCleanup() {
   Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
   // reset the panel UI to the default state
   yield resetCustomization();
   ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
 
   // restore the tabs
   gBrowser.addTab(initialLocation);
   gBrowser.removeTab(newTab);
-  UITour.originTabs.delete(window);
+  UITour.tourBrowsersByWindow.delete(window);
 }
 
 add_task(() => openAboutAccountsFromMenuPanel("syncbutton"));
 add_task(asyncCleanup);
 // Test that uitour is in progress, the entrypoint is `uitour` and not `menupanel`
 add_task(() => openAboutAccountsFromMenuPanel("uitour"));
 add_task(asyncCleanup);
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -63,20 +63,19 @@ XPCOMUtils.defineLazyGetter(this, "log",
     prefix: "UITour",
   };
   return new ConsoleAPI(consoleOptions);
 });
 
 this.UITour = {
   url: null,
   seenPageIDs: null,
-  pageIDSourceTabs: new WeakMap(),
-  pageIDSourceWindows: new WeakMap(),
-  /* Map from browser windows to a set of tabs in which a tour is open */
-  originTabs: new WeakMap(),
+  pageIDSourceBrowsers: new WeakMap(),
+  /* Map from browser chrome windows to a Set of <browser>s in which a tour is open (both visible and hidden) */
+  tourBrowsersByWindow: new WeakMap(),
   urlbarCapture: new WeakMap(),
   appMenuOpenForAnnotation: new Set(),
   availableTargetsCache: new WeakMap(),
 
   _detachingTab: false,
   _annotationPanelMutationObservers: new WeakMap(),
   _queuedEvents: [],
   _pendingDoc: null,
@@ -396,22 +395,17 @@ this.UITour = {
         // pageID, as it could make parsing the telemetry bucket name difficult.
         if (typeof data.pageID != "string" ||
             data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) {
           log.warn("registerPageID: Invalid page ID specified");
           break;
         }
 
         this.addSeenPageID(data.pageID);
-
-        // Store tabs and windows separately so we don't need to loop over all
-        // tabs when a window is closed.
-        this.pageIDSourceTabs.set(tab, data.pageID);
-        this.pageIDSourceWindows.set(window, data.pageID);
-
+        this.pageIDSourceBrowsers.set(browser, data.pageID);
         this.setTelemetryBucket(data.pageID);
 
         break;
       }
 
       case "showHighlight": {
         let targetPromise = this.getTarget(window, data.target);
         targetPromise.then(target => {
@@ -649,104 +643,91 @@ this.UITour = {
 
       case "ping": {
         if (typeof data.callbackID == "string")
           this.sendPageCallback(messageManager, data.callbackID);
         break;
       }
     }
 
-    if (!window.gMultiProcessBrowser) { // Non-e10s. See bug 1089000.
-      if (!this.originTabs.has(window)) {
-        this.originTabs.set(window, new Set());
-      }
+    if (!this.tourBrowsersByWindow.has(window)) {
+      this.tourBrowsersByWindow.set(window, new Set());
+    }
+    this.tourBrowsersByWindow.get(window).add(browser);
 
-      this.originTabs.get(window).add(tab);
+    // We don't have a tab if it's being detached or we're in a <browser> without a tab.
+    if (tab) {
       tab.addEventListener("TabClose", this);
-      tab.addEventListener("TabBecomingWindow", this);
-      window.addEventListener("SSWindowClosing", this);
+      if (!window.gMultiProcessBrowser) {
+        tab.addEventListener("TabBecomingWindow", this);
+      }
     }
 
+    window.addEventListener("SSWindowClosing", this);
+
     return true;
   },
 
   handleEvent: function(aEvent) {
+    log.debug("handleEvent: type =", aEvent.type, "event =", aEvent);
     switch (aEvent.type) {
       case "pagehide": {
         let window = this.getChromeWindow(aEvent.target);
-        this.teardownTour(window);
+        this.teardownTourForWindow(window);
         break;
       }
 
       case "TabBecomingWindow":
         this._detachingTab = true;
         // Fall through
       case "TabClose": {
         let tab = aEvent.target;
-        if (this.pageIDSourceTabs.has(tab)) {
-          let pageID = this.pageIDSourceTabs.get(tab);
-
-          // Delete this from the window cache, so if the window is closed we
-          // don't expire this page ID twice.
-          let window = tab.ownerDocument.defaultView;
-          if (this.pageIDSourceWindows.get(window) == pageID)
-            this.pageIDSourceWindows.delete(window);
-
-          this.setExpiringTelemetryBucket(pageID, "closed");
-        }
-
         let window = tab.ownerDocument.defaultView;
-        this.teardownTour(window);
+        this.teardownTourForBrowser(window, tab.linkedBrowser, true);
         break;
       }
 
       case "TabSelect": {
+        let window = aEvent.target.ownerDocument.defaultView;
+
         if (aEvent.detail && aEvent.detail.previousTab) {
           let previousTab = aEvent.detail.previousTab;
-
-          if (this.pageIDSourceTabs.has(previousTab)) {
-            let pageID = this.pageIDSourceTabs.get(previousTab);
-            this.setExpiringTelemetryBucket(pageID, "inactive");
+          let openTourWindows = this.tourBrowsersByWindow.get(window);
+          if (openTourWindows.has(previousTab.linkedBrowser)) {
+            this.teardownTourForBrowser(window, previousTab.linkedBrowser, false);
           }
         }
 
-        let window = aEvent.target.ownerDocument.defaultView;
         let pendingDoc;
         if (this._detachingTab && this._pendingDoc && (pendingDoc = this._pendingDoc.get())) {
           let selectedTab = window.gBrowser.selectedTab;
           if (selectedTab.linkedBrowser.contentDocument == pendingDoc) {
-            if (!this.originTabs.get(window)) {
-              this.originTabs.set(window, new Set());
+            if (!this.tourBrowsersByWindow.get(window)) {
+              this.tourBrowsersByWindow.set(window, new Set());
             }
-            this.originTabs.get(window).add(selectedTab);
+            this.tourBrowsersByWindow.get(window).add(selectedTab.linkedBrowser);
             this.pendingDoc = null;
             this._detachingTab = false;
             while (this._queuedEvents.length) {
               try {
                 this.onPageEvent(this._queuedEvents.shift());
               } catch (ex) {
                 log.error(ex);
               }
             }
             break;
           }
         }
 
-        this.teardownTour(window);
         break;
       }
 
       case "SSWindowClosing": {
         let window = aEvent.target;
-        if (this.pageIDSourceWindows.has(window)) {
-          let pageID = this.pageIDSourceWindows.get(window);
-          this.setExpiringTelemetryBucket(pageID, "closed");
-        }
-
-        this.teardownTour(window, true);
+        this.teardownTourForWindow(window);
         break;
       }
 
       case "input": {
         if (aEvent.target.id == "urlbar") {
           let window = aEvent.target.ownerDocument.defaultView;
           this.handleUrlbarInput(window);
         }
@@ -771,48 +752,84 @@ this.UITour = {
   // This is registered with UITelemetry by BrowserUITelemetry, so that UITour
   // can remain lazy-loaded on-demand.
   getTelemetry: function() {
     return {
       seenPageIDs: [...this.seenPageIDs.keys()],
     };
   },
 
-  teardownTour: function(aWindow, aWindowClosing = false) {
-    log.debug("teardownTour: aWindowClosing = " + aWindowClosing);
-    aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
-    aWindow.removeEventListener("SSWindowClosing", this);
+  /**
+   * Tear down a tour from a tab e.g. upon switching/closing tabs.
+   */
+  teardownTourForBrowser: function(aWindow, aBrowser, aTourPageClosing = false) {
+    log.debug("teardownTourForBrowser: aBrowser = ", aBrowser, aTourPageClosing);
 
-    let originTabs = this.originTabs.get(aWindow);
-    if (originTabs) {
-      for (let tab of originTabs) {
+    if (this.pageIDSourceBrowsers.has(aBrowser)) {
+      let pageID = this.pageIDSourceBrowsers.get(aBrowser);
+      this.setExpiringTelemetryBucket(pageID, aTourPageClosing ? "closed" : "inactive");
+    }
+
+    let openTourBrowsers = this.tourBrowsersByWindow.get(aWindow);
+    if (aTourPageClosing) {
+      let tab = aWindow.gBrowser.getTabForBrowser(aBrowser);
+      if (tab) { // Handle standalone <browser>
         tab.removeEventListener("TabClose", this);
         tab.removeEventListener("TabBecomingWindow", this);
+        if (openTourBrowsers) {
+          openTourBrowsers.delete(aBrowser);
+        }
       }
     }
-    this.originTabs.delete(aWindow);
 
-    if (!aWindowClosing) {
-      this.hideHighlight(aWindow);
-      this.hideInfo(aWindow);
-      // Ensure the menu panel is hidden before calling recreatePopup so popup events occur.
-      this.hideMenu(aWindow, "appMenu");
-      this.hideMenu(aWindow, "loop");
-    }
+    this.hideHighlight(aWindow);
+    this.hideInfo(aWindow);
+    // Ensure the menu panel is hidden before calling recreatePopup so popup events occur.
+    this.hideMenu(aWindow, "appMenu");
+    this.hideMenu(aWindow, "loop");
 
-    // Clean up panel listeners after we may have called hideMenu above.
+    // Clean up panel listeners after calling hideMenu above.
     aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hideAppMenuAnnotations);
     aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hideAppMenuAnnotations);
     aWindow.PanelUI.panel.removeEventListener("popuphidden", this.onPanelHidden);
     let loopPanel = aWindow.document.getElementById("loop-notification-panel");
     loopPanel.removeEventListener("popuphidden", this.onPanelHidden);
     loopPanel.removeEventListener("popuphiding", this.hideLoopPanelAnnotations);
 
     this.endUrlbarCapture(aWindow);
     this.resetTheme();
+
+    // If there are no more tour tabs left in the window, teardown the tour for the whole window.
+    if (!openTourBrowsers || openTourBrowsers.size == 0) {
+      this.teardownTourForWindow(aWindow);
+    }
+  },
+
+  /**
+   * Tear down all tours for a ChromeWindow.
+   */
+  teardownTourForWindow: function(aWindow) {
+    log.debug("teardownTourForWindow");
+    aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
+    aWindow.removeEventListener("SSWindowClosing", this);
+
+    let openTourBrowsers = this.tourBrowsersByWindow.get(aWindow);
+    if (openTourBrowsers) {
+      for (let browser of openTourBrowsers) {
+        if (this.pageIDSourceBrowsers.has(browser)) {
+          let pageID = this.pageIDSourceBrowsers.get(browser);
+          this.setExpiringTelemetryBucket(pageID, "closed");
+        }
+
+        let tab = aWindow.gBrowser.getTabForBrowser(browser);
+        tab.removeEventListener("TabClose", this);
+      }
+    }
+
+    this.tourBrowsersByWindow.delete(aWindow);
   },
 
   getChromeWindow: function(aContentDocument) {
     return aContentDocument.defaultView
                            .window
                            .QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIWebNavigation)
                            .QueryInterface(Ci.nsIDocShellTreeItem)
@@ -1243,16 +1260,17 @@ this.UITour = {
     this._setAppMenuStateForAnnotation(aWindow, "info", false);
 
     let tooltipButtons = document.getElementById("UITourTooltipButtons");
     while (tooltipButtons.firstChild)
       tooltipButtons.firstChild.remove();
   },
 
   showMenu: function(aWindow, aMenuName, aOpenCallback = null) {
+    log.debug("showMenu:", aMenuName);
     function openMenuButton(aMenuBtn) {
       if (!aMenuBtn || !aMenuBtn.boxObject || aMenuBtn.open) {
         if (aOpenCallback)
           aOpenCallback();
         return;
       }
       if (aOpenCallback)
         aMenuBtn.addEventListener("popupshown", onPopupShown);
@@ -1307,16 +1325,17 @@ this.UITour = {
     } else if (aMenuName == "searchEngines") {
       this.getTarget(aWindow, "searchProvider").then(target => {
         openMenuButton(target.node);
       }).catch(log.error);
     }
   },
 
   hideMenu: function(aWindow, aMenuName) {
+    log.debug("hideMenu:", aMenuName);
     function closeMenuButton(aMenuBtn) {
       if (aMenuBtn && aMenuBtn.boxObject)
         aMenuBtn.boxObject.openMenu(false);
     }
 
     if (aMenuName == "appMenu") {
       aWindow.PanelUI.hide();
     } else if (aMenuName == "bookmarks") {
@@ -1636,22 +1655,26 @@ this.UITour = {
 
   notify(eventName, params) {
     let winEnum = Services.wm.getEnumerator("navigator:browser");
     while (winEnum.hasMoreElements()) {
       let window = winEnum.getNext();
       if (window.closed)
         continue;
 
-      let originTabs = this.originTabs.get(window);
-      if (!originTabs)
+      let openTourBrowsers = this.tourBrowsersByWindow.get(window);
+      if (!openTourBrowsers)
         continue;
 
-      for (let tab of originTabs) {
-        let messageManager = tab.linkedBrowser.messageManager;
+      for (let browser of openTourBrowsers) {
+        let messageManager = browser.messageManager;
+        if (!messageManager) {
+          log.error("notify: Trying to notify a browser without a messageManager", browser);
+          continue;
+        }
         let detail = {
           event: eventName,
           params: params,
         };
         messageManager.sendAsyncMessage("UITour:SendPageNotification", detail);
       }
     }
   },
--- a/browser/modules/test/browser_UITour_detach_tab.js
+++ b/browser/modules/test/browser_UITour_detach_tab.js
@@ -63,18 +63,18 @@ let tests = [
     gContentWindow = yield browserStartupDeferred.promise;
 
     // This highlight should be shown thanks to the visibilitychange listener.
     let newWindowHighlight = gContentWindow.document.getElementById("UITourHighlight");
     yield elementVisiblePromise(newWindowHighlight);
 
     let selectedTab = gContentWindow.gBrowser.selectedTab;
     is(selectedTab.linkedBrowser && selectedTab.linkedBrowser.contentDocument, gContentDoc, "Document should be selected in new window");
-    ok(UITour.originTabs && UITour.originTabs.has(gContentWindow), "Window should be known");
-    ok(UITour.originTabs.get(gContentWindow).has(selectedTab), "Tab should be known");
+    ok(UITour.tourBrowsersByWindow && UITour.tourBrowsersByWindow.has(gContentWindow), "Window should be known");
+    ok(UITour.tourBrowsersByWindow.get(gContentWindow).has(selectedTab.linkedBrowser), "Selected browser should be known");
 
     let shownPromise = promisePanelShown(gContentWindow);
     gContentAPI.showMenu("appMenu");
     yield shownPromise;
 
     isnot(gContentWindow.PanelUI.panel.state, "closed", "Panel should be open");
     ok(gContentWindow.PanelUI.contents.children.length > 0, "Panel contents should have children");
     gContentAPI.hideHighlight();
--- a/browser/modules/test/browser_UITour_observe.js
+++ b/browser/modules/test/browser_UITour_observe.js
@@ -43,9 +43,45 @@ let tests = [
       gContentAPI.observe(null);
       done();
     }
 
     gContentAPI.observe(listener, () => {
       UITour.notify("test-event-3", {key: "something"});
     });
   },
-];
\ No newline at end of file
+  function test_background_tab(done) {
+    function listener(event, params) {
+      is(event, "test-event-background-1", "Correct event name");
+      is(params, null, "No param object");
+      gContentAPI.observe(null);
+      gBrowser.removeCurrentTab();
+      done();
+    }
+
+    gContentAPI.observe(listener, () => {
+      gBrowser.selectedTab = gBrowser.addTab("about:blank");
+      isnot(gBrowser.selectedTab, gTestTab, "Make sure the selected tab changed");
+
+      UITour.notify("test-event-background-1");
+    });
+  },
+  // Make sure the tab isn't torn down when switching back to the tour one.
+  function test_background_then_foreground_tab(done) {
+    let blankTab = null;
+    function listener(event, params) {
+      is(event, "test-event-4", "Correct event name");
+      is(params, null, "No param object");
+      gContentAPI.observe(null);
+      gBrowser.removeTab(blankTab);
+      done();
+    }
+
+    gContentAPI.observe(listener, () => {
+      blankTab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+      isnot(gBrowser.selectedTab, gTestTab, "Make sure the selected tab changed");
+      gBrowser.selectedTab = gTestTab;
+      is(gBrowser.selectedTab, gTestTab, "Switch back to the test tab");
+
+      UITour.notify("test-event-4");
+    });
+  },
+];