Bug 989194 - Show captive portal notification bar when detected. r=MattN draft
authorNihanth Subramanya <nhnt11@gmail.com>
Mon, 27 Jun 2016 19:12:12 -0700
changeset 430781 b52a42f1ecd3202165da13afedc38a4835ebff42
parent 430780 71536044069576fc877bf7ef0ba13a4ada793706
child 430797 a9d7ae2782d28fe15b3a6f32eca3ca44071d3210
child 430799 599f77501ae6cecdbeb5aa9130a2911feec15115
push id33886
push usernhnt11@gmail.com
push dateFri, 28 Oct 2016 04:49:07 +0000
reviewersMattN
bugs989194
milestone52.0a1
Bug 989194 - Show captive portal notification bar when detected. r=MattN MozReview-Commit-ID: KFvtTCBpMeS
browser/locales/en-US/chrome/browser/browser.properties
browser/modules/CaptivePortalWatcher.jsm
browser/modules/test/browser_CaptivePortalWatcher.js
testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -732,16 +732,25 @@ decoder.noCodecs.message = To play video, you may need to install Microsoft’s Media Feature Pack.
 decoder.noCodecsVista.message = To play video, you may need to install Microsoft’s Platform Update Supplement for Windows Vista.
 decoder.noCodecsXP.message = To play video, you may need to enable Adobe’s Primetime Content Decryption Module.
 decoder.noCodecsLinux.message = To play video, you may need to install the required video codecs.
 decoder.noHWAcceleration.message = To improve video quality, you may need to install Microsoft’s Media Feature Pack.
 decoder.noHWAccelerationVista.message = To improve video quality, you may need to install Microsoft’s Platform Update Supplement for Windows Vista.
 decoder.noPulseAudio.message = To play audio, you may need to install the required PulseAudio software.
 decoder.unsupportedLibavcodec.message = libavcodec may be vulnerable or is not supported, and should be updated to play video.
 
+# LOCALIZATION NOTE (captivePortal.infoMessage):
+# This string is shown in a notification bar when we detect a captive portal is blocking network access
+# and requires the user to log in before browsing. %1$S is replaced with brandShortName.
+captivePortal.infoMessage=This network may require you to login to use the internet. %1$S has opened the login page for you.
+# LOCALIZATION NOTE (captivePortal.showLoginPage):
+# The label for a button shown in the info bar in all tabs except the login page tab.
+# The button shows the portal login page tab when clicked.
+captivePortal.showLoginPage=Show Login Page
+
 permissions.remove.tooltip = Clear this permission and ask again
 
 # LOCALIZATION NOTE (aboutDialog.architecture.*):
 # The sixtyFourBit and thirtyTwoBit strings describe the architecture of the
 # current Firefox build: 32-bit or 64-bit. These strings are used in parentheses
 # between the Firefox version and the "What's new" link in the About dialog,
 # e.g.: "48.0.2 (32-bit) <What's new>" or "51.0a1 (2016-09-05) (64-bit)".
 aboutDialog.architecture.sixtyFourBit = 64-bit
--- a/browser/modules/CaptivePortalWatcher.jsm
+++ b/browser/modules/CaptivePortalWatcher.jsm
@@ -7,32 +7,38 @@
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 /**
  * This constant is chosen to be large enough for a portal recheck to complete,
  * and small enough that the delay in opening a tab isn't too noticeable.
  * Please see comments for _delayedAddCaptivePortalTab for more details.
  */
 const PORTAL_RECHECK_DELAY_MS = 150;
 
+// This is the value used to identify the captive portal notification.
+const PORTAL_NOTIFICATION_VALUE = "captive-portal-detected";
+
 this.EXPORTED_SYMBOLS = [ "CaptivePortalWatcher" ];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/RecentWindow.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "cps",
                                    "@mozilla.org/network/captive-portal-service;1",
                                    "nsICaptivePortalService");
 
 this.CaptivePortalWatcher = {
   // This holds a weak reference to the captive portal tab so that we
   // don't leak it if the user closes it.
   _captivePortalTab: null,
 
+  // This holds a weak reference to the captive portal notification.
+  _captivePortalNotification: null,
+
   _initialized: false,
 
   /**
    * If a portal is detected when we don't have focus, we first wait for focus
    * and then add the tab after a small delay. This is set to true while we wait
    * so that in the unlikely event that we receive another notification while
    * waiting, we can avoid adding a second tab.
    */
@@ -45,17 +51,17 @@ this.CaptivePortalWatcher = {
   init() {
     Services.obs.addObserver(this, "captive-portal-login", false);
     Services.obs.addObserver(this, "captive-portal-login-abort", false);
     Services.obs.addObserver(this, "captive-portal-login-success", false);
     this._initialized = true;
 
     if (cps.state == cps.LOCKED_PORTAL) {
       // A captive portal has already been detected.
-      this._addCaptivePortalTab();
+      this._captivePortalDetected();
       return;
     }
 
     cps.recheckCaptivePortal();
   },
 
   uninit() {
     if (!this._initialized) {
@@ -64,48 +70,63 @@ this.CaptivePortalWatcher = {
     Services.obs.removeObserver(this, "captive-portal-login");
     Services.obs.removeObserver(this, "captive-portal-login-abort");
     Services.obs.removeObserver(this, "captive-portal-login-success");
   },
 
   observe(subject, topic, data) {
     switch (topic) {
       case "captive-portal-login":
-        this._addCaptivePortalTab();
+        this._captivePortalDetected();
         break;
       case "captive-portal-login-abort":
       case "captive-portal-login-success":
         this._captivePortalGone();
         break;
       case "xul-window-visible":
         this._delayedAddCaptivePortalTab();
         break;
     }
   },
 
-  _addCaptivePortalTab() {
+  _captivePortalDetected() {
     if (this._waitingToAddTab) {
       return;
     }
 
     let win = RecentWindow.getMostRecentBrowserWindow();
     // If there's no browser window or none have focus, open and show the
     // tab when we regain focus. This is so that if a different application was
     // focused, when the user (re-)focuses a browser window, we open the tab
     // immediately in that window so they can login before continuing to browse.
     if (!win || !win.document.hasFocus()) {
       this._waitingToAddTab = true;
       Services.obs.addObserver(this, "xul-window-visible", false);
       return;
     }
 
-    // The browser is in use - add the tab without selecting it.
-    let tab = win.gBrowser.addTab(this.canonicalURL);
+    // The browser is in use - show a notification and add the tab without
+    // selecting it, unless the caller specifically requested selection.
+    this._ensureCaptivePortalTab(win);
+    this._showNotification(win);
+  },
+
+  _ensureCaptivePortalTab(win) {
+    let tab;
+    if (this._captivePortalTab) {
+      tab = this._captivePortalTab.get();
+    }
+
+    // If the tab is gone or going, we need to open a new one.
+    if (!tab || tab.closing || !tab.parentNode) {
+      tab = win.gBrowser.addTab(this.canonicalURL);
+    }
+
     this._captivePortalTab = Cu.getWeakReference(tab);
-    return;
+    return tab;
   },
 
   /**
    * Called after we regain focus if we detect a portal while a browser window
    * doesn't have focus. Triggers a portal recheck to reaffirm state, and adds
    * the tab if needed after a short delay to allow the recheck to complete.
    */
   _delayedAddCaptivePortalTab() {
@@ -130,35 +151,37 @@ this.CaptivePortalWatcher = {
     // - If it is, the delay is chosen to not be extremely noticeable.
     setTimeout(() => {
       this._waitingToAddTab = false;
       if (cps.state != cps.LOCKED_PORTAL) {
         // We're free of the portal!
         return;
       }
 
-      let tab = win.gBrowser.addTab(this.canonicalURL);
+      this._showNotification(win);
+      let tab = this._ensureCaptivePortalTab(win);
+
       // Focus the tab only if the recheck has completed, i.e. we're sure
       // that the portal is still locked. This way, if the recheck completes
       // after we add the tab and we're free of the portal, the tab contents
       // won't flicker.
       if (cps.lastChecked != lastChecked) {
         win.gBrowser.selectedTab = tab;
       }
-
-      this._captivePortalTab = Cu.getWeakReference(tab);
     }, PORTAL_RECHECK_DELAY_MS);
   },
 
   _captivePortalGone() {
     if (this._waitingToAddTab) {
       Services.obs.removeObserver(this, "xul-window-visible");
       this._waitingToAddTab = false;
     }
 
+    this._removeNotification();
+
     if (!this._captivePortalTab) {
       return;
     }
 
     let tab = this._captivePortalTab.get();
     // In all the cases below, we want to stop treating the tab as a
     // captive portal tab.
     this._captivePortalTab = null;
@@ -176,9 +199,86 @@ this.CaptivePortalWatcher = {
     if (tab.linkedBrowser.currentURI.spec != this.canonicalURL &&
         tabbrowser.selectedTab == tab) {
       return;
     }
 
     // Remove the tab.
     tabbrowser.removeTab(tab);
   },
+
+  get _productName() {
+    delete this._productName;
+    return this._productName =
+      Services.strings.createBundle("chrome://branding/locale/brand.properties")
+                      .GetStringFromName("brandShortName");
+  },
+
+  get _browserBundle() {
+    delete this._browserBundle;
+    return this._browserBundle =
+      Services.strings.createBundle("chrome://browser/locale/browser.properties");
+  },
+
+  handleEvent(aEvent) {
+    if (aEvent.type != "TabSelect" || !this._captivePortalTab || !this._captivePortalNotification) {
+      return;
+    }
+
+    let tab = this._captivePortalTab.get();
+    let n = this._captivePortalNotification.get();
+    if (!tab || !n) {
+      return;
+    }
+
+    let doc = tab.ownerDocument;
+    let button = n.querySelector("button.notification-button");
+    if (doc.defaultView.gBrowser.selectedTab == tab) {
+      button.style.visibility = "hidden";
+    } else {
+      button.style.visibility = "visible";
+    }
+  },
+
+  _showNotification(win) {
+    let buttons = [
+      {
+        label: this._browserBundle.GetStringFromName("captivePortal.showLoginPage"),
+        callback: () => {
+          win.gBrowser.selectedTab = this._ensureCaptivePortalTab(win);
+
+          // Returning true prevents the notification from closing.
+          return true;
+        },
+        isDefault: true,
+      },
+    ];
+
+    let message = this._browserBundle.formatStringFromName("captivePortal.infoMessage",
+                                                           [this._productName], 1);
+
+    let closeHandler = (aEventName) => {
+      if (aEventName != "removed") {
+        return;
+      }
+      win.gBrowser.tabContainer.removeEventListener("TabSelect", this);
+    };
+
+    let nb = win.document.getElementById("high-priority-global-notificationbox");
+    let n = nb.appendNotification(message, PORTAL_NOTIFICATION_VALUE, "",
+                                  nb.PRIORITY_INFO_MEDIUM, buttons, closeHandler);
+
+    this._captivePortalNotification = Cu.getWeakReference(n);
+
+    win.gBrowser.tabContainer.addEventListener("TabSelect", this);
+  },
+
+  _removeNotification() {
+    if (!this._captivePortalNotification)
+      return;
+    let n = this._captivePortalNotification.get();
+    this._captivePortalNotification = null;
+    if (!n || !n.parentNode) {
+      return;
+    }
+    n.close();
+  },
 };
--- a/browser/modules/test/browser_CaptivePortalWatcher.js
+++ b/browser/modules/test/browser_CaptivePortalWatcher.js
@@ -1,15 +1,16 @@
 "use strict";
 
 Components.utils.import("resource:///modules/RecentWindow.jsm");
 
 const CANONICAL_CONTENT = "success";
 const CANONICAL_URL = "data:text/plain;charset=utf-8," + CANONICAL_CONTENT;
 const CANONICAL_URL_REDIRECTED = "data:text/plain;charset=utf-8,redirected";
+const PORTAL_NOTIFICATION_VALUE = "captive-portal-detected";
 
 add_task(function* setup() {
   yield SpecialPowers.pushPrefEnv({
     set: [["captivedetect.canonicalURL", CANONICAL_URL],
           ["captivedetect.canonicalContent", CANONICAL_CONTENT]],
   });
 });
 
@@ -20,170 +21,291 @@ add_task(function* setup() {
  */
 function* portalDetectedNoBrowserWindow() {
   let getMostRecentBrowserWindow = RecentWindow.getMostRecentBrowserWindow;
   RecentWindow.getMostRecentBrowserWindow = () => {};
   Services.obs.notifyObservers(null, "captive-portal-login", null);
   RecentWindow.getMostRecentBrowserWindow = getMostRecentBrowserWindow;
 }
 
-function* openWindowAndWaitForPortalTab() {
+function* openWindowAndWaitForPortalTabAndNotification() {
   let win = yield BrowserTestUtils.openNewBrowserWindow();
-  let tab = yield BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL);
+  let [notification, tab] = yield Promise.all([
+    BrowserTestUtils.waitForGlobalNotificationBar(win, PORTAL_NOTIFICATION_VALUE),
+    BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL)
+  ]);
   is(win.gBrowser.selectedTab, tab,
     "The captive portal tab should be open and selected in the new window.");
+  testShowLoginPageButtonVisibility(notification, "hidden");
   return win;
 }
 
 function freePortal(aSuccess) {
   Services.obs.notifyObservers(null,
     "captive-portal-login-" + (aSuccess ? "success" : "abort"), null);
 }
 
+function ensurePortalTab(win) {
+  // For the tests that call this function, it's enough to ensure there
+  // are two tabs in the window - the default tab and the portal tab.
+  is(win.gBrowser.tabs.length, 2,
+    "There should be a captive portal tab in the window.");
+}
+
+function ensurePortalNotification(win) {
+  let notificationBox =
+    win.document.getElementById("high-priority-global-notificationbox");
+  let notification = notificationBox.getNotificationWithValue(PORTAL_NOTIFICATION_VALUE)
+  isnot(notification, null,
+    "There should be a captive portal notification in the window.");
+  return notification;
+}
+
+// Helper to test whether the "Show Login Page" is visible in the captive portal
+// notification (it should be hidden when the portal tab is selected).
+function testShowLoginPageButtonVisibility(notification, visibility) {
+  let showLoginPageButton = notification.querySelector("button.notification-button");
+  // If the visibility property was never changed from default, it will be
+  // an empty string, so we pretend it's "visible" (effectively the same).
+  is(showLoginPageButton.style.visibility || "visible", visibility,
+    "The \"Show Login Page\" button should be " + visibility + ".");
+}
+
+function ensureNoPortalTab(win) {
+  is(win.gBrowser.tabs.length, 1,
+    "There should be no captive portal tab in the window.");
+}
+
+function ensureNoPortalNotification(win) {
+  let notificationBox =
+    win.document.getElementById("high-priority-global-notificationbox");
+  is(notificationBox.getNotificationWithValue(PORTAL_NOTIFICATION_VALUE), null,
+    "There should be no captive portal notification in the window.");
+}
+
 // Each of the test cases below is run twice: once for login-success and once
 // for login-abort (aSuccess set to true and false respectively).
 let testCasesForBothSuccessAndAbort = [
   /**
    * A portal is detected when there's no browser window,
    * then a browser window is opened, then the portal is freed.
    * The portal tab should be added and focused when the window is
    * opened, and closed automatically when the success event is fired.
    */
   function* test_detectedWithNoBrowserWindow_Open(aSuccess) {
     yield portalDetectedNoBrowserWindow();
-    let win = yield openWindowAndWaitForPortalTab();
+    let win = yield openWindowAndWaitForPortalTabAndNotification();
     freePortal(aSuccess);
-    is(win.gBrowser.tabs.length, 1,
-      "The captive portal tab should have been closed.");
+    ensureNoPortalTab(win);
+    ensureNoPortalNotification(win);
     yield BrowserTestUtils.closeWindow(win);
   },
 
   /**
    * A portal is detected when there's no browser window, and the
    * portal is freed before a browser window is opened. No portal
    * tab should be added when a browser window is opened.
    */
   function* test_detectedWithNoBrowserWindow_GoneBeforeOpen(aSuccess) {
     yield portalDetectedNoBrowserWindow();
     freePortal(aSuccess);
     let win = yield BrowserTestUtils.openNewBrowserWindow();
     // Wait for a while to make sure no tab is opened.
     yield new Promise(resolve => {
       setTimeout(resolve, 1000);
     });
-    is(win.gBrowser.tabs.length, 1,
-      "No captive portal tab should have been opened.");
+    ensureNoPortalTab(win);
+    ensureNoPortalNotification(win);
     yield BrowserTestUtils.closeWindow(win);
   },
 
   /**
    * A portal is detected when a browser window has focus. A portal tab should be
    * opened in the background in the focused browser window. If the portal is
    * freed when the tab isn't focused, the tab should be closed automatically.
    */
   function* test_detectedWithFocus(aSuccess) {
     let win = RecentWindow.getMostRecentBrowserWindow();
     let p = BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL);
     Services.obs.notifyObservers(null, "captive-portal-login", null);
     let tab = yield p;
+    ensurePortalTab(win);
+    ensurePortalNotification(win);
     isnot(win.gBrowser.selectedTab, tab,
       "The captive portal tab should be open in the background in the current window.");
     freePortal(aSuccess);
-    is(win.gBrowser.tabs.length, 1,
-      "The portal tab should have been closed.");
+    ensureNoPortalTab(win);
+    ensureNoPortalNotification(win);
   },
 
   /**
    * A portal is detected when a browser window has focus. A portal tab should be
    * opened in the background in the focused browser window. If the portal is
    * freed when the tab has focus, the tab should be closed automatically.
    */
   function* test_detectedWithFocus_selectedTab(aSuccess) {
     let win = RecentWindow.getMostRecentBrowserWindow();
     let p = BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL);
     Services.obs.notifyObservers(null, "captive-portal-login", null);
     let tab = yield p;
+    ensurePortalTab(win);
+    ensurePortalNotification(win);
     isnot(win.gBrowser.selectedTab, tab,
       "The captive portal tab should be open in the background in the current window.");
     win.gBrowser.selectedTab = tab;
     freePortal(aSuccess);
-    is(win.gBrowser.tabs.length, 1,
-      "The portal tab should have been closed.");
+    ensureNoPortalTab(win);
+    ensureNoPortalNotification(win);
   },
 ];
 
 let singleRunTestCases = [
   /**
    * A portal is detected when there's no browser window,
    * then a browser window is opened, and the portal is logged into
    * and redirects to a different page. The portal tab should be added
    * and focused when the window is opened, and left open after login
    * since it redirected.
    */
   function* test_detectedWithNoBrowserWindow_Redirect() {
     yield portalDetectedNoBrowserWindow();
-    let win = yield openWindowAndWaitForPortalTab();
+    let win = yield openWindowAndWaitForPortalTabAndNotification();
     let browser = win.gBrowser.selectedTab.linkedBrowser;
     let loadPromise =
       BrowserTestUtils.browserLoaded(browser, false, CANONICAL_URL_REDIRECTED);
     BrowserTestUtils.loadURI(browser, CANONICAL_URL_REDIRECTED);
     yield loadPromise;
     freePortal(true);
-    is(win.gBrowser.tabs.length, 2,
-      "The captive portal tab should not have been closed.");
+    ensurePortalTab(win);
+    ensureNoPortalNotification(win);
     yield BrowserTestUtils.closeWindow(win);
   },
 
   /**
    * A portal is detected when a browser window has focus. A portal tab should be
    * opened in the background in the focused browser window. If the portal is
    * freed when the tab isn't focused, the tab should be closed automatically,
    * even if the portal has redirected to a URL other than CANONICAL_URL.
    */
   function* test_detectedWithFocus_redirectUnselectedTab() {
     let win = RecentWindow.getMostRecentBrowserWindow();
     let p = BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL);
     Services.obs.notifyObservers(null, "captive-portal-login", null);
     let tab = yield p;
+    ensurePortalTab(win);
+    ensurePortalNotification(win);
     isnot(win.gBrowser.selectedTab, tab,
       "The captive portal tab should be open in the background in the current window.");
     let browser = tab.linkedBrowser;
     let loadPromise =
       BrowserTestUtils.browserLoaded(browser, false, CANONICAL_URL_REDIRECTED);
     BrowserTestUtils.loadURI(browser, CANONICAL_URL_REDIRECTED);
     yield loadPromise;
     freePortal(true);
-    is(win.gBrowser.tabs.length, 1,
-      "The portal tab should have been closed.");
+    ensureNoPortalTab(win);
+    ensureNoPortalNotification(win);
   },
 
   /**
    * A portal is detected when a browser window has focus. A portal tab should be
    * opened in the background in the focused browser window. If the portal is
    * freed when the tab has focus, and it has redirected to another page, the
    * tab should be kept open.
    */
   function* test_detectedWithFocus_redirectSelectedTab() {
     let win = RecentWindow.getMostRecentBrowserWindow();
     let p = BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL);
     Services.obs.notifyObservers(null, "captive-portal-login", null);
     let tab = yield p;
+    ensurePortalNotification(win);
     isnot(win.gBrowser.selectedTab, tab,
       "The captive portal tab should be open in the background in the current window.");
     win.gBrowser.selectedTab = tab;
     let browser = tab.linkedBrowser;
     let loadPromise =
       BrowserTestUtils.browserLoaded(browser, false, CANONICAL_URL_REDIRECTED);
     BrowserTestUtils.loadURI(browser, CANONICAL_URL_REDIRECTED);
     yield loadPromise;
     freePortal(true);
-    is(win.gBrowser.tabs.length, 2,
-      "The portal tab should not have been closed.");
+    ensurePortalTab(win);
+    ensureNoPortalNotification(win);
     yield BrowserTestUtils.removeTab(tab);
   },
+
+  /**
+   * Test the various expected behaviors of the "Show Login Page" button
+   * in the captive portal notification. The button should be visible for
+   * all tabs except the captive portal tab, and when clicked, should
+   * ensure a captive portal tab is open and select it.
+   */
+  function* test_showLoginPageButton() {
+    let win = RecentWindow.getMostRecentBrowserWindow();
+    let p = BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL);
+    Services.obs.notifyObservers(null, "captive-portal-login", null);
+    let tab = yield p;
+    let notification = ensurePortalNotification(win);
+    isnot(win.gBrowser.selectedTab, tab,
+      "The captive portal tab should be open in the background in the current window.");
+    testShowLoginPageButtonVisibility(notification, "visible");
+
+    function testPortalTabSelectedAndButtonNotVisible() {
+      is(win.gBrowser.selectedTab, tab, "The captive portal tab should be selected.");
+      testShowLoginPageButtonVisibility(notification, "hidden");
+    }
+
+    // Select the captive portal tab. The button should hide.
+    let otherTab = win.gBrowser.selectedTab;
+    win.gBrowser.selectedTab = tab;
+    testShowLoginPageButtonVisibility(notification, "hidden");
+
+    // Select the other tab. The button should become visible.
+    win.gBrowser.selectedTab = otherTab;
+    testShowLoginPageButtonVisibility(notification, "visible");
+
+    // Simulate clicking the button. The portal tab should be selected and
+    // the button should hide.
+    let button = notification.querySelector("button.notification-button");
+    button.click();
+    testPortalTabSelectedAndButtonNotVisible();
+
+    // Close the tab. The button should become visible.
+    yield BrowserTestUtils.removeTab(tab);
+    ensureNoPortalTab(win);
+    testShowLoginPageButtonVisibility(notification, "visible");
+
+    function* clickButtonAndExpectNewPortalTab() {
+      p = BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL);
+      button.click();
+      tab = yield p;
+      is(win.gBrowser.selectedTab, tab, "The captive portal tab should be selected.");
+    }
+
+    // When the button is clicked, a new portal tab should be opened and
+    // selected.
+    yield clickButtonAndExpectNewPortalTab();
+
+    // Open another arbitrary tab. The button should become visible. When it's clicked,
+    // the portal tab should be selected.
+    let anotherTab = yield BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+    testShowLoginPageButtonVisibility(notification, "visible");
+    button.click();
+    is(win.gBrowser.selectedTab, tab, "The captive portal tab should be selected.");
+
+    // Close the portal tab and select the arbitrary tab. The button should become
+    // visible and when it's clicked, a new portal tab should be opened.
+    yield BrowserTestUtils.removeTab(tab);
+    win.gBrowser.selectedTab = anotherTab;
+    testShowLoginPageButtonVisibility(notification, "visible");
+    yield clickButtonAndExpectNewPortalTab();
+
+    yield BrowserTestUtils.removeTab(anotherTab);
+    freePortal(true);
+    ensureNoPortalTab(win);
+    ensureNoPortalNotification(win);
+  },
 ];
 
 for (let testcase of testCasesForBothSuccessAndAbort) {
   add_task(testcase.bind(null, true));
   add_task(testcase.bind(null, false));
 }
 
 for (let testcase of singleRunTestCases) {
--- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
@@ -1197,16 +1197,41 @@ this.BrowserTestUtils = {
    * @param notificationValue (string)
    *        The "value" of the notification, which is often used as
    *        a unique identifier. Example: "plugin-crashed".
    * @return Promise
    *        Resolves to the <xul:notification> that is being shown.
    */
   waitForNotificationBar(tabbrowser, browser, notificationValue) {
     let notificationBox = tabbrowser.getNotificationBox(browser);
+    return this.waitForNotificationInNotificationBox(notificationBox,
+                                                     notificationValue);
+  },
+
+  /**
+   * Waits for a <xul:notification> with a particular value to appear
+   * in the global <xul:notificationbox> of the given browser window.
+   *
+   * @param win (<xul:window>)
+   *        The browser window in whose global notificationbox the
+   *        notification is expected to appear.
+   * @param notificationValue (string)
+   *        The "value" of the notification, which is often used as
+   *        a unique identifier. Example: "captive-portal-detected".
+   * @return Promise
+   *        Resolves to the <xul:notification> that is being shown.
+   */
+  waitForGlobalNotificationBar(win, notificationValue) {
+    let notificationBox =
+      win.document.getElementById("high-priority-global-notificationbox");
+    return this.waitForNotificationInNotificationBox(notificationBox,
+                                                     notificationValue);
+  },
+
+  waitForNotificationInNotificationBox(notificationBox, notificationValue) {
     return new Promise((resolve) => {
       let check = (event) => {
         return event.target.value == notificationValue;
       };
 
       BrowserTestUtils.waitForEvent(notificationBox, "AlertActive",
                                     false, check).then((event) => {
         // The originalTarget of the AlertActive on a notificationbox