merge m-c to fx-team; a=merge
authorTim Taubert <ttaubert@mozilla.com>
Sun, 06 Jul 2014 09:16:35 -0700
changeset 213298 1dc6b294800d93529cbc0bece6603d85f8685f6b
parent 213280 9f59e39f70a56c298fc49d4dc4960908502e78fa (current diff)
parent 213297 a86527f8f58af99a98a509b7f4b97c2648c4a521 (diff)
child 213330 699348fd356b06788e6dadf11c32b947270b071f
child 213331 dc44c1bd01a40f372f274ef75c842947dcda6498
push id3857
push userraliiev@mozilla.com
push dateTue, 02 Sep 2014 16:39:23 +0000
treeherdermozilla-beta@5638b907b505 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone33.0a1
first release with
nightly linux32
1dc6b294800d / 33.0a1 / 20140707030202 / files
nightly linux64
1dc6b294800d / 33.0a1 / 20140707030202 / files
nightly mac
1dc6b294800d / 33.0a1 / 20140707030202 / files
nightly win32
1dc6b294800d / 33.0a1 / 20140707030202 / files
nightly win64
1dc6b294800d / 33.0a1 / 20140707030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge m-c to fx-team; a=merge
browser/base/content/test/general/browser_popupNotification.js
--- a/browser/base/content/abouthome/aboutHome.js
+++ b/browser/base/content/abouthome/aboutHome.js
@@ -341,19 +341,19 @@ function setupSearchEngine()
     searchText.placeholder = searchEngineName;
   }
 
 }
 
 /**
  * Inform the test harness that we're done loading the page.
  */
-function loadSucceeded()
+function loadCompleted()
 {
-  var event = new CustomEvent("AboutHomeLoadSnippetsSucceeded", {bubbles:true});
+  var event = new CustomEvent("AboutHomeLoadSnippetsCompleted", {bubbles:true});
   document.dispatchEvent(event);
 }
 
 /**
  * Update the local snippets from the remote storage, then show them through
  * showSnippets.
  */
 function loadSnippets()
@@ -376,42 +376,39 @@ function loadSnippets()
   // Check last snippets update.
   let lastUpdate = gSnippetsMap.get("snippets-last-update");
   let updateURL = document.documentElement.getAttribute("snippetsURL");
   let shouldUpdate = !lastUpdate ||
                      Date.now() - lastUpdate > SNIPPETS_UPDATE_INTERVAL_MS;
   if (updateURL && shouldUpdate) {
     // Try to update from network.
     let xhr = new XMLHttpRequest();
+    xhr.timeout = 5000;
     try {
       xhr.open("GET", updateURL, true);
     } catch (ex) {
       showSnippets();
-      loadSucceeded();
+      loadCompleted();
       return;
     }
     // Even if fetching should fail we don't want to spam the server, thus
     // set the last update time regardless its results.  Will retry tomorrow.
     gSnippetsMap.set("snippets-last-update", Date.now());
-    xhr.onerror = function (event) {
-      showSnippets();
-    };
-    xhr.onload = function (event)
-    {
+    xhr.onloadend = function (event) {
       if (xhr.status == 200) {
         gSnippetsMap.set("snippets", xhr.responseText);
         gSnippetsMap.set("snippets-cached-version", currentVersion);
       }
       showSnippets();
-      loadSucceeded();
+      loadCompleted();
     };
     xhr.send(null);
   } else {
     showSnippets();
-    loadSucceeded();
+    loadCompleted();
   }
 }
 
 /**
  * Shows locally cached remote snippets, or default ones when not available.
  *
  * @note: snippets should never invoke showSnippets(), or they may cause
  *        a "too much recursion" exception.
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -5179,28 +5179,32 @@ function middleMousePaste(event) {
   });
 
   event.stopPropagation();
 }
 
 function stripUnsafeProtocolOnPaste(pasteData) {
   // Don't allow pasting in full URIs which inherit the security context.
   const URI_INHERITS_SECURITY_CONTEXT = Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT;
+
   let pastedURI;
-  pasteData = pasteData.trim();
-  do {
-    if (pastedURI) {
-      pasteData = pastedURI.path.trim();
-    }
+  try {
+    pastedURI = makeURI(pasteData.trim());
+  } catch (ex) {
+    return pasteData;
+  }
+
+  while (Services.netutil.URIChainHasFlags(pastedURI, URI_INHERITS_SECURITY_CONTEXT)) {
+    pasteData = pastedURI.path.trim();
     try {
       pastedURI = makeURI(pasteData);
     } catch (ex) {
       break;
     }
-  } while (Services.netutil.URIChainHasFlags(pastedURI, URI_INHERITS_SECURITY_CONTEXT));
+  }
 
   return pasteData;
 }
 
 function handleDroppedLink(event, url, name)
 {
   let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
 
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -329,18 +329,16 @@ skip-if = e10s # Bug 866413 - PageInfo d
 skip-if = e10s # Bug ?????? - test directly manipulates content
 
 [browser_parsable_css.js]
 
 [browser_pinnedTabs.js]
 skip-if = e10s # Bug 921905 - pinTab/unpinTab fail in e10s
 [browser_plainTextLinks.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (creates and fetches elements directly from content document)
-[browser_popupNotification.js]
-skip-if = toolkit == "windows" || e10s # Disabled on Windows due to frequent failures (bugs 825739, 841341) / e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
 [browser_popupUI.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (tries to get a popup element directly from content)
 [browser_printpreview.js]
 skip-if = e10s # Bug ?????? - timeout after logging "Error: Channel closing: too late to send/recv, messages will be lost"
 [browser_private_browsing_window.js]
 [browser_private_no_prompt.js]
 [browser_relatedTabs.js]
 [browser_removeTabsToTheEnd.js]
--- a/browser/base/content/test/general/browser_aboutHome.js
+++ b/browser/base/content/test/general/browser_aboutHome.js
@@ -392,17 +392,17 @@ function test()
 
       // Create a tab to run the test.
       let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
 
       // Add an event handler to modify the snippets map once it's ready.
       let snippetsPromise = promiseSetupSnippetsMap(tab, test.setup);
 
       // Start loading about:home and wait for it to complete.
-      yield promiseTabLoadEvent(tab, "about:home", "AboutHomeLoadSnippetsSucceeded");
+      yield promiseTabLoadEvent(tab, "about:home", "AboutHomeLoadSnippetsCompleted");
 
       // This promise should already be resolved since the page is done,
       // but we still want to get the snippets map out of it.
       let snippetsMap = yield snippetsPromise;
 
       info("Running test");
       yield test.run(snippetsMap);
       info("Cleanup");
@@ -410,45 +410,16 @@ function test()
     }
   }).then(finish, ex => {
     ok(false, "Unexpected Exception: " + ex);
     finish();
   });
 }
 
 /**
- * Starts a load in an existing tab and waits for it to finish (via some event).
- *
- * @param aTab
- *        The tab to load into.
- * @param aUrl
- *        The url to load.
- * @param aEvent
- *        The load event type to wait for.  Defaults to "load".
- * @return {Promise} resolved when the event is handled.
- */
-function promiseTabLoadEvent(aTab, aURL, aEventType="load")
-{
-  let deferred = Promise.defer();
-  info("Wait tab event: " + aEventType);
-  aTab.linkedBrowser.addEventListener(aEventType, function load(event) {
-    if (event.originalTarget != aTab.linkedBrowser.contentDocument ||
-        event.target.location.href == "about:blank") {
-      info("skipping spurious load event");
-      return;
-    }
-    aTab.linkedBrowser.removeEventListener(aEventType, load, true);
-    info("Tab event received: " + aEventType);
-    deferred.resolve();
-  }, true, true);
-  aTab.linkedBrowser.loadURI(aURL);
-  return deferred.promise;
-}
-
-/**
  * Cleans up snippets and ensures that by default we don't try to check for
  * remote snippets since that may cause network bustage or slowness.
  *
  * @param aTab
  *        The tab containing about:home.
  * @param aSetupFn
  *        The setup function to be run.
  * @return {Promise} resolved when the snippets are ready.  Gets the snippets map.
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -214,21 +214,17 @@ function whenNewTabLoaded(aWindow, aCall
     aCallback();
     return;
   }
 
   whenTabLoaded(aWindow.gBrowser.selectedTab, aCallback);
 }
 
 function whenTabLoaded(aTab, aCallback) {
-  let browser = aTab.linkedBrowser;
-  browser.addEventListener("load", function onLoad() {
-    browser.removeEventListener("load", onLoad, true);
-    executeSoon(aCallback);
-  }, true);
+  promiseTabLoadEvent(aTab).then(aCallback);
 }
 
 function promiseTabLoaded(aTab) {
   let deferred = Promise.defer();
   whenTabLoaded(aTab, deferred.resolve);
   return deferred.promise;
 }
 
@@ -306,16 +302,17 @@ function promiseHistoryClearedState(aURI
  *
  * @return {Promise}
  * @resolves The array [subject, data] from the observed notification.
  * @rejects Never.
  */
 function promiseTopicObserved(topic)
 {
   let deferred = Promise.defer();
+  info("Waiting for observer topic " + topic);
   Services.obs.addObserver(function PTO_observe(subject, topic, data) {
     Services.obs.removeObserver(PTO_observe, topic);
     deferred.resolve([subject, data]);
   }, topic, false);
   return deferred.promise;
 }
 
 /**
@@ -392,18 +389,17 @@ let FullZoomHelper = {
     return deferred.promise;
   },
 
   load: function load(tab, url) {
     let deferred = Promise.defer();
     let didLoad = false;
     let didZoom = false;
 
-    tab.linkedBrowser.addEventListener("load", function (event) {
-      event.currentTarget.removeEventListener("load", arguments.callee, true);
+    promiseTabLoadEvent(tab).then(event => {
       didLoad = true;
       if (didZoom)
         deferred.resolve();
     }, true);
 
     this.waitForLocationChange().then(function () {
       didZoom = true;
       if (didLoad)
@@ -467,8 +463,51 @@ let FullZoomHelper = {
   failAndContinue: function failAndContinue(func) {
     return function (err) {
       ok(false, err);
       func();
     };
   },
 };
 
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ *        The tab to load into.
+ * @param [optional] url
+ *        The url to load, or the current url.
+ * @param [optional] event
+ *        The load event type to wait for.  Defaults to "load".
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url, eventType="load")
+{
+  let deferred = Promise.defer();
+  info("Wait tab event: " + eventType);
+
+  function handle(event) {
+    if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+        event.target.location.href == "about:blank" ||
+        (url && event.target.location.href != url)) {
+      info("Skipping spurious '" + eventType + "'' event" +
+           " for " + event.target.location.href);
+      return;
+    }
+    clearTimeout(timeout);
+    tab.linkedBrowser.removeEventListener(eventType, handle, true);
+    info("Tab event received: " + eventType);
+    deferred.resolve(event);
+  }
+
+  let timeout = setTimeout(() => {
+    tab.linkedBrowser.removeEventListener(eventType, handle, true);
+    deferred.reject(new Error("Timed out while waiting for a '" + eventType + "'' event"));
+  }, 30000);
+
+  tab.linkedBrowser.addEventListener(eventType, handle, true, true);
+  if (url)
+    tab.linkedBrowser.loadURI(url);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files =
+  head.js
+
+[browser_popupNotification.js]
+skip-if = (os == "linux" && debug) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
+[browser_popupNotification_2.js]
+skip-if = (os == "linux" && debug) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
+[browser_popupNotification_3.js]
+skip-if = (os == "linux" && debug) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
+[browser_popupNotification_4.js]
+skip-if = (os == "linux" && debug) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
rename from browser/base/content/test/general/browser_popupNotification.js
rename to browser/base/content/test/popupNotifications/browser_popupNotification.js
--- a/browser/base/content/test/general/browser_popupNotification.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification.js
@@ -1,258 +1,94 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+// These are shared between test #4 to #5
+let wrongBrowserNotificationObject = new BasicNotification("wrongBrowser");
+let wrongBrowserNotification;
+
 function test() {
   waitForExplicitFinish();
 
   ok(PopupNotifications, "PopupNotifications object exists");
   ok(PopupNotifications.panel, "PopupNotifications panel exists");
 
-  // Disable transitions as they slow the test down and we want to click the
-  // mouse buttons in a predictable location.
-  PopupNotifications.transitionsEnabled = false;
-
-  registerCleanupFunction(cleanUp);
-
-  runNextTest();
-}
-
-function cleanUp() {
-  for (var topic in gActiveObservers)
-    Services.obs.removeObserver(gActiveObservers[topic], topic);
-  for (var eventName in gActiveListeners)
-    PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
-  PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
-  PopupNotifications.transitionsEnabled = true;
-}
-
-const PREF_SECURITY_DELAY_INITIAL = Services.prefs.getIntPref("security.notification_enable_delay");
-
-var gActiveListeners = {};
-var gActiveObservers = {};
-var gShownState = {};
-
-function goNext() {
-  if (++gTestIndex == tests.length)
-    executeSoon(finish);
-  else
-    executeSoon(runNextTest);
-}
-
-function runNextTest() {
-  let nextTest = tests[gTestIndex];
-
-  function addObserver(topic) {
-    function observer() {
-      Services.obs.removeObserver(observer, "PopupNotifications-" + topic);
-      delete gActiveObservers["PopupNotifications-" + topic];
-
-      info("[Test #" + gTestIndex + "] observer for " + topic + " called");
-      nextTest[topic]();
-      goNext();
-    }
-    Services.obs.addObserver(observer, "PopupNotifications-" + topic, false);
-    gActiveObservers["PopupNotifications-" + topic] = observer;
-  }
-
-  if (nextTest.backgroundShow) {
-    addObserver("backgroundShow");
-  } else if (nextTest.updateNotShowing) {
-    addObserver("updateNotShowing");
-  } else if (nextTest.onShown) {
-    doOnPopupEvent("popupshowing", function () {
-      info("[Test #" + gTestIndex + "] popup showing");
-    });
-    doOnPopupEvent("popupshown", function () {
-      gShownState[gTestIndex] = true;
-      info("[Test #" + gTestIndex + "] popup shown");
-      nextTest.onShown(this);
-    });
-
-    // We allow multiple onHidden functions to be defined in an array.  They're
-    // called in the order they appear.
-    let onHiddenArray = nextTest.onHidden instanceof Array ?
-                        nextTest.onHidden :
-                        [nextTest.onHidden];
-    doOnPopupEvent("popuphidden", function () {
-      if (!gShownState[gTestIndex]) {
-        // This is expected to happen for test 9, so let's not treat it as a failure.
-        info("Popup from test " + gTestIndex + " was hidden before its popupshown fired");
-      }
-
-      let onHidden = onHiddenArray.shift();
-      info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)");
-      executeSoon(function () {
-        onHidden.call(nextTest, this);
-        if (!onHiddenArray.length)
-          goNext();
-      }.bind(this));
-    }, onHiddenArray.length);
-    info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen);
-  }
-
-  info("[Test #" + gTestIndex + "] running test");
-  nextTest.run();
+  setup();
+  goNext();
 }
 
-function doOnPopupEvent(eventName, callback, numExpected) {
-  gActiveListeners[eventName] = function (event) {
-    if (event.target != PopupNotifications.panel)
-      return;
-    if (typeof(numExpected) === "number")
-      numExpected--;
-    if (!numExpected) {
-      PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
-      delete gActiveListeners[eventName];
-    }
-
-    callback.call(PopupNotifications.panel);
-  }
-  PopupNotifications.panel.addEventListener(eventName, gActiveListeners[eventName], false);
-}
-
-var gTestIndex = 0;
-var gNewTab;
-
-function basicNotification() {
-  var self = this;
-  this.browser = gBrowser.selectedBrowser;
-  this.id = "test-notification-" + gTestIndex;
-  this.message = "This is popup notification " + this.id + " from test " + gTestIndex;
-  this.anchorID = null;
-  this.mainAction = {
-    label: "Main Action",
-    accessKey: "M",
-    callback: function () {
-      self.mainActionClicked = true;
-    }
-  };
-  this.secondaryActions = [
-    {
-      label: "Secondary Action",
-      accessKey: "S",
-      callback: function () {
-        self.secondaryActionClicked = true;
-      }
-    }
-  ];
-  this.options = {
-    eventCallback: function (eventName) {
-      switch (eventName) {
-        case "dismissed":
-          self.dismissalCallbackTriggered = true;
-          break;
-        case "showing":
-          self.showingCallbackTriggered = true;
-          break;
-        case "shown":
-          self.shownCallbackTriggered = true;
-          break;
-        case "removed":
-          self.removedCallbackTriggered = true;
-          break;
-        case "swapping":
-          self.swappingCallbackTriggered = true;
-          break;
-      }
-    }
-  };
-}
-
-basicNotification.prototype.addOptions = function(options) {
-  for (let [name, value] in Iterator(options))
-    this.options[name] = value;
-};
-
-function errorNotification() {
-  var self = this;
-  this.mainAction.callback = function () {
-    self.mainActionClicked = true;
-    throw new Error("Oops!");
-  };
-  this.secondaryActions[0].callback = function () {
-    self.secondaryActionClicked = true;
-    throw new Error("Oops!");
-  };
-}
-
-errorNotification.prototype = new basicNotification();
-errorNotification.prototype.constructor = errorNotification;
-
-var wrongBrowserNotificationObject = new basicNotification();
-var wrongBrowserNotification;
-
-var tests = [
-  { // Test #0
+let tests = [
+  { id: "Test#1",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       triggerMainCommand(popup);
     },
     onHidden: function (popup) {
       ok(this.notifyObj.mainActionClicked, "mainAction was clicked");
       ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
       ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
     }
   },
-  { // Test #1
+  { id: "Test#2",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       triggerSecondaryCommand(popup, 0);
     },
     onHidden: function (popup) {
       ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
       ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
       ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
     }
   },
-  { // Test #2
+  { id: "Test#3",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       dismissNotification(popup);
     },
     onHidden: function (popup) {
       ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
       this.notification.remove();
       ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
     }
   },
   // test opening a notification for a background browser
-  { // Test #3
-    run: function () {
-      gNewTab = gBrowser.addTab("about:blank");
-      isnot(gBrowser.selectedTab, gNewTab, "new tab isn't selected");
-      wrongBrowserNotificationObject.browser = gBrowser.getBrowserForTab(gNewTab);
+  // Note: test 4 to 6 share a tab.
+  { id: "Test#4",
+    run: function* () {
+      let tab = gBrowser.addTab("about:blank");
+      isnot(gBrowser.selectedTab, tab, "new tab isn't selected");
+      wrongBrowserNotificationObject.browser = gBrowser.getBrowserForTab(tab);
+      let promiseTopic = promiseTopicObserved("PopupNotifications-backgroundShow");
       wrongBrowserNotification = showNotification(wrongBrowserNotificationObject);
-    },
-    backgroundShow: function () {
+      yield promiseTopic;
       is(PopupNotifications.isPanelOpen, false, "panel isn't open");
       ok(!wrongBrowserNotificationObject.mainActionClicked, "main action wasn't clicked");
       ok(!wrongBrowserNotificationObject.secondaryActionClicked, "secondary action wasn't clicked");
       ok(!wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback wasn't called");
+      goNext();
     }
   },
   // now select that browser and test to see that the notification appeared
-  { // Test #4
+  { id: "Test#5",
     run: function () {
       this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gNewTab;
+      gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
     },
     onShown: function (popup) {
       checkPopup(popup, wrongBrowserNotificationObject);
       is(PopupNotifications.isPanelOpen, true, "isPanelOpen getter doesn't lie");
 
       // switch back to the old browser
       gBrowser.selectedTab = this.oldSelectedTab;
     },
@@ -260,50 +96,51 @@ var tests = [
       // actually remove the notification to prevent it from reappearing
       ok(wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback triggered due to tab switch");
       wrongBrowserNotification.remove();
       ok(wrongBrowserNotificationObject.removedCallbackTriggered, "removed callback triggered");
       wrongBrowserNotification = null;
     }
   },
   // test that the removed notification isn't shown on browser re-select
-  { // Test #5
-    run: function () {
-      gBrowser.selectedTab = gNewTab;
-    },
-    updateNotShowing: function () {
+  { id: "Test#6",
+    run: function* () {
+      let promiseTopic = promiseTopicObserved("PopupNotifications-updateNotShowing");
+      gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+      yield promiseTopic;
       is(PopupNotifications.isPanelOpen, false, "panel isn't open");
-      gBrowser.removeTab(gNewTab);
+      gBrowser.removeTab(gBrowser.selectedTab);
+      goNext();
     }
   },
   // Test that two notifications with the same ID result in a single displayed
   // notification.
-  { // Test #6
+  { id: "Test#7",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       // Show the same notification twice
       this.notification1 = showNotification(this.notifyObj);
       this.notification2 = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       this.notification2.remove();
     },
     onHidden: function (popup) {
       ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
       ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
     }
   },
   // Test that two notifications with different IDs are displayed
-  { // Test #7
+  { id: "Test#8",
     run: function () {
-      this.testNotif1 = new basicNotification();
+      this.testNotif1 = new BasicNotification(this.id);
       this.testNotif1.message += " 1";
       showNotification(this.testNotif1);
-      this.testNotif2 = new basicNotification();
+      this.testNotif2 = new BasicNotification(this.id);
       this.testNotif2.message += " 2";
       this.testNotif2.id += "-2";
       showNotification(this.testNotif2);
     },
     onShown: function (popup) {
       is(popup.childNodes.length, 2, "two notifications are shown");
       // Trigger the main command for the first notification, and the secondary
       // for the second. Need to do mainCommand first since the secondaryCommand
@@ -318,898 +155,49 @@ var tests = [
       ok(!this.testNotif1.dismissalCallbackTriggered, "dismissal callback #1 wasn't called");
 
       ok(!this.testNotif2.mainActionClicked, "main action #2 wasn't clicked");
       ok(this.testNotif2.secondaryActionClicked, "secondary action #2 was clicked");
       ok(!this.testNotif2.dismissalCallbackTriggered, "dismissal callback #2 wasn't called");
     }
   },
   // Test notification without mainAction
-  { // Test #8
+  { id: "Test#9",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notifyObj.mainAction = null;
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       dismissNotification(popup);
     },
     onHidden: function (popup) {
       this.notification.remove();
     }
   },
   // Test two notifications with different anchors
-  { // Test #9
+  { id: "Test#10",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.firstNotification = showNotification(this.notifyObj);
-      this.notifyObj2 = new basicNotification();
+      this.notifyObj2 = new BasicNotification(this.id);
       this.notifyObj2.id += "-2";
       this.notifyObj2.anchorID = "addons-notification-icon";
       // Second showNotification() overrides the first
       this.secondNotification = showNotification(this.notifyObj2);
     },
     onShown: function (popup) {
       // This also checks that only one element is shown.
       checkPopup(popup, this.notifyObj2);
       is(document.getElementById("geo-notification-icon").boxObject.width, 0,
          "geo anchor shouldn't be visible");
       dismissNotification(popup);
     },
-    onHidden: [
-      // The second showing triggers a popuphidden event that we should ignore.
-      function (popup) {},
-      function (popup) {
-        // Remove the notifications
-        this.firstNotification.remove();
-        this.secondNotification.remove();
-        ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-        ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
-      }
-    ]
-  },
-  // Test optional params
-  { // Test #10
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.secondaryActions = undefined;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
     onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test that icons appear
-  { // Test #11
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.id = "geolocation";
-      this.notifyObj.anchorID = "geo-notification-icon";
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      let icon = document.getElementById("geo-notification-icon");
-      isnot(icon.boxObject.width, 0,
-            "geo anchor should be visible after dismissal");
-      this.notification.remove();
-      is(icon.boxObject.width, 0,
-         "geo anchor should not be visible after removal");
-    }
-  },
-  // Test that persistence allows the notification to persist across reloads
-  { // Test #12
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        self.notifyObj.addOptions({
-          persistence: 2
-        });
-        self.notification = showNotification(self.notifyObj);
-      });
-    },
-    onShown: function (popup) {
-      this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Next load will remove the notification
-          self.complete = true;
-
-          loadURI("http://example.org/");
-        });
-      });
-    },
-    onHidden: function (popup) {
-      ok(this.complete, "Should only have hidden the notification after 3 page loads");
-      ok(this.notifyObj.removedCallbackTriggered, "removal callback triggered");
-      gBrowser.removeTab(gBrowser.selectedTab);
-      gBrowser.selectedTab = this.oldSelectedTab;
-    }
-  },
-  // Test that a timeout allows the notification to persist across reloads
-  { // Test #13
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        // Set a timeout of 10 minutes that should never be hit
-        self.notifyObj.addOptions({
-          timeout: Date.now() + 600000
-        });
-        self.notification = showNotification(self.notifyObj);
-      });
-    },
-    onShown: function (popup) {
-      this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Next load will hide the notification
-          self.notification.options.timeout = Date.now() - 1;
-          self.complete = true;
-
-          loadURI("http://example.org/");
-        });
-      });
-    },
-    onHidden: function (popup) {
-      ok(this.complete, "Should only have hidden the notification after the timeout was passed");
-      this.notification.remove();
-      gBrowser.removeTab(gBrowser.selectedTab);
-      gBrowser.selectedTab = this.oldSelectedTab;
-    }
-  },
-  // Test that setting persistWhileVisible allows a visible notification to
-  // persist across location changes
-  { // Test #14
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        self.notifyObj.addOptions({
-          persistWhileVisible: true
-        });
-        self.notification = showNotification(self.notifyObj);
-      });
-    },
-    onShown: function (popup) {
-      this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Notification should persist across location changes
-          self.complete = true;
-          dismissNotification(popup);
-        });
-      });
-    },
-    onHidden: function (popup) {
-      ok(this.complete, "Should only have hidden the notification after it was dismissed");
-      this.notification.remove();
-      gBrowser.removeTab(gBrowser.selectedTab);
-      gBrowser.selectedTab = this.oldSelectedTab;
-    }
-  },
-  // Test that nested icon nodes correctly activate popups
-  { // Test #15
-    run: function() {
-      // Add a temporary box as the anchor with a button
-      this.box = document.createElement("box");
-      PopupNotifications.iconBox.appendChild(this.box);
-
-      let button = document.createElement("button");
-      button.setAttribute("label", "Please click me!");
-      this.box.appendChild(button);
-
-      // The notification should open up on the box
-      this.notifyObj = new basicNotification();
-      this.notifyObj.anchorID = this.box.id = "nested-box";
-      this.notifyObj.addOptions({dismissed: true});
-      this.notification = showNotification(this.notifyObj);
-
-      // This test places a normal button in the notification area, which has
-      // standard GTK styling and dimensions. Due to the clip-path, this button
-      // gets clipped off, which makes it necessary to synthesize the mouse click
-      // a little bit downward. To be safe, I adjusted the x-offset with the same
-      // amount.
-      EventUtils.synthesizeMouse(button, 4, 4, {});
-    },
-    onShown: function(popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function(popup) {
-      this.notification.remove();
-      this.box.parentNode.removeChild(this.box);
-    }
-  },
-  // Test that popupnotifications without popups have anchor icons shown
-  { // Test #16
-    run: function() {
-      let notifyObj = new basicNotification();
-      notifyObj.anchorID = "geo-notification-icon";
-      notifyObj.addOptions({neverShow: true});
-      showNotification(notifyObj);
-    },
-    updateNotShowing: function() {
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-    }
-  },
-  // Test notification "Not Now" menu item
-  { // Test #17
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerSecondaryCommand(popup, 1);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test notification close button
-  { // Test #18
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      let notification = popup.childNodes[0];
-      EventUtils.synthesizeMouseAtCenter(notification.closebutton, {});
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test notification when chrome is hidden
-  { // Test #19
-    run: function () {
-      window.locationbar.visible = false;
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-      window.locationbar.visible = true;
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      is(popup.anchorNode.className, "tabbrowser-tab", "notification anchored to tab");
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test notification is removed when dismissed if removeOnDismissal is true
-  { // Test #20
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.addOptions({
-        removeOnDismissal: true
-      });
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
+      // Remove the notifications
+      this.firstNotification.remove();
+      this.secondNotification.remove();
       ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test multiple notification icons are shown
-  { // Test #21
-    run: function () {
-      this.notifyObj1 = new basicNotification();
-      this.notifyObj1.id += "_1";
-      this.notifyObj1.anchorID = "default-notification-icon";
-      this.notification1 = showNotification(this.notifyObj1);
-
-      this.notifyObj2 = new basicNotification();
-      this.notifyObj2.id += "_2";
-      this.notifyObj2.anchorID = "geo-notification-icon";
-      this.notification2 = showNotification(this.notifyObj2);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj2);
-
-      // check notifyObj1 anchor icon is showing
-      isnot(document.getElementById("default-notification-icon").boxObject.width, 0,
-            "default anchor should be visible");
-      // check notifyObj2 anchor icon is showing
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-
-      dismissNotification(popup);
-    },
-    onHidden: [
-      function (popup) {
-      },
-      function (popup) {
-        this.notification1.remove();
-        ok(this.notifyObj1.removedCallbackTriggered, "removed callback triggered");
-
-        this.notification2.remove();
-        ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
-      }
-    ]
-  },
-  // Test that multiple notification icons are removed when switching tabs
-  { // Test #22
-    run: function () {
-      // show the notification on old tab.
-      this.notifyObjOld = new basicNotification();
-      this.notifyObjOld.anchorID = "default-notification-icon";
-      this.notificationOld = showNotification(this.notifyObjOld);
-
-      // switch tab
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      // show the notification on new tab.
-      this.notifyObjNew = new basicNotification();
-      this.notifyObjNew.anchorID = "geo-notification-icon";
-      this.notificationNew = showNotification(this.notifyObjNew);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObjNew);
-
-      // check notifyObjOld anchor icon is removed
-      is(document.getElementById("default-notification-icon").boxObject.width, 0,
-         "default anchor shouldn't be visible");
-      // check notifyObjNew anchor icon is showing
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-
-      dismissNotification(popup);
-    },
-    onHidden: [
-      function (popup) {
-      },
-      function (popup) {
-        this.notificationNew.remove();
-        gBrowser.removeTab(gBrowser.selectedTab);
-
-        gBrowser.selectedTab = this.oldSelectedTab;
-        this.notificationOld.remove();
-      }
-    ]
-  },
-  { // Test #23 - test security delay - too early
-    run: function () {
-      // Set the security delay to 100s
-      PopupNotifications.buttonDelay = 100000;
-
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-
-      // Wait to see if the main command worked
-      executeSoon(function delayedDismissal() {
-        dismissNotification(popup);
-      });
-
-    },
-    onHidden: function (popup) {
-      ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked because it was too soon");
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
-    }
-  },
-  { // Test #24  - test security delay - after delay
-    run: function () {
-      // Set the security delay to 10ms
-      PopupNotifications.buttonDelay = 10;
-
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-
-      // Wait until after the delay to trigger the main action
-      setTimeout(function delayedDismissal() {
-        triggerMainCommand(popup);
-      }, 500);
-
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.mainActionClicked, "mainAction was clicked after the delay");
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback was not triggered");
-      PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
-    }
-  },
-  { // Test #25 - reload removes notification
-    run: function () {
-      loadURI("http://example.com/", function() {
-        let notifyObj = new basicNotification();
-        notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(true, "Notification removed in background tab after reloading");
-            executeSoon(function () {
-              goNext();
-            });
-          }
-        };
-        showNotification(notifyObj);
-        executeSoon(function () {
-          gBrowser.selectedBrowser.reload();
-        });
-      });
-    }
-  },
-  { // Test #26 - location change in background tab removes notification
-    run: function () {
-      let oldSelectedTab = gBrowser.selectedTab;
-      let newTab = gBrowser.addTab("about:blank");
-      gBrowser.selectedTab = newTab;
-
-      loadURI("http://example.com/", function() {
-        gBrowser.selectedTab = oldSelectedTab;
-        let browser = gBrowser.getBrowserForTab(newTab);
-
-        let notifyObj = new basicNotification();
-        notifyObj.browser = browser;
-        notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(true, "Notification removed in background tab after reloading");
-            executeSoon(function () {
-              gBrowser.removeTab(newTab);
-              goNext();
-            });
-          }
-        };
-        showNotification(notifyObj);
-        executeSoon(function () {
-          browser.reload();
-        });
-      });
-    }
-  },
-  { // Test #27 -  Popup notification anchor shouldn't disappear when a notification with the same ID is re-added in a background tab
-    run: function () {
-      loadURI("http://example.com/", function () {
-        let originalTab = gBrowser.selectedTab;
-        let bgTab = gBrowser.addTab("about:blank");
-        gBrowser.selectedTab = bgTab;
-        loadURI("http://example.com/", function () {
-          let anchor = document.createElement("box");
-          anchor.id = "test26-anchor";
-          anchor.className = "notification-anchor-icon";
-          PopupNotifications.iconBox.appendChild(anchor);
-
-          gBrowser.selectedTab = originalTab;
-
-          let fgNotifyObj = new basicNotification();
-          fgNotifyObj.anchorID = anchor.id;
-          fgNotifyObj.options.dismissed = true;
-          let fgNotification = showNotification(fgNotifyObj);
-
-          let bgNotifyObj = new basicNotification();
-          bgNotifyObj.anchorID = anchor.id;
-          bgNotifyObj.browser = gBrowser.getBrowserForTab(bgTab);
-          // show the notification in the background tab ...
-          let bgNotification = showNotification(bgNotifyObj);
-          // ... and re-show it
-          bgNotification = showNotification(bgNotifyObj);
-
-          ok(fgNotification.id, "notification has id");
-          is(fgNotification.id, bgNotification.id, "notification ids are the same");
-          is(anchor.getAttribute("showing"), "true", "anchor still showing");
-
-          fgNotification.remove();
-          gBrowser.removeTab(bgTab);
-          goNext();
-        });
-      });
-    }
-  },
-  { // Test #28 - location change in an embedded frame should not remove a notification
-    run: function () {
-      loadURI("data:text/html;charset=utf8,<iframe id='iframe' src='http://example.com/'>", function () {
-        this.notifyObj = new basicNotification();
-        this.notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(false, "Test 28: Notification removed from browser when subframe navigated");
-          }
-        };
-        showNotification(this.notifyObj);
-      }.bind(this));
-    },
-    onShown: function (popup) {
-      let self = this;
-      let progressListener = {
-        onLocationChange: function onLocationChange(aBrowser) {
-          if (aBrowser != gBrowser.selectedBrowser) {
-            return;
-          }
-          let notification = PopupNotifications.getNotification(self.notifyObj.id,
-                                                                self.notifyObj.browser);
-          ok(notification != null, "Test 28: Notification remained when subframe navigated");
-          self.notifyObj.options.eventCallback = undefined;
-
-          notification.remove();
-          gBrowser.removeTabsProgressListener(progressListener);
-        },
-      };
-
-      info("Test 28: Adding progress listener and performing navigation");
-      gBrowser.addTabsProgressListener(progressListener);
-      content.document.getElementById("iframe")
-                      .setAttribute("src", "http://example.org/");
-    },
-    onHidden: function () {}
-  },
-  { // Test #29 - Popup Notifications should catch exceptions from callbacks
-    run: function () {
-      let callbackCount = 0;
-      this.testNotif1 = new basicNotification();
-      this.testNotif1.message += " 1";
-      this.notification1 = showNotification(this.testNotif1);
-      this.testNotif1.options.eventCallback = function (eventName) {
-        info("notifyObj1.options.eventCallback: " + eventName);
-        if (eventName == "dismissed") {
-          throw new Error("Oops 1!");
-          if (++callbackCount == 2) {
-            executeSoon(goNext);
-          }
-        }
-      };
-
-      this.testNotif2 = new basicNotification();
-      this.testNotif2.message += " 2";
-      this.testNotif2.id += "-2";
-      this.testNotif2.options.eventCallback = function (eventName) {
-        info("notifyObj2.options.eventCallback: " + eventName);
-        if (eventName == "dismissed") {
-          throw new Error("Oops 2!");
-          if (++callbackCount == 2) {
-            executeSoon(goNext);
-          }
-        }
-      };
-      this.notification2 = showNotification(this.testNotif2);
-    },
-    onShown: function (popup) {
-      is(popup.childNodes.length, 2, "two notifications are shown");
-      dismissNotification(popup);
-    },
-    onHidden: function () {
-      this.notification1.remove();
-      this.notification2.remove();
-    }
-  },
-  { // Test #30 - Popup Notifications main actions should catch exceptions from callbacks
-    run: function () {
-      this.testNotif = new errorNotification();
-      showNotification(this.testNotif);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.testNotif);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.testNotif.mainActionClicked, "main action has been triggered");
-    }
-  },
-  { // Test #31 - Popup Notifications secondary actions should catch exceptions from callbacks
-    run: function () {
-      this.testNotif = new errorNotification();
-      showNotification(this.testNotif);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.testNotif);
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.testNotif.secondaryActionClicked, "secondary action has been triggered");
-    }
-  },
-  { // Test #32 -  Existing popup notification shouldn't disappear when adding a dismissed notification
-    run: function () {
-      this.notifyObj1 = new basicNotification();
-      this.notifyObj1.id += "_1";
-      this.notifyObj1.anchorID = "default-notification-icon";
-      this.notification1 = showNotification(this.notifyObj1);
-    },
-    onShown: function (popup) {
-      // Now show a dismissed notification, and check that it doesn't clobber
-      // the showing one.
-      this.notifyObj2 = new basicNotification();
-      this.notifyObj2.id += "_2";
-      this.notifyObj2.anchorID = "geo-notification-icon";
-      this.notifyObj2.options.dismissed = true;
-      this.notification2 = showNotification(this.notifyObj2);
-
-      checkPopup(popup, this.notifyObj1);
-
-      // check that both anchor icons are showing
-      is(document.getElementById("default-notification-icon").getAttribute("showing"), "true",
-         "notification1 anchor should be visible");
-      is(document.getElementById("geo-notification-icon").getAttribute("showing"), "true",
-         "notification2 anchor should be visible");
-
-      dismissNotification(popup);
-    },
-    onHidden: function(popup) {
-      this.notification1.remove();
-      this.notification2.remove();
-    }
-  },
-  { // Test #33 - Showing should be able to modify the popup data
-    run: function() {
-      this.notifyObj = new basicNotification();
-      var normalCallback = this.notifyObj.options.eventCallback;
-      this.notifyObj.options.eventCallback = function (eventName) {
-        if (eventName == "showing") {
-          this.mainAction.label = "Alternate Label";
-        }
-        normalCallback.call(this, eventName);
-      };
-      showNotification(this.notifyObj);
-    },
-    onShown: function(popup) {
-      // checkPopup checks for the matching label. Note that this assumes that
-      // this.notifyObj.mainAction is the same as notification.mainAction,
-      // which could be a problem if we ever decided to deep-copy.
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function() { }
-  },
-  { // Test #34 - Moving a tab to a new window should remove non-swappable
-    // notifications.
-    run: function() {
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-      let notifyObj = new basicNotification();
-      showNotification(notifyObj);
-      let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
-      whenDelayedStartupFinished(win, function() {
-        let [tab] = win.gBrowser.tabs;
-        let anchor = win.document.getElementById("default-notification-icon");
-        win.PopupNotifications._reshowNotifications(anchor);
-        ok(win.PopupNotifications.panel.childNodes.length == 0,
-           "no notification displayed in new window");
-        ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
-        ok(notifyObj.removedCallbackTriggered, "the removed callback was triggered");
-        win.close();
-        goNext();
-      });
-    }
-  },
-  { // Test #35 - Moving a tab to a new window should preserve swappable notifications.
-    run: function() {
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-      let notifyObj = new basicNotification();
-      let originalCallback = notifyObj.options.eventCallback;
-      notifyObj.options.eventCallback = function (eventName) {
-        originalCallback(eventName);
-        return eventName == "swapping";
-      };
-
-      showNotification(notifyObj);
-      let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
-      whenDelayedStartupFinished(win, function() {
-        let [tab] = win.gBrowser.tabs;
-        let anchor = win.document.getElementById("default-notification-icon");
-        win.PopupNotifications._reshowNotifications(anchor);
-        checkPopup(win.PopupNotifications.panel, notifyObj);
-        ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
-        win.close();
-        goNext();
-      });
-    }
-  },
-  { // Test #36 - the hideNotNow option
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.options.hideNotNow = true;
-      this.notifyObj.mainAction.dismiss = true;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      // checkPopup verifies that the Not Now item is hidden, and that no separator is added.
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      this.notification.remove();
-    }
-  },
-  { // Test #37 - the main action callback can keep the notification.
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.mainAction.dismiss = true;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
-      ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
-      this.notification.remove();
-    }
-  },
-  { // Test #38 - a secondary action callback can keep the notification.
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.secondaryActions[0].dismiss = true;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
-      ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
-      this.notification.remove();
-    }
-  },
-  { // Test #39 - returning true in the showing callback should dismiss the notification.
-    run: function() {
-      let notifyObj = new basicNotification();
-      let originalCallback = notifyObj.options.eventCallback;
-      notifyObj.options.eventCallback = function (eventName) {
-        originalCallback(eventName);
-        return eventName == "showing";
-      };
-
-      let notification = showNotification(notifyObj);
-      ok(notifyObj.showingCallbackTriggered, "the showing callback was triggered");
-      ok(!notifyObj.shownCallbackTriggered, "the shown callback wasn't triggered");
-      notification.remove();
-      goNext();
+      ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
     }
   }
 ];
-
-function showNotification(notifyObj) {
-  return PopupNotifications.show(notifyObj.browser,
-                                 notifyObj.id,
-                                 notifyObj.message,
-                                 notifyObj.anchorID,
-                                 notifyObj.mainAction,
-                                 notifyObj.secondaryActions,
-                                 notifyObj.options);
-}
-
-function checkPopup(popup, notificationObj) {
-  info("[Test #" + gTestIndex + "] checking popup");
-
-  ok(notificationObj.showingCallbackTriggered, "showing callback was triggered");
-  ok(notificationObj.shownCallbackTriggered, "shown callback was triggered");
-
-  let notifications = popup.childNodes;
-  is(notifications.length, 1, "one notification displayed");
-  let notification = notifications[0];
-  if (!notification)
-    return;
-  let icon = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-icon");
-  if (notificationObj.id == "geolocation") {
-    isnot(icon.boxObject.width, 0, "icon for geo displayed");
-    is(popup.anchorNode.className, "notification-anchor-icon", "notification anchored to icon");
-  }
-  is(notification.getAttribute("label"), notificationObj.message, "message matches");
-  is(notification.id, notificationObj.id + "-notification", "id matches");
-  if (notificationObj.mainAction) {
-    is(notification.getAttribute("buttonlabel"), notificationObj.mainAction.label, "main action label matches");
-    is(notification.getAttribute("buttonaccesskey"), notificationObj.mainAction.accessKey, "main action accesskey matches");
-  }
-  let actualSecondaryActions = Array.filter(notification.childNodes,
-                                            function (child) child.nodeName == "menuitem");
-  let secondaryActions = notificationObj.secondaryActions || [];
-  let actualSecondaryActionsCount = actualSecondaryActions.length;
-  if (notificationObj.options.hideNotNow) {
-    is(notification.getAttribute("hidenotnow"), "true", "Not Now item hidden");
-    if (secondaryActions.length)
-      is(notification.lastChild.tagName, "menuitem", "no menuseparator");
-  }
-  else if (secondaryActions.length) {
-    is(notification.lastChild.tagName, "menuseparator", "menuseparator exists");
-  }
-  is(actualSecondaryActionsCount, secondaryActions.length, actualSecondaryActions.length + " secondary actions");
-  secondaryActions.forEach(function (a, i) {
-    is(actualSecondaryActions[i].getAttribute("label"), a.label, "label for secondary action " + i + " matches");
-    is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey, "accessKey for secondary action " + i + " matches");
-  });
-}
-
-function triggerMainCommand(popup) {
-  info("[Test #" + gTestIndex + "] triggering main command");
-  let notifications = popup.childNodes;
-  ok(notifications.length > 0, "at least one notification displayed");
-  let notification = notifications[0];
-
-  // 20, 10 so that the inner button is hit
-  EventUtils.synthesizeMouse(notification.button, 20, 10, {});
-}
-
-function triggerSecondaryCommand(popup, index) {
-  info("[Test #" + gTestIndex + "] triggering secondary command");
-  let notifications = popup.childNodes;
-  ok(notifications.length > 0, "at least one notification displayed");
-  let notification = notifications[0];
-
-  // Cancel the arrow panel slide-in transition (bug 767133) such that
-  // it won't interfere with us interacting with the dropdown.
-  document.getAnonymousNodes(popup)[0].style.transition = "none";
-
-  notification.button.focus();
-
-  popup.addEventListener("popupshown", function () {
-    popup.removeEventListener("popupshown", arguments.callee, false);
-
-    // Press down until the desired command is selected
-    for (let i = 0; i <= index; i++)
-      EventUtils.synthesizeKey("VK_DOWN", {});
-
-    // Activate
-    EventUtils.synthesizeKey("VK_RETURN", {});
-  }, false);
-
-  // One down event to open the popup
-  EventUtils.synthesizeKey("VK_DOWN", { altKey: !navigator.platform.contains("Mac") });
-}
-
-function loadURI(uri, callback) {
-  if (callback) {
-    gBrowser.addEventListener("load", function() {
-      // Ignore the about:blank load
-      if (gBrowser.currentURI.spec == "about:blank")
-        return;
-
-      gBrowser.removeEventListener("load", arguments.callee, true);
-
-      callback();
-    }, true);
-  }
-  gBrowser.loadURI(uri);
-}
-
-function dismissNotification(popup) {
-  info("[Test #" + gTestIndex + "] dismissing notification");
-  executeSoon(function () {
-    EventUtils.synthesizeKey("VK_ESCAPE", {});
-  });
-}
copy from browser/base/content/test/general/browser_popupNotification.js
copy to browser/base/content/test/popupNotifications/browser_popupNotification_2.js
--- a/browser/base/content/test/general/browser_popupNotification.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_2.js
@@ -3,396 +3,42 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 function test() {
   waitForExplicitFinish();
 
   ok(PopupNotifications, "PopupNotifications object exists");
   ok(PopupNotifications.panel, "PopupNotifications panel exists");
 
-  // Disable transitions as they slow the test down and we want to click the
-  // mouse buttons in a predictable location.
-  PopupNotifications.transitionsEnabled = false;
-
-  registerCleanupFunction(cleanUp);
-
-  runNextTest();
-}
-
-function cleanUp() {
-  for (var topic in gActiveObservers)
-    Services.obs.removeObserver(gActiveObservers[topic], topic);
-  for (var eventName in gActiveListeners)
-    PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
-  PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
-  PopupNotifications.transitionsEnabled = true;
-}
-
-const PREF_SECURITY_DELAY_INITIAL = Services.prefs.getIntPref("security.notification_enable_delay");
-
-var gActiveListeners = {};
-var gActiveObservers = {};
-var gShownState = {};
-
-function goNext() {
-  if (++gTestIndex == tests.length)
-    executeSoon(finish);
-  else
-    executeSoon(runNextTest);
-}
-
-function runNextTest() {
-  let nextTest = tests[gTestIndex];
-
-  function addObserver(topic) {
-    function observer() {
-      Services.obs.removeObserver(observer, "PopupNotifications-" + topic);
-      delete gActiveObservers["PopupNotifications-" + topic];
-
-      info("[Test #" + gTestIndex + "] observer for " + topic + " called");
-      nextTest[topic]();
-      goNext();
-    }
-    Services.obs.addObserver(observer, "PopupNotifications-" + topic, false);
-    gActiveObservers["PopupNotifications-" + topic] = observer;
-  }
-
-  if (nextTest.backgroundShow) {
-    addObserver("backgroundShow");
-  } else if (nextTest.updateNotShowing) {
-    addObserver("updateNotShowing");
-  } else if (nextTest.onShown) {
-    doOnPopupEvent("popupshowing", function () {
-      info("[Test #" + gTestIndex + "] popup showing");
-    });
-    doOnPopupEvent("popupshown", function () {
-      gShownState[gTestIndex] = true;
-      info("[Test #" + gTestIndex + "] popup shown");
-      nextTest.onShown(this);
-    });
-
-    // We allow multiple onHidden functions to be defined in an array.  They're
-    // called in the order they appear.
-    let onHiddenArray = nextTest.onHidden instanceof Array ?
-                        nextTest.onHidden :
-                        [nextTest.onHidden];
-    doOnPopupEvent("popuphidden", function () {
-      if (!gShownState[gTestIndex]) {
-        // This is expected to happen for test 9, so let's not treat it as a failure.
-        info("Popup from test " + gTestIndex + " was hidden before its popupshown fired");
-      }
-
-      let onHidden = onHiddenArray.shift();
-      info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)");
-      executeSoon(function () {
-        onHidden.call(nextTest, this);
-        if (!onHiddenArray.length)
-          goNext();
-      }.bind(this));
-    }, onHiddenArray.length);
-    info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen);
-  }
-
-  info("[Test #" + gTestIndex + "] running test");
-  nextTest.run();
-}
-
-function doOnPopupEvent(eventName, callback, numExpected) {
-  gActiveListeners[eventName] = function (event) {
-    if (event.target != PopupNotifications.panel)
-      return;
-    if (typeof(numExpected) === "number")
-      numExpected--;
-    if (!numExpected) {
-      PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
-      delete gActiveListeners[eventName];
-    }
-
-    callback.call(PopupNotifications.panel);
-  }
-  PopupNotifications.panel.addEventListener(eventName, gActiveListeners[eventName], false);
-}
-
-var gTestIndex = 0;
-var gNewTab;
-
-function basicNotification() {
-  var self = this;
-  this.browser = gBrowser.selectedBrowser;
-  this.id = "test-notification-" + gTestIndex;
-  this.message = "This is popup notification " + this.id + " from test " + gTestIndex;
-  this.anchorID = null;
-  this.mainAction = {
-    label: "Main Action",
-    accessKey: "M",
-    callback: function () {
-      self.mainActionClicked = true;
-    }
-  };
-  this.secondaryActions = [
-    {
-      label: "Secondary Action",
-      accessKey: "S",
-      callback: function () {
-        self.secondaryActionClicked = true;
-      }
-    }
-  ];
-  this.options = {
-    eventCallback: function (eventName) {
-      switch (eventName) {
-        case "dismissed":
-          self.dismissalCallbackTriggered = true;
-          break;
-        case "showing":
-          self.showingCallbackTriggered = true;
-          break;
-        case "shown":
-          self.shownCallbackTriggered = true;
-          break;
-        case "removed":
-          self.removedCallbackTriggered = true;
-          break;
-        case "swapping":
-          self.swappingCallbackTriggered = true;
-          break;
-      }
-    }
-  };
-}
-
-basicNotification.prototype.addOptions = function(options) {
-  for (let [name, value] in Iterator(options))
-    this.options[name] = value;
-};
-
-function errorNotification() {
-  var self = this;
-  this.mainAction.callback = function () {
-    self.mainActionClicked = true;
-    throw new Error("Oops!");
-  };
-  this.secondaryActions[0].callback = function () {
-    self.secondaryActionClicked = true;
-    throw new Error("Oops!");
-  };
+  setup();
+  goNext();
 }
 
-errorNotification.prototype = new basicNotification();
-errorNotification.prototype.constructor = errorNotification;
-
-var wrongBrowserNotificationObject = new basicNotification();
-var wrongBrowserNotification;
-
-var tests = [
-  { // Test #0
-    run: function () {
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.mainActionClicked, "mainAction was clicked");
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  { // Test #1
-    run: function () {
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  { // Test #2
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // test opening a notification for a background browser
-  { // Test #3
-    run: function () {
-      gNewTab = gBrowser.addTab("about:blank");
-      isnot(gBrowser.selectedTab, gNewTab, "new tab isn't selected");
-      wrongBrowserNotificationObject.browser = gBrowser.getBrowserForTab(gNewTab);
-      wrongBrowserNotification = showNotification(wrongBrowserNotificationObject);
-    },
-    backgroundShow: function () {
-      is(PopupNotifications.isPanelOpen, false, "panel isn't open");
-      ok(!wrongBrowserNotificationObject.mainActionClicked, "main action wasn't clicked");
-      ok(!wrongBrowserNotificationObject.secondaryActionClicked, "secondary action wasn't clicked");
-      ok(!wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback wasn't called");
-    }
-  },
-  // now select that browser and test to see that the notification appeared
-  { // Test #4
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gNewTab;
-    },
-    onShown: function (popup) {
-      checkPopup(popup, wrongBrowserNotificationObject);
-      is(PopupNotifications.isPanelOpen, true, "isPanelOpen getter doesn't lie");
-
-      // switch back to the old browser
-      gBrowser.selectedTab = this.oldSelectedTab;
-    },
-    onHidden: function (popup) {
-      // actually remove the notification to prevent it from reappearing
-      ok(wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback triggered due to tab switch");
-      wrongBrowserNotification.remove();
-      ok(wrongBrowserNotificationObject.removedCallbackTriggered, "removed callback triggered");
-      wrongBrowserNotification = null;
-    }
-  },
-  // test that the removed notification isn't shown on browser re-select
-  { // Test #5
+let tests = [
+  // Test optional params
+  { id: "Test#1",
     run: function () {
-      gBrowser.selectedTab = gNewTab;
-    },
-    updateNotShowing: function () {
-      is(PopupNotifications.isPanelOpen, false, "panel isn't open");
-      gBrowser.removeTab(gNewTab);
-    }
-  },
-  // Test that two notifications with the same ID result in a single displayed
-  // notification.
-  { // Test #6
-    run: function () {
-      this.notifyObj = new basicNotification();
-      // Show the same notification twice
-      this.notification1 = showNotification(this.notifyObj);
-      this.notification2 = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      this.notification2.remove();
-    },
-    onHidden: function (popup) {
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test that two notifications with different IDs are displayed
-  { // Test #7
-    run: function () {
-      this.testNotif1 = new basicNotification();
-      this.testNotif1.message += " 1";
-      showNotification(this.testNotif1);
-      this.testNotif2 = new basicNotification();
-      this.testNotif2.message += " 2";
-      this.testNotif2.id += "-2";
-      showNotification(this.testNotif2);
-    },
-    onShown: function (popup) {
-      is(popup.childNodes.length, 2, "two notifications are shown");
-      // Trigger the main command for the first notification, and the secondary
-      // for the second. Need to do mainCommand first since the secondaryCommand
-      // triggering is async.
-      triggerMainCommand(popup);
-      is(popup.childNodes.length, 1, "only one notification left");
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.testNotif1.mainActionClicked, "main action #1 was clicked");
-      ok(!this.testNotif1.secondaryActionClicked, "secondary action #1 wasn't clicked");
-      ok(!this.testNotif1.dismissalCallbackTriggered, "dismissal callback #1 wasn't called");
-
-      ok(!this.testNotif2.mainActionClicked, "main action #2 wasn't clicked");
-      ok(this.testNotif2.secondaryActionClicked, "secondary action #2 was clicked");
-      ok(!this.testNotif2.dismissalCallbackTriggered, "dismissal callback #2 wasn't called");
-    }
-  },
-  // Test notification without mainAction
-  { // Test #8
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.mainAction = null;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      this.notification.remove();
-    }
-  },
-  // Test two notifications with different anchors
-  { // Test #9
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.firstNotification = showNotification(this.notifyObj);
-      this.notifyObj2 = new basicNotification();
-      this.notifyObj2.id += "-2";
-      this.notifyObj2.anchorID = "addons-notification-icon";
-      // Second showNotification() overrides the first
-      this.secondNotification = showNotification(this.notifyObj2);
-    },
-    onShown: function (popup) {
-      // This also checks that only one element is shown.
-      checkPopup(popup, this.notifyObj2);
-      is(document.getElementById("geo-notification-icon").boxObject.width, 0,
-         "geo anchor shouldn't be visible");
-      dismissNotification(popup);
-    },
-    onHidden: [
-      // The second showing triggers a popuphidden event that we should ignore.
-      function (popup) {},
-      function (popup) {
-        // Remove the notifications
-        this.firstNotification.remove();
-        this.secondNotification.remove();
-        ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-        ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
-      }
-    ]
-  },
-  // Test optional params
-  { // Test #10
-    run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notifyObj.secondaryActions = undefined;
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       dismissNotification(popup);
     },
     onHidden: function (popup) {
       ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
       this.notification.remove();
       ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
     }
   },
   // Test that icons appear
-  { // Test #11
+  { id: "Test#2",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notifyObj.id = "geolocation";
       this.notifyObj.anchorID = "geo-notification-icon";
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
             "geo anchor should be visible");
@@ -402,139 +48,116 @@ var tests = [
       let icon = document.getElementById("geo-notification-icon");
       isnot(icon.boxObject.width, 0,
             "geo anchor should be visible after dismissal");
       this.notification.remove();
       is(icon.boxObject.width, 0,
          "geo anchor should not be visible after removal");
     }
   },
+
   // Test that persistence allows the notification to persist across reloads
-  { // Test #12
-    run: function () {
+  { id: "Test#3",
+    run: function* () {
       this.oldSelectedTab = gBrowser.selectedTab;
       gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        self.notifyObj.addOptions({
-          persistence: 2
-        });
-        self.notification = showNotification(self.notifyObj);
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      this.notifyObj = new BasicNotification(this.id);
+      this.notifyObj.addOptions({
+        persistence: 2
       });
+      this.notification = showNotification(this.notifyObj);
     },
-    onShown: function (popup) {
+    onShown: function* (popup) {
       this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Next load will remove the notification
-          self.complete = true;
-
-          loadURI("http://example.org/");
-        });
-      });
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/")
+      // Next load will remove the notification
+      this.complete = true;
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
     },
     onHidden: function (popup) {
       ok(this.complete, "Should only have hidden the notification after 3 page loads");
       ok(this.notifyObj.removedCallbackTriggered, "removal callback triggered");
       gBrowser.removeTab(gBrowser.selectedTab);
       gBrowser.selectedTab = this.oldSelectedTab;
     }
   },
   // Test that a timeout allows the notification to persist across reloads
-  { // Test #13
-    run: function () {
+  { id: "Test#4",
+    run: function* () {
       this.oldSelectedTab = gBrowser.selectedTab;
       gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        // Set a timeout of 10 minutes that should never be hit
-        self.notifyObj.addOptions({
-          timeout: Date.now() + 600000
-        });
-        self.notification = showNotification(self.notifyObj);
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      this.notifyObj = new BasicNotification(this.id);
+      // Set a timeout of 10 minutes that should never be hit
+      this.notifyObj.addOptions({
+        timeout: Date.now() + 600000
       });
+      this.notification = showNotification(this.notifyObj);
     },
-    onShown: function (popup) {
+    onShown: function* (popup) {
       this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Next load will hide the notification
-          self.notification.options.timeout = Date.now() - 1;
-          self.complete = true;
-
-          loadURI("http://example.org/");
-        });
-      });
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      // Next load will hide the notification
+      this.notification.options.timeout = Date.now() - 1;
+      this.complete = true;
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
     },
     onHidden: function (popup) {
       ok(this.complete, "Should only have hidden the notification after the timeout was passed");
       this.notification.remove();
       gBrowser.removeTab(gBrowser.selectedTab);
       gBrowser.selectedTab = this.oldSelectedTab;
     }
   },
   // Test that setting persistWhileVisible allows a visible notification to
   // persist across location changes
-  { // Test #14
-    run: function () {
+  { id: "Test#5",
+    run: function* () {
       this.oldSelectedTab = gBrowser.selectedTab;
       gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        self.notifyObj.addOptions({
-          persistWhileVisible: true
-        });
-        self.notification = showNotification(self.notifyObj);
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      this.notifyObj = new BasicNotification(this.id);
+      this.notifyObj.addOptions({
+        persistWhileVisible: true
       });
+      this.notification = showNotification(this.notifyObj);
     },
-    onShown: function (popup) {
+    onShown: function* (popup) {
       this.complete = false;
 
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Notification should persist across location changes
-          self.complete = true;
-          dismissNotification(popup);
-        });
-      });
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      // Notification should persist across location changes
+      this.complete = true;
+      dismissNotification(popup);
     },
     onHidden: function (popup) {
       ok(this.complete, "Should only have hidden the notification after it was dismissed");
       this.notification.remove();
       gBrowser.removeTab(gBrowser.selectedTab);
       gBrowser.selectedTab = this.oldSelectedTab;
     }
   },
+
   // Test that nested icon nodes correctly activate popups
-  { // Test #15
+  { id: "Test#6",
     run: function() {
       // Add a temporary box as the anchor with a button
       this.box = document.createElement("box");
       PopupNotifications.iconBox.appendChild(this.box);
 
       let button = document.createElement("button");
       button.setAttribute("label", "Please click me!");
       this.box.appendChild(button);
 
       // The notification should open up on the box
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notifyObj.anchorID = this.box.id = "nested-box";
       this.notifyObj.addOptions({dismissed: true});
       this.notification = showNotification(this.notifyObj);
 
       // This test places a normal button in the notification area, which has
       // standard GTK styling and dimensions. Due to the clip-path, this button
       // gets clipped off, which makes it necessary to synthesize the mouse click
       // a little bit downward. To be safe, I adjusted the x-offset with the same
@@ -546,670 +169,74 @@ var tests = [
       dismissNotification(popup);
     },
     onHidden: function(popup) {
       this.notification.remove();
       this.box.parentNode.removeChild(this.box);
     }
   },
   // Test that popupnotifications without popups have anchor icons shown
-  { // Test #16
-    run: function() {
-      let notifyObj = new basicNotification();
+  { id: "Test#7",
+    run: function* () {
+      let notifyObj = new BasicNotification(this.id);
       notifyObj.anchorID = "geo-notification-icon";
       notifyObj.addOptions({neverShow: true});
+      let promiseTopic = promiseTopicObserved("PopupNotifications-updateNotShowing");
       showNotification(notifyObj);
-    },
-    updateNotShowing: function() {
+      yield promiseTopic;
       isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
             "geo anchor should be visible");
+      goNext();
     }
   },
   // Test notification "Not Now" menu item
-  { // Test #17
+  { id: "Test#8",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       triggerSecondaryCommand(popup, 1);
     },
     onHidden: function (popup) {
       ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
       this.notification.remove();
       ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
     }
   },
   // Test notification close button
-  { // Test #18
+  { id: "Test#9",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       let notification = popup.childNodes[0];
       EventUtils.synthesizeMouseAtCenter(notification.closebutton, {});
     },
     onHidden: function (popup) {
       ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
       this.notification.remove();
       ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
     }
   },
   // Test notification when chrome is hidden
-  { // Test #19
+  { id: "Test#10",
     run: function () {
       window.locationbar.visible = false;
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notification = showNotification(this.notifyObj);
       window.locationbar.visible = true;
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       is(popup.anchorNode.className, "tabbrowser-tab", "notification anchored to tab");
       dismissNotification(popup);
     },
     onHidden: function (popup) {
       ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
       this.notification.remove();
       ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
     }
-  },
-  // Test notification is removed when dismissed if removeOnDismissal is true
-  { // Test #20
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.addOptions({
-        removeOnDismissal: true
-      });
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test multiple notification icons are shown
-  { // Test #21
-    run: function () {
-      this.notifyObj1 = new basicNotification();
-      this.notifyObj1.id += "_1";
-      this.notifyObj1.anchorID = "default-notification-icon";
-      this.notification1 = showNotification(this.notifyObj1);
-
-      this.notifyObj2 = new basicNotification();
-      this.notifyObj2.id += "_2";
-      this.notifyObj2.anchorID = "geo-notification-icon";
-      this.notification2 = showNotification(this.notifyObj2);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj2);
-
-      // check notifyObj1 anchor icon is showing
-      isnot(document.getElementById("default-notification-icon").boxObject.width, 0,
-            "default anchor should be visible");
-      // check notifyObj2 anchor icon is showing
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-
-      dismissNotification(popup);
-    },
-    onHidden: [
-      function (popup) {
-      },
-      function (popup) {
-        this.notification1.remove();
-        ok(this.notifyObj1.removedCallbackTriggered, "removed callback triggered");
-
-        this.notification2.remove();
-        ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
-      }
-    ]
-  },
-  // Test that multiple notification icons are removed when switching tabs
-  { // Test #22
-    run: function () {
-      // show the notification on old tab.
-      this.notifyObjOld = new basicNotification();
-      this.notifyObjOld.anchorID = "default-notification-icon";
-      this.notificationOld = showNotification(this.notifyObjOld);
-
-      // switch tab
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      // show the notification on new tab.
-      this.notifyObjNew = new basicNotification();
-      this.notifyObjNew.anchorID = "geo-notification-icon";
-      this.notificationNew = showNotification(this.notifyObjNew);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObjNew);
-
-      // check notifyObjOld anchor icon is removed
-      is(document.getElementById("default-notification-icon").boxObject.width, 0,
-         "default anchor shouldn't be visible");
-      // check notifyObjNew anchor icon is showing
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-
-      dismissNotification(popup);
-    },
-    onHidden: [
-      function (popup) {
-      },
-      function (popup) {
-        this.notificationNew.remove();
-        gBrowser.removeTab(gBrowser.selectedTab);
-
-        gBrowser.selectedTab = this.oldSelectedTab;
-        this.notificationOld.remove();
-      }
-    ]
-  },
-  { // Test #23 - test security delay - too early
-    run: function () {
-      // Set the security delay to 100s
-      PopupNotifications.buttonDelay = 100000;
-
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-
-      // Wait to see if the main command worked
-      executeSoon(function delayedDismissal() {
-        dismissNotification(popup);
-      });
-
-    },
-    onHidden: function (popup) {
-      ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked because it was too soon");
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
-    }
-  },
-  { // Test #24  - test security delay - after delay
-    run: function () {
-      // Set the security delay to 10ms
-      PopupNotifications.buttonDelay = 10;
-
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-
-      // Wait until after the delay to trigger the main action
-      setTimeout(function delayedDismissal() {
-        triggerMainCommand(popup);
-      }, 500);
-
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.mainActionClicked, "mainAction was clicked after the delay");
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback was not triggered");
-      PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
-    }
-  },
-  { // Test #25 - reload removes notification
-    run: function () {
-      loadURI("http://example.com/", function() {
-        let notifyObj = new basicNotification();
-        notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(true, "Notification removed in background tab after reloading");
-            executeSoon(function () {
-              goNext();
-            });
-          }
-        };
-        showNotification(notifyObj);
-        executeSoon(function () {
-          gBrowser.selectedBrowser.reload();
-        });
-      });
-    }
-  },
-  { // Test #26 - location change in background tab removes notification
-    run: function () {
-      let oldSelectedTab = gBrowser.selectedTab;
-      let newTab = gBrowser.addTab("about:blank");
-      gBrowser.selectedTab = newTab;
-
-      loadURI("http://example.com/", function() {
-        gBrowser.selectedTab = oldSelectedTab;
-        let browser = gBrowser.getBrowserForTab(newTab);
-
-        let notifyObj = new basicNotification();
-        notifyObj.browser = browser;
-        notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(true, "Notification removed in background tab after reloading");
-            executeSoon(function () {
-              gBrowser.removeTab(newTab);
-              goNext();
-            });
-          }
-        };
-        showNotification(notifyObj);
-        executeSoon(function () {
-          browser.reload();
-        });
-      });
-    }
-  },
-  { // Test #27 -  Popup notification anchor shouldn't disappear when a notification with the same ID is re-added in a background tab
-    run: function () {
-      loadURI("http://example.com/", function () {
-        let originalTab = gBrowser.selectedTab;
-        let bgTab = gBrowser.addTab("about:blank");
-        gBrowser.selectedTab = bgTab;
-        loadURI("http://example.com/", function () {
-          let anchor = document.createElement("box");
-          anchor.id = "test26-anchor";
-          anchor.className = "notification-anchor-icon";
-          PopupNotifications.iconBox.appendChild(anchor);
-
-          gBrowser.selectedTab = originalTab;
-
-          let fgNotifyObj = new basicNotification();
-          fgNotifyObj.anchorID = anchor.id;
-          fgNotifyObj.options.dismissed = true;
-          let fgNotification = showNotification(fgNotifyObj);
-
-          let bgNotifyObj = new basicNotification();
-          bgNotifyObj.anchorID = anchor.id;
-          bgNotifyObj.browser = gBrowser.getBrowserForTab(bgTab);
-          // show the notification in the background tab ...
-          let bgNotification = showNotification(bgNotifyObj);
-          // ... and re-show it
-          bgNotification = showNotification(bgNotifyObj);
-
-          ok(fgNotification.id, "notification has id");
-          is(fgNotification.id, bgNotification.id, "notification ids are the same");
-          is(anchor.getAttribute("showing"), "true", "anchor still showing");
-
-          fgNotification.remove();
-          gBrowser.removeTab(bgTab);
-          goNext();
-        });
-      });
-    }
-  },
-  { // Test #28 - location change in an embedded frame should not remove a notification
-    run: function () {
-      loadURI("data:text/html;charset=utf8,<iframe id='iframe' src='http://example.com/'>", function () {
-        this.notifyObj = new basicNotification();
-        this.notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(false, "Test 28: Notification removed from browser when subframe navigated");
-          }
-        };
-        showNotification(this.notifyObj);
-      }.bind(this));
-    },
-    onShown: function (popup) {
-      let self = this;
-      let progressListener = {
-        onLocationChange: function onLocationChange(aBrowser) {
-          if (aBrowser != gBrowser.selectedBrowser) {
-            return;
-          }
-          let notification = PopupNotifications.getNotification(self.notifyObj.id,
-                                                                self.notifyObj.browser);
-          ok(notification != null, "Test 28: Notification remained when subframe navigated");
-          self.notifyObj.options.eventCallback = undefined;
-
-          notification.remove();
-          gBrowser.removeTabsProgressListener(progressListener);
-        },
-      };
-
-      info("Test 28: Adding progress listener and performing navigation");
-      gBrowser.addTabsProgressListener(progressListener);
-      content.document.getElementById("iframe")
-                      .setAttribute("src", "http://example.org/");
-    },
-    onHidden: function () {}
-  },
-  { // Test #29 - Popup Notifications should catch exceptions from callbacks
-    run: function () {
-      let callbackCount = 0;
-      this.testNotif1 = new basicNotification();
-      this.testNotif1.message += " 1";
-      this.notification1 = showNotification(this.testNotif1);
-      this.testNotif1.options.eventCallback = function (eventName) {
-        info("notifyObj1.options.eventCallback: " + eventName);
-        if (eventName == "dismissed") {
-          throw new Error("Oops 1!");
-          if (++callbackCount == 2) {
-            executeSoon(goNext);
-          }
-        }
-      };
-
-      this.testNotif2 = new basicNotification();
-      this.testNotif2.message += " 2";
-      this.testNotif2.id += "-2";
-      this.testNotif2.options.eventCallback = function (eventName) {
-        info("notifyObj2.options.eventCallback: " + eventName);
-        if (eventName == "dismissed") {
-          throw new Error("Oops 2!");
-          if (++callbackCount == 2) {
-            executeSoon(goNext);
-          }
-        }
-      };
-      this.notification2 = showNotification(this.testNotif2);
-    },
-    onShown: function (popup) {
-      is(popup.childNodes.length, 2, "two notifications are shown");
-      dismissNotification(popup);
-    },
-    onHidden: function () {
-      this.notification1.remove();
-      this.notification2.remove();
-    }
-  },
-  { // Test #30 - Popup Notifications main actions should catch exceptions from callbacks
-    run: function () {
-      this.testNotif = new errorNotification();
-      showNotification(this.testNotif);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.testNotif);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.testNotif.mainActionClicked, "main action has been triggered");
-    }
-  },
-  { // Test #31 - Popup Notifications secondary actions should catch exceptions from callbacks
-    run: function () {
-      this.testNotif = new errorNotification();
-      showNotification(this.testNotif);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.testNotif);
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.testNotif.secondaryActionClicked, "secondary action has been triggered");
-    }
-  },
-  { // Test #32 -  Existing popup notification shouldn't disappear when adding a dismissed notification
-    run: function () {
-      this.notifyObj1 = new basicNotification();
-      this.notifyObj1.id += "_1";
-      this.notifyObj1.anchorID = "default-notification-icon";
-      this.notification1 = showNotification(this.notifyObj1);
-    },
-    onShown: function (popup) {
-      // Now show a dismissed notification, and check that it doesn't clobber
-      // the showing one.
-      this.notifyObj2 = new basicNotification();
-      this.notifyObj2.id += "_2";
-      this.notifyObj2.anchorID = "geo-notification-icon";
-      this.notifyObj2.options.dismissed = true;
-      this.notification2 = showNotification(this.notifyObj2);
-
-      checkPopup(popup, this.notifyObj1);
-
-      // check that both anchor icons are showing
-      is(document.getElementById("default-notification-icon").getAttribute("showing"), "true",
-         "notification1 anchor should be visible");
-      is(document.getElementById("geo-notification-icon").getAttribute("showing"), "true",
-         "notification2 anchor should be visible");
-
-      dismissNotification(popup);
-    },
-    onHidden: function(popup) {
-      this.notification1.remove();
-      this.notification2.remove();
-    }
-  },
-  { // Test #33 - Showing should be able to modify the popup data
-    run: function() {
-      this.notifyObj = new basicNotification();
-      var normalCallback = this.notifyObj.options.eventCallback;
-      this.notifyObj.options.eventCallback = function (eventName) {
-        if (eventName == "showing") {
-          this.mainAction.label = "Alternate Label";
-        }
-        normalCallback.call(this, eventName);
-      };
-      showNotification(this.notifyObj);
-    },
-    onShown: function(popup) {
-      // checkPopup checks for the matching label. Note that this assumes that
-      // this.notifyObj.mainAction is the same as notification.mainAction,
-      // which could be a problem if we ever decided to deep-copy.
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function() { }
-  },
-  { // Test #34 - Moving a tab to a new window should remove non-swappable
-    // notifications.
-    run: function() {
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-      let notifyObj = new basicNotification();
-      showNotification(notifyObj);
-      let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
-      whenDelayedStartupFinished(win, function() {
-        let [tab] = win.gBrowser.tabs;
-        let anchor = win.document.getElementById("default-notification-icon");
-        win.PopupNotifications._reshowNotifications(anchor);
-        ok(win.PopupNotifications.panel.childNodes.length == 0,
-           "no notification displayed in new window");
-        ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
-        ok(notifyObj.removedCallbackTriggered, "the removed callback was triggered");
-        win.close();
-        goNext();
-      });
-    }
-  },
-  { // Test #35 - Moving a tab to a new window should preserve swappable notifications.
-    run: function() {
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-      let notifyObj = new basicNotification();
-      let originalCallback = notifyObj.options.eventCallback;
-      notifyObj.options.eventCallback = function (eventName) {
-        originalCallback(eventName);
-        return eventName == "swapping";
-      };
-
-      showNotification(notifyObj);
-      let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
-      whenDelayedStartupFinished(win, function() {
-        let [tab] = win.gBrowser.tabs;
-        let anchor = win.document.getElementById("default-notification-icon");
-        win.PopupNotifications._reshowNotifications(anchor);
-        checkPopup(win.PopupNotifications.panel, notifyObj);
-        ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
-        win.close();
-        goNext();
-      });
-    }
-  },
-  { // Test #36 - the hideNotNow option
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.options.hideNotNow = true;
-      this.notifyObj.mainAction.dismiss = true;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      // checkPopup verifies that the Not Now item is hidden, and that no separator is added.
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      this.notification.remove();
-    }
-  },
-  { // Test #37 - the main action callback can keep the notification.
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.mainAction.dismiss = true;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
-      ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
-      this.notification.remove();
-    }
-  },
-  { // Test #38 - a secondary action callback can keep the notification.
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.secondaryActions[0].dismiss = true;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
-      ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
-      this.notification.remove();
-    }
-  },
-  { // Test #39 - returning true in the showing callback should dismiss the notification.
-    run: function() {
-      let notifyObj = new basicNotification();
-      let originalCallback = notifyObj.options.eventCallback;
-      notifyObj.options.eventCallback = function (eventName) {
-        originalCallback(eventName);
-        return eventName == "showing";
-      };
-
-      let notification = showNotification(notifyObj);
-      ok(notifyObj.showingCallbackTriggered, "the showing callback was triggered");
-      ok(!notifyObj.shownCallbackTriggered, "the shown callback wasn't triggered");
-      notification.remove();
-      goNext();
-    }
   }
 ];
-
-function showNotification(notifyObj) {
-  return PopupNotifications.show(notifyObj.browser,
-                                 notifyObj.id,
-                                 notifyObj.message,
-                                 notifyObj.anchorID,
-                                 notifyObj.mainAction,
-                                 notifyObj.secondaryActions,
-                                 notifyObj.options);
-}
-
-function checkPopup(popup, notificationObj) {
-  info("[Test #" + gTestIndex + "] checking popup");
-
-  ok(notificationObj.showingCallbackTriggered, "showing callback was triggered");
-  ok(notificationObj.shownCallbackTriggered, "shown callback was triggered");
-
-  let notifications = popup.childNodes;
-  is(notifications.length, 1, "one notification displayed");
-  let notification = notifications[0];
-  if (!notification)
-    return;
-  let icon = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-icon");
-  if (notificationObj.id == "geolocation") {
-    isnot(icon.boxObject.width, 0, "icon for geo displayed");
-    is(popup.anchorNode.className, "notification-anchor-icon", "notification anchored to icon");
-  }
-  is(notification.getAttribute("label"), notificationObj.message, "message matches");
-  is(notification.id, notificationObj.id + "-notification", "id matches");
-  if (notificationObj.mainAction) {
-    is(notification.getAttribute("buttonlabel"), notificationObj.mainAction.label, "main action label matches");
-    is(notification.getAttribute("buttonaccesskey"), notificationObj.mainAction.accessKey, "main action accesskey matches");
-  }
-  let actualSecondaryActions = Array.filter(notification.childNodes,
-                                            function (child) child.nodeName == "menuitem");
-  let secondaryActions = notificationObj.secondaryActions || [];
-  let actualSecondaryActionsCount = actualSecondaryActions.length;
-  if (notificationObj.options.hideNotNow) {
-    is(notification.getAttribute("hidenotnow"), "true", "Not Now item hidden");
-    if (secondaryActions.length)
-      is(notification.lastChild.tagName, "menuitem", "no menuseparator");
-  }
-  else if (secondaryActions.length) {
-    is(notification.lastChild.tagName, "menuseparator", "menuseparator exists");
-  }
-  is(actualSecondaryActionsCount, secondaryActions.length, actualSecondaryActions.length + " secondary actions");
-  secondaryActions.forEach(function (a, i) {
-    is(actualSecondaryActions[i].getAttribute("label"), a.label, "label for secondary action " + i + " matches");
-    is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey, "accessKey for secondary action " + i + " matches");
-  });
-}
-
-function triggerMainCommand(popup) {
-  info("[Test #" + gTestIndex + "] triggering main command");
-  let notifications = popup.childNodes;
-  ok(notifications.length > 0, "at least one notification displayed");
-  let notification = notifications[0];
-
-  // 20, 10 so that the inner button is hit
-  EventUtils.synthesizeMouse(notification.button, 20, 10, {});
-}
-
-function triggerSecondaryCommand(popup, index) {
-  info("[Test #" + gTestIndex + "] triggering secondary command");
-  let notifications = popup.childNodes;
-  ok(notifications.length > 0, "at least one notification displayed");
-  let notification = notifications[0];
-
-  // Cancel the arrow panel slide-in transition (bug 767133) such that
-  // it won't interfere with us interacting with the dropdown.
-  document.getAnonymousNodes(popup)[0].style.transition = "none";
-
-  notification.button.focus();
-
-  popup.addEventListener("popupshown", function () {
-    popup.removeEventListener("popupshown", arguments.callee, false);
-
-    // Press down until the desired command is selected
-    for (let i = 0; i <= index; i++)
-      EventUtils.synthesizeKey("VK_DOWN", {});
-
-    // Activate
-    EventUtils.synthesizeKey("VK_RETURN", {});
-  }, false);
-
-  // One down event to open the popup
-  EventUtils.synthesizeKey("VK_DOWN", { altKey: !navigator.platform.contains("Mac") });
-}
-
-function loadURI(uri, callback) {
-  if (callback) {
-    gBrowser.addEventListener("load", function() {
-      // Ignore the about:blank load
-      if (gBrowser.currentURI.spec == "about:blank")
-        return;
-
-      gBrowser.removeEventListener("load", arguments.callee, true);
-
-      callback();
-    }, true);
-  }
-  gBrowser.loadURI(uri);
-}
-
-function dismissNotification(popup) {
-  info("[Test #" + gTestIndex + "] dismissing notification");
-  executeSoon(function () {
-    EventUtils.synthesizeKey("VK_ESCAPE", {});
-  });
-}
copy from browser/base/content/test/general/browser_popupNotification.js
copy to browser/base/content/test/popupNotifications/browser_popupNotification_3.js
--- a/browser/base/content/test/general/browser_popupNotification.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js
@@ -3,725 +3,116 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 function test() {
   waitForExplicitFinish();
 
   ok(PopupNotifications, "PopupNotifications object exists");
   ok(PopupNotifications.panel, "PopupNotifications panel exists");
 
-  // Disable transitions as they slow the test down and we want to click the
-  // mouse buttons in a predictable location.
-  PopupNotifications.transitionsEnabled = false;
-
-  registerCleanupFunction(cleanUp);
-
-  runNextTest();
-}
-
-function cleanUp() {
-  for (var topic in gActiveObservers)
-    Services.obs.removeObserver(gActiveObservers[topic], topic);
-  for (var eventName in gActiveListeners)
-    PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
-  PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
-  PopupNotifications.transitionsEnabled = true;
-}
-
-const PREF_SECURITY_DELAY_INITIAL = Services.prefs.getIntPref("security.notification_enable_delay");
-
-var gActiveListeners = {};
-var gActiveObservers = {};
-var gShownState = {};
-
-function goNext() {
-  if (++gTestIndex == tests.length)
-    executeSoon(finish);
-  else
-    executeSoon(runNextTest);
-}
-
-function runNextTest() {
-  let nextTest = tests[gTestIndex];
-
-  function addObserver(topic) {
-    function observer() {
-      Services.obs.removeObserver(observer, "PopupNotifications-" + topic);
-      delete gActiveObservers["PopupNotifications-" + topic];
-
-      info("[Test #" + gTestIndex + "] observer for " + topic + " called");
-      nextTest[topic]();
-      goNext();
-    }
-    Services.obs.addObserver(observer, "PopupNotifications-" + topic, false);
-    gActiveObservers["PopupNotifications-" + topic] = observer;
-  }
-
-  if (nextTest.backgroundShow) {
-    addObserver("backgroundShow");
-  } else if (nextTest.updateNotShowing) {
-    addObserver("updateNotShowing");
-  } else if (nextTest.onShown) {
-    doOnPopupEvent("popupshowing", function () {
-      info("[Test #" + gTestIndex + "] popup showing");
-    });
-    doOnPopupEvent("popupshown", function () {
-      gShownState[gTestIndex] = true;
-      info("[Test #" + gTestIndex + "] popup shown");
-      nextTest.onShown(this);
-    });
-
-    // We allow multiple onHidden functions to be defined in an array.  They're
-    // called in the order they appear.
-    let onHiddenArray = nextTest.onHidden instanceof Array ?
-                        nextTest.onHidden :
-                        [nextTest.onHidden];
-    doOnPopupEvent("popuphidden", function () {
-      if (!gShownState[gTestIndex]) {
-        // This is expected to happen for test 9, so let's not treat it as a failure.
-        info("Popup from test " + gTestIndex + " was hidden before its popupshown fired");
-      }
-
-      let onHidden = onHiddenArray.shift();
-      info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)");
-      executeSoon(function () {
-        onHidden.call(nextTest, this);
-        if (!onHiddenArray.length)
-          goNext();
-      }.bind(this));
-    }, onHiddenArray.length);
-    info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen);
-  }
-
-  info("[Test #" + gTestIndex + "] running test");
-  nextTest.run();
-}
-
-function doOnPopupEvent(eventName, callback, numExpected) {
-  gActiveListeners[eventName] = function (event) {
-    if (event.target != PopupNotifications.panel)
-      return;
-    if (typeof(numExpected) === "number")
-      numExpected--;
-    if (!numExpected) {
-      PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
-      delete gActiveListeners[eventName];
-    }
-
-    callback.call(PopupNotifications.panel);
-  }
-  PopupNotifications.panel.addEventListener(eventName, gActiveListeners[eventName], false);
-}
-
-var gTestIndex = 0;
-var gNewTab;
-
-function basicNotification() {
-  var self = this;
-  this.browser = gBrowser.selectedBrowser;
-  this.id = "test-notification-" + gTestIndex;
-  this.message = "This is popup notification " + this.id + " from test " + gTestIndex;
-  this.anchorID = null;
-  this.mainAction = {
-    label: "Main Action",
-    accessKey: "M",
-    callback: function () {
-      self.mainActionClicked = true;
-    }
-  };
-  this.secondaryActions = [
-    {
-      label: "Secondary Action",
-      accessKey: "S",
-      callback: function () {
-        self.secondaryActionClicked = true;
-      }
-    }
-  ];
-  this.options = {
-    eventCallback: function (eventName) {
-      switch (eventName) {
-        case "dismissed":
-          self.dismissalCallbackTriggered = true;
-          break;
-        case "showing":
-          self.showingCallbackTriggered = true;
-          break;
-        case "shown":
-          self.shownCallbackTriggered = true;
-          break;
-        case "removed":
-          self.removedCallbackTriggered = true;
-          break;
-        case "swapping":
-          self.swappingCallbackTriggered = true;
-          break;
-      }
-    }
-  };
-}
-
-basicNotification.prototype.addOptions = function(options) {
-  for (let [name, value] in Iterator(options))
-    this.options[name] = value;
-};
-
-function errorNotification() {
-  var self = this;
-  this.mainAction.callback = function () {
-    self.mainActionClicked = true;
-    throw new Error("Oops!");
-  };
-  this.secondaryActions[0].callback = function () {
-    self.secondaryActionClicked = true;
-    throw new Error("Oops!");
-  };
+  setup();
+  goNext();
 }
 
-errorNotification.prototype = new basicNotification();
-errorNotification.prototype.constructor = errorNotification;
-
-var wrongBrowserNotificationObject = new basicNotification();
-var wrongBrowserNotification;
-
-var tests = [
-  { // Test #0
-    run: function () {
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.mainActionClicked, "mainAction was clicked");
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  { // Test #1
-    run: function () {
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  { // Test #2
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // test opening a notification for a background browser
-  { // Test #3
-    run: function () {
-      gNewTab = gBrowser.addTab("about:blank");
-      isnot(gBrowser.selectedTab, gNewTab, "new tab isn't selected");
-      wrongBrowserNotificationObject.browser = gBrowser.getBrowserForTab(gNewTab);
-      wrongBrowserNotification = showNotification(wrongBrowserNotificationObject);
-    },
-    backgroundShow: function () {
-      is(PopupNotifications.isPanelOpen, false, "panel isn't open");
-      ok(!wrongBrowserNotificationObject.mainActionClicked, "main action wasn't clicked");
-      ok(!wrongBrowserNotificationObject.secondaryActionClicked, "secondary action wasn't clicked");
-      ok(!wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback wasn't called");
-    }
-  },
-  // now select that browser and test to see that the notification appeared
-  { // Test #4
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gNewTab;
-    },
-    onShown: function (popup) {
-      checkPopup(popup, wrongBrowserNotificationObject);
-      is(PopupNotifications.isPanelOpen, true, "isPanelOpen getter doesn't lie");
-
-      // switch back to the old browser
-      gBrowser.selectedTab = this.oldSelectedTab;
-    },
-    onHidden: function (popup) {
-      // actually remove the notification to prevent it from reappearing
-      ok(wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback triggered due to tab switch");
-      wrongBrowserNotification.remove();
-      ok(wrongBrowserNotificationObject.removedCallbackTriggered, "removed callback triggered");
-      wrongBrowserNotification = null;
-    }
-  },
-  // test that the removed notification isn't shown on browser re-select
-  { // Test #5
-    run: function () {
-      gBrowser.selectedTab = gNewTab;
-    },
-    updateNotShowing: function () {
-      is(PopupNotifications.isPanelOpen, false, "panel isn't open");
-      gBrowser.removeTab(gNewTab);
-    }
-  },
-  // Test that two notifications with the same ID result in a single displayed
-  // notification.
-  { // Test #6
-    run: function () {
-      this.notifyObj = new basicNotification();
-      // Show the same notification twice
-      this.notification1 = showNotification(this.notifyObj);
-      this.notification2 = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      this.notification2.remove();
-    },
-    onHidden: function (popup) {
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test that two notifications with different IDs are displayed
-  { // Test #7
-    run: function () {
-      this.testNotif1 = new basicNotification();
-      this.testNotif1.message += " 1";
-      showNotification(this.testNotif1);
-      this.testNotif2 = new basicNotification();
-      this.testNotif2.message += " 2";
-      this.testNotif2.id += "-2";
-      showNotification(this.testNotif2);
-    },
-    onShown: function (popup) {
-      is(popup.childNodes.length, 2, "two notifications are shown");
-      // Trigger the main command for the first notification, and the secondary
-      // for the second. Need to do mainCommand first since the secondaryCommand
-      // triggering is async.
-      triggerMainCommand(popup);
-      is(popup.childNodes.length, 1, "only one notification left");
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.testNotif1.mainActionClicked, "main action #1 was clicked");
-      ok(!this.testNotif1.secondaryActionClicked, "secondary action #1 wasn't clicked");
-      ok(!this.testNotif1.dismissalCallbackTriggered, "dismissal callback #1 wasn't called");
-
-      ok(!this.testNotif2.mainActionClicked, "main action #2 wasn't clicked");
-      ok(this.testNotif2.secondaryActionClicked, "secondary action #2 was clicked");
-      ok(!this.testNotif2.dismissalCallbackTriggered, "dismissal callback #2 wasn't called");
-    }
-  },
-  // Test notification without mainAction
-  { // Test #8
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.mainAction = null;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      this.notification.remove();
-    }
-  },
-  // Test two notifications with different anchors
-  { // Test #9
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.firstNotification = showNotification(this.notifyObj);
-      this.notifyObj2 = new basicNotification();
-      this.notifyObj2.id += "-2";
-      this.notifyObj2.anchorID = "addons-notification-icon";
-      // Second showNotification() overrides the first
-      this.secondNotification = showNotification(this.notifyObj2);
-    },
-    onShown: function (popup) {
-      // This also checks that only one element is shown.
-      checkPopup(popup, this.notifyObj2);
-      is(document.getElementById("geo-notification-icon").boxObject.width, 0,
-         "geo anchor shouldn't be visible");
-      dismissNotification(popup);
-    },
-    onHidden: [
-      // The second showing triggers a popuphidden event that we should ignore.
-      function (popup) {},
-      function (popup) {
-        // Remove the notifications
-        this.firstNotification.remove();
-        this.secondNotification.remove();
-        ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-        ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
-      }
-    ]
-  },
-  // Test optional params
-  { // Test #10
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.secondaryActions = undefined;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test that icons appear
-  { // Test #11
+let tests = [
+  // Test notification is removed when dismissed if removeOnDismissal is true
+  { id: "Test#1",
     run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.id = "geolocation";
-      this.notifyObj.anchorID = "geo-notification-icon";
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      let icon = document.getElementById("geo-notification-icon");
-      isnot(icon.boxObject.width, 0,
-            "geo anchor should be visible after dismissal");
-      this.notification.remove();
-      is(icon.boxObject.width, 0,
-         "geo anchor should not be visible after removal");
-    }
-  },
-  // Test that persistence allows the notification to persist across reloads
-  { // Test #12
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        self.notifyObj.addOptions({
-          persistence: 2
-        });
-        self.notification = showNotification(self.notifyObj);
-      });
-    },
-    onShown: function (popup) {
-      this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Next load will remove the notification
-          self.complete = true;
-
-          loadURI("http://example.org/");
-        });
-      });
-    },
-    onHidden: function (popup) {
-      ok(this.complete, "Should only have hidden the notification after 3 page loads");
-      ok(this.notifyObj.removedCallbackTriggered, "removal callback triggered");
-      gBrowser.removeTab(gBrowser.selectedTab);
-      gBrowser.selectedTab = this.oldSelectedTab;
-    }
-  },
-  // Test that a timeout allows the notification to persist across reloads
-  { // Test #13
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        // Set a timeout of 10 minutes that should never be hit
-        self.notifyObj.addOptions({
-          timeout: Date.now() + 600000
-        });
-        self.notification = showNotification(self.notifyObj);
-      });
-    },
-    onShown: function (popup) {
-      this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Next load will hide the notification
-          self.notification.options.timeout = Date.now() - 1;
-          self.complete = true;
-
-          loadURI("http://example.org/");
-        });
-      });
-    },
-    onHidden: function (popup) {
-      ok(this.complete, "Should only have hidden the notification after the timeout was passed");
-      this.notification.remove();
-      gBrowser.removeTab(gBrowser.selectedTab);
-      gBrowser.selectedTab = this.oldSelectedTab;
-    }
-  },
-  // Test that setting persistWhileVisible allows a visible notification to
-  // persist across location changes
-  { // Test #14
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        self.notifyObj.addOptions({
-          persistWhileVisible: true
-        });
-        self.notification = showNotification(self.notifyObj);
-      });
-    },
-    onShown: function (popup) {
-      this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Notification should persist across location changes
-          self.complete = true;
-          dismissNotification(popup);
-        });
-      });
-    },
-    onHidden: function (popup) {
-      ok(this.complete, "Should only have hidden the notification after it was dismissed");
-      this.notification.remove();
-      gBrowser.removeTab(gBrowser.selectedTab);
-      gBrowser.selectedTab = this.oldSelectedTab;
-    }
-  },
-  // Test that nested icon nodes correctly activate popups
-  { // Test #15
-    run: function() {
-      // Add a temporary box as the anchor with a button
-      this.box = document.createElement("box");
-      PopupNotifications.iconBox.appendChild(this.box);
-
-      let button = document.createElement("button");
-      button.setAttribute("label", "Please click me!");
-      this.box.appendChild(button);
-
-      // The notification should open up on the box
-      this.notifyObj = new basicNotification();
-      this.notifyObj.anchorID = this.box.id = "nested-box";
-      this.notifyObj.addOptions({dismissed: true});
-      this.notification = showNotification(this.notifyObj);
-
-      // This test places a normal button in the notification area, which has
-      // standard GTK styling and dimensions. Due to the clip-path, this button
-      // gets clipped off, which makes it necessary to synthesize the mouse click
-      // a little bit downward. To be safe, I adjusted the x-offset with the same
-      // amount.
-      EventUtils.synthesizeMouse(button, 4, 4, {});
-    },
-    onShown: function(popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function(popup) {
-      this.notification.remove();
-      this.box.parentNode.removeChild(this.box);
-    }
-  },
-  // Test that popupnotifications without popups have anchor icons shown
-  { // Test #16
-    run: function() {
-      let notifyObj = new basicNotification();
-      notifyObj.anchorID = "geo-notification-icon";
-      notifyObj.addOptions({neverShow: true});
-      showNotification(notifyObj);
-    },
-    updateNotShowing: function() {
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-    }
-  },
-  // Test notification "Not Now" menu item
-  { // Test #17
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerSecondaryCommand(popup, 1);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test notification close button
-  { // Test #18
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      let notification = popup.childNodes[0];
-      EventUtils.synthesizeMouseAtCenter(notification.closebutton, {});
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test notification when chrome is hidden
-  { // Test #19
-    run: function () {
-      window.locationbar.visible = false;
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-      window.locationbar.visible = true;
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      is(popup.anchorNode.className, "tabbrowser-tab", "notification anchored to tab");
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test notification is removed when dismissed if removeOnDismissal is true
-  { // Test #20
-    run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notifyObj.addOptions({
         removeOnDismissal: true
       });
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       dismissNotification(popup);
     },
     onHidden: function (popup) {
       ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
       ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
     }
   },
   // Test multiple notification icons are shown
-  { // Test #21
+  { id: "Test#2",
     run: function () {
-      this.notifyObj1 = new basicNotification();
+      this.notifyObj1 = new BasicNotification(this.id);
       this.notifyObj1.id += "_1";
       this.notifyObj1.anchorID = "default-notification-icon";
       this.notification1 = showNotification(this.notifyObj1);
 
-      this.notifyObj2 = new basicNotification();
+      this.notifyObj2 = new BasicNotification(this.id);
       this.notifyObj2.id += "_2";
       this.notifyObj2.anchorID = "geo-notification-icon";
       this.notification2 = showNotification(this.notifyObj2);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj2);
 
       // check notifyObj1 anchor icon is showing
       isnot(document.getElementById("default-notification-icon").boxObject.width, 0,
             "default anchor should be visible");
       // check notifyObj2 anchor icon is showing
       isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
             "geo anchor should be visible");
 
       dismissNotification(popup);
     },
-    onHidden: [
-      function (popup) {
-      },
-      function (popup) {
-        this.notification1.remove();
-        ok(this.notifyObj1.removedCallbackTriggered, "removed callback triggered");
+    onHidden: function (popup) {
+      this.notification1.remove();
+      ok(this.notifyObj1.removedCallbackTriggered, "removed callback triggered");
 
-        this.notification2.remove();
-        ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
-      }
-    ]
+      this.notification2.remove();
+      ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
+    }
   },
   // Test that multiple notification icons are removed when switching tabs
-  { // Test #22
+  { id: "Test#3",
     run: function () {
       // show the notification on old tab.
-      this.notifyObjOld = new basicNotification();
+      this.notifyObjOld = new BasicNotification(this.id);
       this.notifyObjOld.anchorID = "default-notification-icon";
       this.notificationOld = showNotification(this.notifyObjOld);
 
       // switch tab
       this.oldSelectedTab = gBrowser.selectedTab;
       gBrowser.selectedTab = gBrowser.addTab("about:blank");
 
       // show the notification on new tab.
-      this.notifyObjNew = new basicNotification();
+      this.notifyObjNew = new BasicNotification(this.id);
       this.notifyObjNew.anchorID = "geo-notification-icon";
       this.notificationNew = showNotification(this.notifyObjNew);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObjNew);
 
       // check notifyObjOld anchor icon is removed
       is(document.getElementById("default-notification-icon").boxObject.width, 0,
          "default anchor shouldn't be visible");
       // check notifyObjNew anchor icon is showing
       isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
             "geo anchor should be visible");
 
       dismissNotification(popup);
     },
-    onHidden: [
-      function (popup) {
-      },
-      function (popup) {
-        this.notificationNew.remove();
-        gBrowser.removeTab(gBrowser.selectedTab);
+    onHidden: function (popup) {
+      this.notificationNew.remove();
+      gBrowser.removeTab(gBrowser.selectedTab);
 
-        gBrowser.selectedTab = this.oldSelectedTab;
-        this.notificationOld.remove();
-      }
-    ]
+      gBrowser.selectedTab = this.oldSelectedTab;
+      this.notificationOld.remove();
+    }
   },
-  { // Test #23 - test security delay - too early
+  // test security delay - too early
+  { id: "Test#4",
     run: function () {
       // Set the security delay to 100s
       PopupNotifications.buttonDelay = 100000;
 
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       triggerMainCommand(popup);
 
       // Wait to see if the main command worked
       executeSoon(function delayedDismissal() {
@@ -729,22 +120,23 @@ var tests = [
       });
 
     },
     onHidden: function (popup) {
       ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked because it was too soon");
       ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
     }
   },
-  { // Test #24  - test security delay - after delay
+  // test security delay - after delay
+  { id: "Test#5",
     run: function () {
       // Set the security delay to 10ms
       PopupNotifications.buttonDelay = 10;
 
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
 
       // Wait until after the delay to trigger the main action
       setTimeout(function delayedDismissal() {
         triggerMainCommand(popup);
@@ -752,464 +144,167 @@ var tests = [
 
     },
     onHidden: function (popup) {
       ok(this.notifyObj.mainActionClicked, "mainAction was clicked after the delay");
       ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback was not triggered");
       PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
     }
   },
-  { // Test #25 - reload removes notification
-    run: function () {
-      loadURI("http://example.com/", function() {
-        let notifyObj = new basicNotification();
-        notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(true, "Notification removed in background tab after reloading");
-            executeSoon(function () {
-              goNext();
-            });
-          }
-        };
-        showNotification(notifyObj);
-        executeSoon(function () {
-          gBrowser.selectedBrowser.reload();
-        });
+  // reload removes notification
+  { id: "Test#6",
+    run: function* () {
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      let notifyObj = new BasicNotification(this.id);
+      notifyObj.options.eventCallback = function (eventName) {
+        if (eventName == "removed") {
+          ok(true, "Notification removed in background tab after reloading");
+          goNext();
+        }
+      };
+      showNotification(notifyObj);
+      executeSoon(function () {
+        gBrowser.selectedBrowser.reload();
       });
     }
   },
-  { // Test #26 - location change in background tab removes notification
-    run: function () {
+  // location change in background tab removes notification
+  { id: "Test#7",
+    run: function* () {
       let oldSelectedTab = gBrowser.selectedTab;
       let newTab = gBrowser.addTab("about:blank");
       gBrowser.selectedTab = newTab;
 
-      loadURI("http://example.com/", function() {
-        gBrowser.selectedTab = oldSelectedTab;
-        let browser = gBrowser.getBrowserForTab(newTab);
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      gBrowser.selectedTab = oldSelectedTab;
+      let browser = gBrowser.getBrowserForTab(newTab);
 
-        let notifyObj = new basicNotification();
-        notifyObj.browser = browser;
-        notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(true, "Notification removed in background tab after reloading");
-            executeSoon(function () {
-              gBrowser.removeTab(newTab);
-              goNext();
-            });
-          }
-        };
-        showNotification(notifyObj);
-        executeSoon(function () {
-          browser.reload();
-        });
+      let notifyObj = new BasicNotification(this.id);
+      notifyObj.browser = browser;
+      notifyObj.options.eventCallback = function (eventName) {
+        if (eventName == "removed") {
+          ok(true, "Notification removed in background tab after reloading");
+          executeSoon(function () {
+            gBrowser.removeTab(newTab);
+            goNext();
+          });
+        }
+      };
+      showNotification(notifyObj);
+      executeSoon(function () {
+        browser.reload();
       });
     }
   },
-  { // Test #27 -  Popup notification anchor shouldn't disappear when a notification with the same ID is re-added in a background tab
-    run: function () {
-      loadURI("http://example.com/", function () {
-        let originalTab = gBrowser.selectedTab;
-        let bgTab = gBrowser.addTab("about:blank");
-        gBrowser.selectedTab = bgTab;
-        loadURI("http://example.com/", function () {
-          let anchor = document.createElement("box");
-          anchor.id = "test26-anchor";
-          anchor.className = "notification-anchor-icon";
-          PopupNotifications.iconBox.appendChild(anchor);
+  // Popup notification anchor shouldn't disappear when a notification with the same ID is re-added in a background tab
+  { id: "Test#8",
+    run: function* () {
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      let originalTab = gBrowser.selectedTab;
+      let bgTab = gBrowser.addTab("about:blank");
+      gBrowser.selectedTab = bgTab;
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      let anchor = document.createElement("box");
+      anchor.id = "test26-anchor";
+      anchor.className = "notification-anchor-icon";
+      PopupNotifications.iconBox.appendChild(anchor);
 
-          gBrowser.selectedTab = originalTab;
+      gBrowser.selectedTab = originalTab;
 
-          let fgNotifyObj = new basicNotification();
-          fgNotifyObj.anchorID = anchor.id;
-          fgNotifyObj.options.dismissed = true;
-          let fgNotification = showNotification(fgNotifyObj);
+      let fgNotifyObj = new BasicNotification(this.id);
+      fgNotifyObj.anchorID = anchor.id;
+      fgNotifyObj.options.dismissed = true;
+      let fgNotification = showNotification(fgNotifyObj);
 
-          let bgNotifyObj = new basicNotification();
-          bgNotifyObj.anchorID = anchor.id;
-          bgNotifyObj.browser = gBrowser.getBrowserForTab(bgTab);
-          // show the notification in the background tab ...
-          let bgNotification = showNotification(bgNotifyObj);
-          // ... and re-show it
-          bgNotification = showNotification(bgNotifyObj);
+      let bgNotifyObj = new BasicNotification(this.id);
+      bgNotifyObj.anchorID = anchor.id;
+      bgNotifyObj.browser = gBrowser.getBrowserForTab(bgTab);
+      // show the notification in the background tab ...
+      let bgNotification = showNotification(bgNotifyObj);
+      // ... and re-show it
+      bgNotification = showNotification(bgNotifyObj);
 
-          ok(fgNotification.id, "notification has id");
-          is(fgNotification.id, bgNotification.id, "notification ids are the same");
-          is(anchor.getAttribute("showing"), "true", "anchor still showing");
+      ok(fgNotification.id, "notification has id");
+      is(fgNotification.id, bgNotification.id, "notification ids are the same");
+      is(anchor.getAttribute("showing"), "true", "anchor still showing");
 
-          fgNotification.remove();
-          gBrowser.removeTab(bgTab);
-          goNext();
-        });
-      });
+      fgNotification.remove();
+      gBrowser.removeTab(bgTab);
+      goNext();
     }
   },
-  { // Test #28 - location change in an embedded frame should not remove a notification
-    run: function () {
-      loadURI("data:text/html;charset=utf8,<iframe id='iframe' src='http://example.com/'>", function () {
-        this.notifyObj = new basicNotification();
-        this.notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(false, "Test 28: Notification removed from browser when subframe navigated");
-          }
-        };
-        showNotification(this.notifyObj);
-      }.bind(this));
+  // location change in an embedded frame should not remove a notification
+  { id: "Test#9",
+    run: function* () {
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html;charset=utf8,<iframe%20id='iframe'%20src='http://example.com/'>");
+      this.notifyObj = new BasicNotification(this.id);
+      this.notifyObj.options.eventCallback = function (eventName) {
+        if (eventName == "removed") {
+          ok(false, "Notification removed from browser when subframe navigated");
+        }
+      };
+      showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       let self = this;
       let progressListener = {
         onLocationChange: function onLocationChange(aBrowser) {
           if (aBrowser != gBrowser.selectedBrowser) {
             return;
           }
           let notification = PopupNotifications.getNotification(self.notifyObj.id,
                                                                 self.notifyObj.browser);
-          ok(notification != null, "Test 28: Notification remained when subframe navigated");
+          ok(notification != null, "Notification remained when subframe navigated");
           self.notifyObj.options.eventCallback = undefined;
 
           notification.remove();
           gBrowser.removeTabsProgressListener(progressListener);
         },
       };
 
-      info("Test 28: Adding progress listener and performing navigation");
+      info("Adding progress listener and performing navigation");
       gBrowser.addTabsProgressListener(progressListener);
       content.document.getElementById("iframe")
                       .setAttribute("src", "http://example.org/");
     },
     onHidden: function () {}
   },
-  { // Test #29 - Popup Notifications should catch exceptions from callbacks
+  // Popup Notifications should catch exceptions from callbacks
+  { id: "Test#10",
     run: function () {
       let callbackCount = 0;
-      this.testNotif1 = new basicNotification();
+      this.testNotif1 = new BasicNotification(this.id);
       this.testNotif1.message += " 1";
       this.notification1 = showNotification(this.testNotif1);
       this.testNotif1.options.eventCallback = function (eventName) {
         info("notifyObj1.options.eventCallback: " + eventName);
         if (eventName == "dismissed") {
           throw new Error("Oops 1!");
           if (++callbackCount == 2) {
-            executeSoon(goNext);
+            goNext();
           }
         }
       };
 
-      this.testNotif2 = new basicNotification();
+      this.testNotif2 = new BasicNotification(this.id);
       this.testNotif2.message += " 2";
       this.testNotif2.id += "-2";
       this.testNotif2.options.eventCallback = function (eventName) {
         info("notifyObj2.options.eventCallback: " + eventName);
         if (eventName == "dismissed") {
           throw new Error("Oops 2!");
           if (++callbackCount == 2) {
-            executeSoon(goNext);
+            goNext();
           }
         }
       };
       this.notification2 = showNotification(this.testNotif2);
     },
     onShown: function (popup) {
       is(popup.childNodes.length, 2, "two notifications are shown");
       dismissNotification(popup);
     },
     onHidden: function () {
       this.notification1.remove();
       this.notification2.remove();
     }
-  },
-  { // Test #30 - Popup Notifications main actions should catch exceptions from callbacks
-    run: function () {
-      this.testNotif = new errorNotification();
-      showNotification(this.testNotif);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.testNotif);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.testNotif.mainActionClicked, "main action has been triggered");
-    }
-  },
-  { // Test #31 - Popup Notifications secondary actions should catch exceptions from callbacks
-    run: function () {
-      this.testNotif = new errorNotification();
-      showNotification(this.testNotif);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.testNotif);
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.testNotif.secondaryActionClicked, "secondary action has been triggered");
-    }
-  },
-  { // Test #32 -  Existing popup notification shouldn't disappear when adding a dismissed notification
-    run: function () {
-      this.notifyObj1 = new basicNotification();
-      this.notifyObj1.id += "_1";
-      this.notifyObj1.anchorID = "default-notification-icon";
-      this.notification1 = showNotification(this.notifyObj1);
-    },
-    onShown: function (popup) {
-      // Now show a dismissed notification, and check that it doesn't clobber
-      // the showing one.
-      this.notifyObj2 = new basicNotification();
-      this.notifyObj2.id += "_2";
-      this.notifyObj2.anchorID = "geo-notification-icon";
-      this.notifyObj2.options.dismissed = true;
-      this.notification2 = showNotification(this.notifyObj2);
-
-      checkPopup(popup, this.notifyObj1);
-
-      // check that both anchor icons are showing
-      is(document.getElementById("default-notification-icon").getAttribute("showing"), "true",
-         "notification1 anchor should be visible");
-      is(document.getElementById("geo-notification-icon").getAttribute("showing"), "true",
-         "notification2 anchor should be visible");
-
-      dismissNotification(popup);
-    },
-    onHidden: function(popup) {
-      this.notification1.remove();
-      this.notification2.remove();
-    }
-  },
-  { // Test #33 - Showing should be able to modify the popup data
-    run: function() {
-      this.notifyObj = new basicNotification();
-      var normalCallback = this.notifyObj.options.eventCallback;
-      this.notifyObj.options.eventCallback = function (eventName) {
-        if (eventName == "showing") {
-          this.mainAction.label = "Alternate Label";
-        }
-        normalCallback.call(this, eventName);
-      };
-      showNotification(this.notifyObj);
-    },
-    onShown: function(popup) {
-      // checkPopup checks for the matching label. Note that this assumes that
-      // this.notifyObj.mainAction is the same as notification.mainAction,
-      // which could be a problem if we ever decided to deep-copy.
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function() { }
-  },
-  { // Test #34 - Moving a tab to a new window should remove non-swappable
-    // notifications.
-    run: function() {
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-      let notifyObj = new basicNotification();
-      showNotification(notifyObj);
-      let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
-      whenDelayedStartupFinished(win, function() {
-        let [tab] = win.gBrowser.tabs;
-        let anchor = win.document.getElementById("default-notification-icon");
-        win.PopupNotifications._reshowNotifications(anchor);
-        ok(win.PopupNotifications.panel.childNodes.length == 0,
-           "no notification displayed in new window");
-        ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
-        ok(notifyObj.removedCallbackTriggered, "the removed callback was triggered");
-        win.close();
-        goNext();
-      });
-    }
-  },
-  { // Test #35 - Moving a tab to a new window should preserve swappable notifications.
-    run: function() {
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-      let notifyObj = new basicNotification();
-      let originalCallback = notifyObj.options.eventCallback;
-      notifyObj.options.eventCallback = function (eventName) {
-        originalCallback(eventName);
-        return eventName == "swapping";
-      };
-
-      showNotification(notifyObj);
-      let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
-      whenDelayedStartupFinished(win, function() {
-        let [tab] = win.gBrowser.tabs;
-        let anchor = win.document.getElementById("default-notification-icon");
-        win.PopupNotifications._reshowNotifications(anchor);
-        checkPopup(win.PopupNotifications.panel, notifyObj);
-        ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
-        win.close();
-        goNext();
-      });
-    }
-  },
-  { // Test #36 - the hideNotNow option
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.options.hideNotNow = true;
-      this.notifyObj.mainAction.dismiss = true;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      // checkPopup verifies that the Not Now item is hidden, and that no separator is added.
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      this.notification.remove();
-    }
-  },
-  { // Test #37 - the main action callback can keep the notification.
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.mainAction.dismiss = true;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
-      ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
-      this.notification.remove();
-    }
-  },
-  { // Test #38 - a secondary action callback can keep the notification.
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.secondaryActions[0].dismiss = true;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
-      ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
-      this.notification.remove();
-    }
-  },
-  { // Test #39 - returning true in the showing callback should dismiss the notification.
-    run: function() {
-      let notifyObj = new basicNotification();
-      let originalCallback = notifyObj.options.eventCallback;
-      notifyObj.options.eventCallback = function (eventName) {
-        originalCallback(eventName);
-        return eventName == "showing";
-      };
-
-      let notification = showNotification(notifyObj);
-      ok(notifyObj.showingCallbackTriggered, "the showing callback was triggered");
-      ok(!notifyObj.shownCallbackTriggered, "the shown callback wasn't triggered");
-      notification.remove();
-      goNext();
-    }
   }
 ];
-
-function showNotification(notifyObj) {
-  return PopupNotifications.show(notifyObj.browser,
-                                 notifyObj.id,
-                                 notifyObj.message,
-                                 notifyObj.anchorID,
-                                 notifyObj.mainAction,
-                                 notifyObj.secondaryActions,
-                                 notifyObj.options);
-}
-
-function checkPopup(popup, notificationObj) {
-  info("[Test #" + gTestIndex + "] checking popup");
-
-  ok(notificationObj.showingCallbackTriggered, "showing callback was triggered");
-  ok(notificationObj.shownCallbackTriggered, "shown callback was triggered");
-
-  let notifications = popup.childNodes;
-  is(notifications.length, 1, "one notification displayed");
-  let notification = notifications[0];
-  if (!notification)
-    return;
-  let icon = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-icon");
-  if (notificationObj.id == "geolocation") {
-    isnot(icon.boxObject.width, 0, "icon for geo displayed");
-    is(popup.anchorNode.className, "notification-anchor-icon", "notification anchored to icon");
-  }
-  is(notification.getAttribute("label"), notificationObj.message, "message matches");
-  is(notification.id, notificationObj.id + "-notification", "id matches");
-  if (notificationObj.mainAction) {
-    is(notification.getAttribute("buttonlabel"), notificationObj.mainAction.label, "main action label matches");
-    is(notification.getAttribute("buttonaccesskey"), notificationObj.mainAction.accessKey, "main action accesskey matches");
-  }
-  let actualSecondaryActions = Array.filter(notification.childNodes,
-                                            function (child) child.nodeName == "menuitem");
-  let secondaryActions = notificationObj.secondaryActions || [];
-  let actualSecondaryActionsCount = actualSecondaryActions.length;
-  if (notificationObj.options.hideNotNow) {
-    is(notification.getAttribute("hidenotnow"), "true", "Not Now item hidden");
-    if (secondaryActions.length)
-      is(notification.lastChild.tagName, "menuitem", "no menuseparator");
-  }
-  else if (secondaryActions.length) {
-    is(notification.lastChild.tagName, "menuseparator", "menuseparator exists");
-  }
-  is(actualSecondaryActionsCount, secondaryActions.length, actualSecondaryActions.length + " secondary actions");
-  secondaryActions.forEach(function (a, i) {
-    is(actualSecondaryActions[i].getAttribute("label"), a.label, "label for secondary action " + i + " matches");
-    is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey, "accessKey for secondary action " + i + " matches");
-  });
-}
-
-function triggerMainCommand(popup) {
-  info("[Test #" + gTestIndex + "] triggering main command");
-  let notifications = popup.childNodes;
-  ok(notifications.length > 0, "at least one notification displayed");
-  let notification = notifications[0];
-
-  // 20, 10 so that the inner button is hit
-  EventUtils.synthesizeMouse(notification.button, 20, 10, {});
-}
-
-function triggerSecondaryCommand(popup, index) {
-  info("[Test #" + gTestIndex + "] triggering secondary command");
-  let notifications = popup.childNodes;
-  ok(notifications.length > 0, "at least one notification displayed");
-  let notification = notifications[0];
-
-  // Cancel the arrow panel slide-in transition (bug 767133) such that
-  // it won't interfere with us interacting with the dropdown.
-  document.getAnonymousNodes(popup)[0].style.transition = "none";
-
-  notification.button.focus();
-
-  popup.addEventListener("popupshown", function () {
-    popup.removeEventListener("popupshown", arguments.callee, false);
-
-    // Press down until the desired command is selected
-    for (let i = 0; i <= index; i++)
-      EventUtils.synthesizeKey("VK_DOWN", {});
-
-    // Activate
-    EventUtils.synthesizeKey("VK_RETURN", {});
-  }, false);
-
-  // One down event to open the popup
-  EventUtils.synthesizeKey("VK_DOWN", { altKey: !navigator.platform.contains("Mac") });
-}
-
-function loadURI(uri, callback) {
-  if (callback) {
-    gBrowser.addEventListener("load", function() {
-      // Ignore the about:blank load
-      if (gBrowser.currentURI.spec == "about:blank")
-        return;
-
-      gBrowser.removeEventListener("load", arguments.callee, true);
-
-      callback();
-    }, true);
-  }
-  gBrowser.loadURI(uri);
-}
-
-function dismissNotification(popup) {
-  info("[Test #" + gTestIndex + "] dismissing notification");
-  executeSoon(function () {
-    EventUtils.synthesizeKey("VK_ESCAPE", {});
-  });
-}
copy from browser/base/content/test/general/browser_popupNotification.js
copy to browser/base/content/test/popupNotifications/browser_popupNotification_4.js
--- a/browser/base/content/test/general/browser_popupNotification.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
@@ -1,964 +1,63 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
+ 
 function test() {
   waitForExplicitFinish();
 
   ok(PopupNotifications, "PopupNotifications object exists");
   ok(PopupNotifications.panel, "PopupNotifications panel exists");
 
-  // Disable transitions as they slow the test down and we want to click the
-  // mouse buttons in a predictable location.
-  PopupNotifications.transitionsEnabled = false;
-
-  registerCleanupFunction(cleanUp);
-
-  runNextTest();
-}
-
-function cleanUp() {
-  for (var topic in gActiveObservers)
-    Services.obs.removeObserver(gActiveObservers[topic], topic);
-  for (var eventName in gActiveListeners)
-    PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
-  PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
-  PopupNotifications.transitionsEnabled = true;
-}
-
-const PREF_SECURITY_DELAY_INITIAL = Services.prefs.getIntPref("security.notification_enable_delay");
-
-var gActiveListeners = {};
-var gActiveObservers = {};
-var gShownState = {};
-
-function goNext() {
-  if (++gTestIndex == tests.length)
-    executeSoon(finish);
-  else
-    executeSoon(runNextTest);
-}
-
-function runNextTest() {
-  let nextTest = tests[gTestIndex];
-
-  function addObserver(topic) {
-    function observer() {
-      Services.obs.removeObserver(observer, "PopupNotifications-" + topic);
-      delete gActiveObservers["PopupNotifications-" + topic];
-
-      info("[Test #" + gTestIndex + "] observer for " + topic + " called");
-      nextTest[topic]();
-      goNext();
-    }
-    Services.obs.addObserver(observer, "PopupNotifications-" + topic, false);
-    gActiveObservers["PopupNotifications-" + topic] = observer;
-  }
-
-  if (nextTest.backgroundShow) {
-    addObserver("backgroundShow");
-  } else if (nextTest.updateNotShowing) {
-    addObserver("updateNotShowing");
-  } else if (nextTest.onShown) {
-    doOnPopupEvent("popupshowing", function () {
-      info("[Test #" + gTestIndex + "] popup showing");
-    });
-    doOnPopupEvent("popupshown", function () {
-      gShownState[gTestIndex] = true;
-      info("[Test #" + gTestIndex + "] popup shown");
-      nextTest.onShown(this);
-    });
-
-    // We allow multiple onHidden functions to be defined in an array.  They're
-    // called in the order they appear.
-    let onHiddenArray = nextTest.onHidden instanceof Array ?
-                        nextTest.onHidden :
-                        [nextTest.onHidden];
-    doOnPopupEvent("popuphidden", function () {
-      if (!gShownState[gTestIndex]) {
-        // This is expected to happen for test 9, so let's not treat it as a failure.
-        info("Popup from test " + gTestIndex + " was hidden before its popupshown fired");
-      }
-
-      let onHidden = onHiddenArray.shift();
-      info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)");
-      executeSoon(function () {
-        onHidden.call(nextTest, this);
-        if (!onHiddenArray.length)
-          goNext();
-      }.bind(this));
-    }, onHiddenArray.length);
-    info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen);
-  }
-
-  info("[Test #" + gTestIndex + "] running test");
-  nextTest.run();
-}
-
-function doOnPopupEvent(eventName, callback, numExpected) {
-  gActiveListeners[eventName] = function (event) {
-    if (event.target != PopupNotifications.panel)
-      return;
-    if (typeof(numExpected) === "number")
-      numExpected--;
-    if (!numExpected) {
-      PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
-      delete gActiveListeners[eventName];
-    }
-
-    callback.call(PopupNotifications.panel);
-  }
-  PopupNotifications.panel.addEventListener(eventName, gActiveListeners[eventName], false);
-}
-
-var gTestIndex = 0;
-var gNewTab;
-
-function basicNotification() {
-  var self = this;
-  this.browser = gBrowser.selectedBrowser;
-  this.id = "test-notification-" + gTestIndex;
-  this.message = "This is popup notification " + this.id + " from test " + gTestIndex;
-  this.anchorID = null;
-  this.mainAction = {
-    label: "Main Action",
-    accessKey: "M",
-    callback: function () {
-      self.mainActionClicked = true;
-    }
-  };
-  this.secondaryActions = [
-    {
-      label: "Secondary Action",
-      accessKey: "S",
-      callback: function () {
-        self.secondaryActionClicked = true;
-      }
-    }
-  ];
-  this.options = {
-    eventCallback: function (eventName) {
-      switch (eventName) {
-        case "dismissed":
-          self.dismissalCallbackTriggered = true;
-          break;
-        case "showing":
-          self.showingCallbackTriggered = true;
-          break;
-        case "shown":
-          self.shownCallbackTriggered = true;
-          break;
-        case "removed":
-          self.removedCallbackTriggered = true;
-          break;
-        case "swapping":
-          self.swappingCallbackTriggered = true;
-          break;
-      }
-    }
-  };
-}
-
-basicNotification.prototype.addOptions = function(options) {
-  for (let [name, value] in Iterator(options))
-    this.options[name] = value;
-};
-
-function errorNotification() {
-  var self = this;
-  this.mainAction.callback = function () {
-    self.mainActionClicked = true;
-    throw new Error("Oops!");
-  };
-  this.secondaryActions[0].callback = function () {
-    self.secondaryActionClicked = true;
-    throw new Error("Oops!");
-  };
+  setup();
+  goNext();
 }
 
-errorNotification.prototype = new basicNotification();
-errorNotification.prototype.constructor = errorNotification;
-
-var wrongBrowserNotificationObject = new basicNotification();
-var wrongBrowserNotification;
-
-var tests = [
-  { // Test #0
-    run: function () {
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.mainActionClicked, "mainAction was clicked");
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  { // Test #1
-    run: function () {
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  { // Test #2
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // test opening a notification for a background browser
-  { // Test #3
-    run: function () {
-      gNewTab = gBrowser.addTab("about:blank");
-      isnot(gBrowser.selectedTab, gNewTab, "new tab isn't selected");
-      wrongBrowserNotificationObject.browser = gBrowser.getBrowserForTab(gNewTab);
-      wrongBrowserNotification = showNotification(wrongBrowserNotificationObject);
-    },
-    backgroundShow: function () {
-      is(PopupNotifications.isPanelOpen, false, "panel isn't open");
-      ok(!wrongBrowserNotificationObject.mainActionClicked, "main action wasn't clicked");
-      ok(!wrongBrowserNotificationObject.secondaryActionClicked, "secondary action wasn't clicked");
-      ok(!wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback wasn't called");
-    }
-  },
-  // now select that browser and test to see that the notification appeared
-  { // Test #4
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gNewTab;
-    },
-    onShown: function (popup) {
-      checkPopup(popup, wrongBrowserNotificationObject);
-      is(PopupNotifications.isPanelOpen, true, "isPanelOpen getter doesn't lie");
-
-      // switch back to the old browser
-      gBrowser.selectedTab = this.oldSelectedTab;
-    },
-    onHidden: function (popup) {
-      // actually remove the notification to prevent it from reappearing
-      ok(wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback triggered due to tab switch");
-      wrongBrowserNotification.remove();
-      ok(wrongBrowserNotificationObject.removedCallbackTriggered, "removed callback triggered");
-      wrongBrowserNotification = null;
-    }
-  },
-  // test that the removed notification isn't shown on browser re-select
-  { // Test #5
-    run: function () {
-      gBrowser.selectedTab = gNewTab;
-    },
-    updateNotShowing: function () {
-      is(PopupNotifications.isPanelOpen, false, "panel isn't open");
-      gBrowser.removeTab(gNewTab);
-    }
-  },
-  // Test that two notifications with the same ID result in a single displayed
-  // notification.
-  { // Test #6
-    run: function () {
-      this.notifyObj = new basicNotification();
-      // Show the same notification twice
-      this.notification1 = showNotification(this.notifyObj);
-      this.notification2 = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      this.notification2.remove();
-    },
-    onHidden: function (popup) {
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test that two notifications with different IDs are displayed
-  { // Test #7
-    run: function () {
-      this.testNotif1 = new basicNotification();
-      this.testNotif1.message += " 1";
-      showNotification(this.testNotif1);
-      this.testNotif2 = new basicNotification();
-      this.testNotif2.message += " 2";
-      this.testNotif2.id += "-2";
-      showNotification(this.testNotif2);
-    },
-    onShown: function (popup) {
-      is(popup.childNodes.length, 2, "two notifications are shown");
-      // Trigger the main command for the first notification, and the secondary
-      // for the second. Need to do mainCommand first since the secondaryCommand
-      // triggering is async.
-      triggerMainCommand(popup);
-      is(popup.childNodes.length, 1, "only one notification left");
-      triggerSecondaryCommand(popup, 0);
-    },
-    onHidden: function (popup) {
-      ok(this.testNotif1.mainActionClicked, "main action #1 was clicked");
-      ok(!this.testNotif1.secondaryActionClicked, "secondary action #1 wasn't clicked");
-      ok(!this.testNotif1.dismissalCallbackTriggered, "dismissal callback #1 wasn't called");
-
-      ok(!this.testNotif2.mainActionClicked, "main action #2 wasn't clicked");
-      ok(this.testNotif2.secondaryActionClicked, "secondary action #2 was clicked");
-      ok(!this.testNotif2.dismissalCallbackTriggered, "dismissal callback #2 wasn't called");
-    }
-  },
-  // Test notification without mainAction
-  { // Test #8
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.mainAction = null;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      this.notification.remove();
-    }
-  },
-  // Test two notifications with different anchors
-  { // Test #9
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.firstNotification = showNotification(this.notifyObj);
-      this.notifyObj2 = new basicNotification();
-      this.notifyObj2.id += "-2";
-      this.notifyObj2.anchorID = "addons-notification-icon";
-      // Second showNotification() overrides the first
-      this.secondNotification = showNotification(this.notifyObj2);
-    },
-    onShown: function (popup) {
-      // This also checks that only one element is shown.
-      checkPopup(popup, this.notifyObj2);
-      is(document.getElementById("geo-notification-icon").boxObject.width, 0,
-         "geo anchor shouldn't be visible");
-      dismissNotification(popup);
-    },
-    onHidden: [
-      // The second showing triggers a popuphidden event that we should ignore.
-      function (popup) {},
-      function (popup) {
-        // Remove the notifications
-        this.firstNotification.remove();
-        this.secondNotification.remove();
-        ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-        ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
-      }
-    ]
-  },
-  // Test optional params
-  { // Test #10
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.secondaryActions = undefined;
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test that icons appear
-  { // Test #11
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.id = "geolocation";
-      this.notifyObj.anchorID = "geo-notification-icon";
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      let icon = document.getElementById("geo-notification-icon");
-      isnot(icon.boxObject.width, 0,
-            "geo anchor should be visible after dismissal");
-      this.notification.remove();
-      is(icon.boxObject.width, 0,
-         "geo anchor should not be visible after removal");
-    }
-  },
-  // Test that persistence allows the notification to persist across reloads
-  { // Test #12
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        self.notifyObj.addOptions({
-          persistence: 2
-        });
-        self.notification = showNotification(self.notifyObj);
-      });
-    },
-    onShown: function (popup) {
-      this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Next load will remove the notification
-          self.complete = true;
-
-          loadURI("http://example.org/");
-        });
-      });
-    },
-    onHidden: function (popup) {
-      ok(this.complete, "Should only have hidden the notification after 3 page loads");
-      ok(this.notifyObj.removedCallbackTriggered, "removal callback triggered");
-      gBrowser.removeTab(gBrowser.selectedTab);
-      gBrowser.selectedTab = this.oldSelectedTab;
-    }
-  },
-  // Test that a timeout allows the notification to persist across reloads
-  { // Test #13
-    run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        // Set a timeout of 10 minutes that should never be hit
-        self.notifyObj.addOptions({
-          timeout: Date.now() + 600000
-        });
-        self.notification = showNotification(self.notifyObj);
-      });
-    },
-    onShown: function (popup) {
-      this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Next load will hide the notification
-          self.notification.options.timeout = Date.now() - 1;
-          self.complete = true;
-
-          loadURI("http://example.org/");
-        });
-      });
-    },
-    onHidden: function (popup) {
-      ok(this.complete, "Should only have hidden the notification after the timeout was passed");
-      this.notification.remove();
-      gBrowser.removeTab(gBrowser.selectedTab);
-      gBrowser.selectedTab = this.oldSelectedTab;
-    }
-  },
-  // Test that setting persistWhileVisible allows a visible notification to
-  // persist across location changes
-  { // Test #14
+let tests = [
+  // Popup Notifications main actions should catch exceptions from callbacks
+  { id: "Test#1",
     run: function () {
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      let self = this;
-      loadURI("http://example.com/", function() {
-        self.notifyObj = new basicNotification();
-        self.notifyObj.addOptions({
-          persistWhileVisible: true
-        });
-        self.notification = showNotification(self.notifyObj);
-      });
-    },
-    onShown: function (popup) {
-      this.complete = false;
-
-      let self = this;
-      loadURI("http://example.org/", function() {
-        loadURI("http://example.com/", function() {
-
-          // Notification should persist across location changes
-          self.complete = true;
-          dismissNotification(popup);
-        });
-      });
-    },
-    onHidden: function (popup) {
-      ok(this.complete, "Should only have hidden the notification after it was dismissed");
-      this.notification.remove();
-      gBrowser.removeTab(gBrowser.selectedTab);
-      gBrowser.selectedTab = this.oldSelectedTab;
-    }
-  },
-  // Test that nested icon nodes correctly activate popups
-  { // Test #15
-    run: function() {
-      // Add a temporary box as the anchor with a button
-      this.box = document.createElement("box");
-      PopupNotifications.iconBox.appendChild(this.box);
-
-      let button = document.createElement("button");
-      button.setAttribute("label", "Please click me!");
-      this.box.appendChild(button);
-
-      // The notification should open up on the box
-      this.notifyObj = new basicNotification();
-      this.notifyObj.anchorID = this.box.id = "nested-box";
-      this.notifyObj.addOptions({dismissed: true});
-      this.notification = showNotification(this.notifyObj);
-
-      // This test places a normal button in the notification area, which has
-      // standard GTK styling and dimensions. Due to the clip-path, this button
-      // gets clipped off, which makes it necessary to synthesize the mouse click
-      // a little bit downward. To be safe, I adjusted the x-offset with the same
-      // amount.
-      EventUtils.synthesizeMouse(button, 4, 4, {});
-    },
-    onShown: function(popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function(popup) {
-      this.notification.remove();
-      this.box.parentNode.removeChild(this.box);
-    }
-  },
-  // Test that popupnotifications without popups have anchor icons shown
-  { // Test #16
-    run: function() {
-      let notifyObj = new basicNotification();
-      notifyObj.anchorID = "geo-notification-icon";
-      notifyObj.addOptions({neverShow: true});
-      showNotification(notifyObj);
-    },
-    updateNotShowing: function() {
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-    }
-  },
-  // Test notification "Not Now" menu item
-  { // Test #17
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerSecondaryCommand(popup, 1);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test notification close button
-  { // Test #18
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      let notification = popup.childNodes[0];
-      EventUtils.synthesizeMouseAtCenter(notification.closebutton, {});
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test notification when chrome is hidden
-  { // Test #19
-    run: function () {
-      window.locationbar.visible = false;
-      this.notifyObj = new basicNotification();
-      this.notification = showNotification(this.notifyObj);
-      window.locationbar.visible = true;
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      is(popup.anchorNode.className, "tabbrowser-tab", "notification anchored to tab");
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
-      this.notification.remove();
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test notification is removed when dismissed if removeOnDismissal is true
-  { // Test #20
-    run: function () {
-      this.notifyObj = new basicNotification();
-      this.notifyObj.addOptions({
-        removeOnDismissal: true
-      });
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
-    },
-    onHidden: function (popup) {
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
-      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
-    }
-  },
-  // Test multiple notification icons are shown
-  { // Test #21
-    run: function () {
-      this.notifyObj1 = new basicNotification();
-      this.notifyObj1.id += "_1";
-      this.notifyObj1.anchorID = "default-notification-icon";
-      this.notification1 = showNotification(this.notifyObj1);
-
-      this.notifyObj2 = new basicNotification();
-      this.notifyObj2.id += "_2";
-      this.notifyObj2.anchorID = "geo-notification-icon";
-      this.notification2 = showNotification(this.notifyObj2);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj2);
-
-      // check notifyObj1 anchor icon is showing
-      isnot(document.getElementById("default-notification-icon").boxObject.width, 0,
-            "default anchor should be visible");
-      // check notifyObj2 anchor icon is showing
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-
-      dismissNotification(popup);
-    },
-    onHidden: [
-      function (popup) {
-      },
-      function (popup) {
-        this.notification1.remove();
-        ok(this.notifyObj1.removedCallbackTriggered, "removed callback triggered");
-
-        this.notification2.remove();
-        ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
-      }
-    ]
-  },
-  // Test that multiple notification icons are removed when switching tabs
-  { // Test #22
-    run: function () {
-      // show the notification on old tab.
-      this.notifyObjOld = new basicNotification();
-      this.notifyObjOld.anchorID = "default-notification-icon";
-      this.notificationOld = showNotification(this.notifyObjOld);
-
-      // switch tab
-      this.oldSelectedTab = gBrowser.selectedTab;
-      gBrowser.selectedTab = gBrowser.addTab("about:blank");
-
-      // show the notification on new tab.
-      this.notifyObjNew = new basicNotification();
-      this.notifyObjNew.anchorID = "geo-notification-icon";
-      this.notificationNew = showNotification(this.notifyObjNew);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObjNew);
-
-      // check notifyObjOld anchor icon is removed
-      is(document.getElementById("default-notification-icon").boxObject.width, 0,
-         "default anchor shouldn't be visible");
-      // check notifyObjNew anchor icon is showing
-      isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
-            "geo anchor should be visible");
-
-      dismissNotification(popup);
-    },
-    onHidden: [
-      function (popup) {
-      },
-      function (popup) {
-        this.notificationNew.remove();
-        gBrowser.removeTab(gBrowser.selectedTab);
-
-        gBrowser.selectedTab = this.oldSelectedTab;
-        this.notificationOld.remove();
-      }
-    ]
-  },
-  { // Test #23 - test security delay - too early
-    run: function () {
-      // Set the security delay to 100s
-      PopupNotifications.buttonDelay = 100000;
-
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-      triggerMainCommand(popup);
-
-      // Wait to see if the main command worked
-      executeSoon(function delayedDismissal() {
-        dismissNotification(popup);
-      });
-
-    },
-    onHidden: function (popup) {
-      ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked because it was too soon");
-      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
-    }
-  },
-  { // Test #24  - test security delay - after delay
-    run: function () {
-      // Set the security delay to 10ms
-      PopupNotifications.buttonDelay = 10;
-
-      this.notifyObj = new basicNotification();
-      showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-
-      // Wait until after the delay to trigger the main action
-      setTimeout(function delayedDismissal() {
-        triggerMainCommand(popup);
-      }, 500);
-
-    },
-    onHidden: function (popup) {
-      ok(this.notifyObj.mainActionClicked, "mainAction was clicked after the delay");
-      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback was not triggered");
-      PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
-    }
-  },
-  { // Test #25 - reload removes notification
-    run: function () {
-      loadURI("http://example.com/", function() {
-        let notifyObj = new basicNotification();
-        notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(true, "Notification removed in background tab after reloading");
-            executeSoon(function () {
-              goNext();
-            });
-          }
-        };
-        showNotification(notifyObj);
-        executeSoon(function () {
-          gBrowser.selectedBrowser.reload();
-        });
-      });
-    }
-  },
-  { // Test #26 - location change in background tab removes notification
-    run: function () {
-      let oldSelectedTab = gBrowser.selectedTab;
-      let newTab = gBrowser.addTab("about:blank");
-      gBrowser.selectedTab = newTab;
-
-      loadURI("http://example.com/", function() {
-        gBrowser.selectedTab = oldSelectedTab;
-        let browser = gBrowser.getBrowserForTab(newTab);
-
-        let notifyObj = new basicNotification();
-        notifyObj.browser = browser;
-        notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(true, "Notification removed in background tab after reloading");
-            executeSoon(function () {
-              gBrowser.removeTab(newTab);
-              goNext();
-            });
-          }
-        };
-        showNotification(notifyObj);
-        executeSoon(function () {
-          browser.reload();
-        });
-      });
-    }
-  },
-  { // Test #27 -  Popup notification anchor shouldn't disappear when a notification with the same ID is re-added in a background tab
-    run: function () {
-      loadURI("http://example.com/", function () {
-        let originalTab = gBrowser.selectedTab;
-        let bgTab = gBrowser.addTab("about:blank");
-        gBrowser.selectedTab = bgTab;
-        loadURI("http://example.com/", function () {
-          let anchor = document.createElement("box");
-          anchor.id = "test26-anchor";
-          anchor.className = "notification-anchor-icon";
-          PopupNotifications.iconBox.appendChild(anchor);
-
-          gBrowser.selectedTab = originalTab;
-
-          let fgNotifyObj = new basicNotification();
-          fgNotifyObj.anchorID = anchor.id;
-          fgNotifyObj.options.dismissed = true;
-          let fgNotification = showNotification(fgNotifyObj);
-
-          let bgNotifyObj = new basicNotification();
-          bgNotifyObj.anchorID = anchor.id;
-          bgNotifyObj.browser = gBrowser.getBrowserForTab(bgTab);
-          // show the notification in the background tab ...
-          let bgNotification = showNotification(bgNotifyObj);
-          // ... and re-show it
-          bgNotification = showNotification(bgNotifyObj);
-
-          ok(fgNotification.id, "notification has id");
-          is(fgNotification.id, bgNotification.id, "notification ids are the same");
-          is(anchor.getAttribute("showing"), "true", "anchor still showing");
-
-          fgNotification.remove();
-          gBrowser.removeTab(bgTab);
-          goNext();
-        });
-      });
-    }
-  },
-  { // Test #28 - location change in an embedded frame should not remove a notification
-    run: function () {
-      loadURI("data:text/html;charset=utf8,<iframe id='iframe' src='http://example.com/'>", function () {
-        this.notifyObj = new basicNotification();
-        this.notifyObj.options.eventCallback = function (eventName) {
-          if (eventName == "removed") {
-            ok(false, "Test 28: Notification removed from browser when subframe navigated");
-          }
-        };
-        showNotification(this.notifyObj);
-      }.bind(this));
-    },
-    onShown: function (popup) {
-      let self = this;
-      let progressListener = {
-        onLocationChange: function onLocationChange(aBrowser) {
-          if (aBrowser != gBrowser.selectedBrowser) {
-            return;
-          }
-          let notification = PopupNotifications.getNotification(self.notifyObj.id,
-                                                                self.notifyObj.browser);
-          ok(notification != null, "Test 28: Notification remained when subframe navigated");
-          self.notifyObj.options.eventCallback = undefined;
-
-          notification.remove();
-          gBrowser.removeTabsProgressListener(progressListener);
-        },
-      };
-
-      info("Test 28: Adding progress listener and performing navigation");
-      gBrowser.addTabsProgressListener(progressListener);
-      content.document.getElementById("iframe")
-                      .setAttribute("src", "http://example.org/");
-    },
-    onHidden: function () {}
-  },
-  { // Test #29 - Popup Notifications should catch exceptions from callbacks
-    run: function () {
-      let callbackCount = 0;
-      this.testNotif1 = new basicNotification();
-      this.testNotif1.message += " 1";
-      this.notification1 = showNotification(this.testNotif1);
-      this.testNotif1.options.eventCallback = function (eventName) {
-        info("notifyObj1.options.eventCallback: " + eventName);
-        if (eventName == "dismissed") {
-          throw new Error("Oops 1!");
-          if (++callbackCount == 2) {
-            executeSoon(goNext);
-          }
-        }
-      };
-
-      this.testNotif2 = new basicNotification();
-      this.testNotif2.message += " 2";
-      this.testNotif2.id += "-2";
-      this.testNotif2.options.eventCallback = function (eventName) {
-        info("notifyObj2.options.eventCallback: " + eventName);
-        if (eventName == "dismissed") {
-          throw new Error("Oops 2!");
-          if (++callbackCount == 2) {
-            executeSoon(goNext);
-          }
-        }
-      };
-      this.notification2 = showNotification(this.testNotif2);
-    },
-    onShown: function (popup) {
-      is(popup.childNodes.length, 2, "two notifications are shown");
-      dismissNotification(popup);
-    },
-    onHidden: function () {
-      this.notification1.remove();
-      this.notification2.remove();
-    }
-  },
-  { // Test #30 - Popup Notifications main actions should catch exceptions from callbacks
-    run: function () {
-      this.testNotif = new errorNotification();
+      this.testNotif = new ErrorNotification();
       showNotification(this.testNotif);
     },
     onShown: function (popup) {
       checkPopup(popup, this.testNotif);
       triggerMainCommand(popup);
     },
     onHidden: function (popup) {
       ok(this.testNotif.mainActionClicked, "main action has been triggered");
     }
   },
-  { // Test #31 - Popup Notifications secondary actions should catch exceptions from callbacks
+  // Popup Notifications secondary actions should catch exceptions from callbacks
+  { id: "Test#2",
     run: function () {
-      this.testNotif = new errorNotification();
+      this.testNotif = new ErrorNotification();
       showNotification(this.testNotif);
     },
     onShown: function (popup) {
       checkPopup(popup, this.testNotif);
       triggerSecondaryCommand(popup, 0);
     },
     onHidden: function (popup) {
       ok(this.testNotif.secondaryActionClicked, "secondary action has been triggered");
     }
   },
-  { // Test #32 -  Existing popup notification shouldn't disappear when adding a dismissed notification
+  // Existing popup notification shouldn't disappear when adding a dismissed notification
+  { id: "Test#3",
     run: function () {
-      this.notifyObj1 = new basicNotification();
+      this.notifyObj1 = new BasicNotification(this.id);
       this.notifyObj1.id += "_1";
       this.notifyObj1.anchorID = "default-notification-icon";
       this.notification1 = showNotification(this.notifyObj1);
     },
     onShown: function (popup) {
       // Now show a dismissed notification, and check that it doesn't clobber
       // the showing one.
-      this.notifyObj2 = new basicNotification();
+      this.notifyObj2 = new BasicNotification(this.id);
       this.notifyObj2.id += "_2";
       this.notifyObj2.anchorID = "geo-notification-icon";
       this.notifyObj2.options.dismissed = true;
       this.notification2 = showNotification(this.notifyObj2);
 
       checkPopup(popup, this.notifyObj1);
 
       // check that both anchor icons are showing
@@ -969,20 +68,21 @@ var tests = [
 
       dismissNotification(popup);
     },
     onHidden: function(popup) {
       this.notification1.remove();
       this.notification2.remove();
     }
   },
-  { // Test #33 - Showing should be able to modify the popup data
+  // Showing should be able to modify the popup data
+  { id: "Test#4",
     run: function() {
-      this.notifyObj = new basicNotification();
-      var normalCallback = this.notifyObj.options.eventCallback;
+      this.notifyObj = new BasicNotification(this.id);
+      let normalCallback = this.notifyObj.options.eventCallback;
       this.notifyObj.options.eventCallback = function (eventName) {
         if (eventName == "showing") {
           this.mainAction.label = "Alternate Label";
         }
         normalCallback.call(this, eventName);
       };
       showNotification(this.notifyObj);
     },
@@ -990,40 +90,41 @@ var tests = [
       // checkPopup checks for the matching label. Note that this assumes that
       // this.notifyObj.mainAction is the same as notification.mainAction,
       // which could be a problem if we ever decided to deep-copy.
       checkPopup(popup, this.notifyObj);
       triggerMainCommand(popup);
     },
     onHidden: function() { }
   },
-  { // Test #34 - Moving a tab to a new window should remove non-swappable
-    // notifications.
+  // Moving a tab to a new window should remove non-swappable notifications.
+  { id: "Test#5",
     run: function() {
       gBrowser.selectedTab = gBrowser.addTab("about:blank");
-      let notifyObj = new basicNotification();
+      let notifyObj = new BasicNotification(this.id);
       showNotification(notifyObj);
       let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
       whenDelayedStartupFinished(win, function() {
         let [tab] = win.gBrowser.tabs;
         let anchor = win.document.getElementById("default-notification-icon");
         win.PopupNotifications._reshowNotifications(anchor);
         ok(win.PopupNotifications.panel.childNodes.length == 0,
            "no notification displayed in new window");
         ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
         ok(notifyObj.removedCallbackTriggered, "the removed callback was triggered");
         win.close();
         goNext();
       });
     }
   },
-  { // Test #35 - Moving a tab to a new window should preserve swappable notifications.
+  // Moving a tab to a new window should preserve swappable notifications.
+  { id: "Test#6",
     run: function() {
       gBrowser.selectedTab = gBrowser.addTab("about:blank");
-      let notifyObj = new basicNotification();
+      let notifyObj = new BasicNotification(this.id);
       let originalCallback = notifyObj.options.eventCallback;
       notifyObj.options.eventCallback = function (eventName) {
         originalCallback(eventName);
         return eventName == "swapping";
       };
 
       showNotification(notifyObj);
       let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
@@ -1033,183 +134,77 @@ var tests = [
         win.PopupNotifications._reshowNotifications(anchor);
         checkPopup(win.PopupNotifications.panel, notifyObj);
         ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
         win.close();
         goNext();
       });
     }
   },
-  { // Test #36 - the hideNotNow option
+  // the hideNotNow option
+  { id: "Test#7",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notifyObj.options.hideNotNow = true;
       this.notifyObj.mainAction.dismiss = true;
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       // checkPopup verifies that the Not Now item is hidden, and that no separator is added.
       checkPopup(popup, this.notifyObj);
       triggerMainCommand(popup);
     },
     onHidden: function (popup) {
       this.notification.remove();
     }
   },
-  { // Test #37 - the main action callback can keep the notification.
+  // the main action callback can keep the notification.
+  { id: "Test#8",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notifyObj.mainAction.dismiss = true;
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       triggerMainCommand(popup);
     },
     onHidden: function (popup) {
       ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
       ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
       this.notification.remove();
     }
   },
-  { // Test #38 - a secondary action callback can keep the notification.
+  // a secondary action callback can keep the notification.
+  { id: "Test#9",
     run: function () {
-      this.notifyObj = new basicNotification();
+      this.notifyObj = new BasicNotification(this.id);
       this.notifyObj.secondaryActions[0].dismiss = true;
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj);
       triggerSecondaryCommand(popup, 0);
     },
     onHidden: function (popup) {
       ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
       ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
       this.notification.remove();
     }
   },
-  { // Test #39 - returning true in the showing callback should dismiss the notification.
+  // returning true in the showing callback should dismiss the notification.
+  { id: "Test#10",
     run: function() {
-      let notifyObj = new basicNotification();
+      let notifyObj = new BasicNotification(this.id);
       let originalCallback = notifyObj.options.eventCallback;
       notifyObj.options.eventCallback = function (eventName) {
         originalCallback(eventName);
         return eventName == "showing";
       };
 
       let notification = showNotification(notifyObj);
       ok(notifyObj.showingCallbackTriggered, "the showing callback was triggered");
       ok(!notifyObj.shownCallbackTriggered, "the shown callback wasn't triggered");
       notification.remove();
       goNext();
     }
   }
 ];
-
-function showNotification(notifyObj) {
-  return PopupNotifications.show(notifyObj.browser,
-                                 notifyObj.id,
-                                 notifyObj.message,
-                                 notifyObj.anchorID,
-                                 notifyObj.mainAction,
-                                 notifyObj.secondaryActions,
-                                 notifyObj.options);
-}
-
-function checkPopup(popup, notificationObj) {
-  info("[Test #" + gTestIndex + "] checking popup");
-
-  ok(notificationObj.showingCallbackTriggered, "showing callback was triggered");
-  ok(notificationObj.shownCallbackTriggered, "shown callback was triggered");
-
-  let notifications = popup.childNodes;
-  is(notifications.length, 1, "one notification displayed");
-  let notification = notifications[0];
-  if (!notification)
-    return;
-  let icon = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-icon");
-  if (notificationObj.id == "geolocation") {
-    isnot(icon.boxObject.width, 0, "icon for geo displayed");
-    is(popup.anchorNode.className, "notification-anchor-icon", "notification anchored to icon");
-  }
-  is(notification.getAttribute("label"), notificationObj.message, "message matches");
-  is(notification.id, notificationObj.id + "-notification", "id matches");
-  if (notificationObj.mainAction) {
-    is(notification.getAttribute("buttonlabel"), notificationObj.mainAction.label, "main action label matches");
-    is(notification.getAttribute("buttonaccesskey"), notificationObj.mainAction.accessKey, "main action accesskey matches");
-  }
-  let actualSecondaryActions = Array.filter(notification.childNodes,
-                                            function (child) child.nodeName == "menuitem");
-  let secondaryActions = notificationObj.secondaryActions || [];
-  let actualSecondaryActionsCount = actualSecondaryActions.length;
-  if (notificationObj.options.hideNotNow) {
-    is(notification.getAttribute("hidenotnow"), "true", "Not Now item hidden");
-    if (secondaryActions.length)
-      is(notification.lastChild.tagName, "menuitem", "no menuseparator");
-  }
-  else if (secondaryActions.length) {
-    is(notification.lastChild.tagName, "menuseparator", "menuseparator exists");
-  }
-  is(actualSecondaryActionsCount, secondaryActions.length, actualSecondaryActions.length + " secondary actions");
-  secondaryActions.forEach(function (a, i) {
-    is(actualSecondaryActions[i].getAttribute("label"), a.label, "label for secondary action " + i + " matches");
-    is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey, "accessKey for secondary action " + i + " matches");
-  });
-}
-
-function triggerMainCommand(popup) {
-  info("[Test #" + gTestIndex + "] triggering main command");
-  let notifications = popup.childNodes;
-  ok(notifications.length > 0, "at least one notification displayed");
-  let notification = notifications[0];
-
-  // 20, 10 so that the inner button is hit
-  EventUtils.synthesizeMouse(notification.button, 20, 10, {});
-}
-
-function triggerSecondaryCommand(popup, index) {
-  info("[Test #" + gTestIndex + "] triggering secondary command");
-  let notifications = popup.childNodes;
-  ok(notifications.length > 0, "at least one notification displayed");
-  let notification = notifications[0];
-
-  // Cancel the arrow panel slide-in transition (bug 767133) such that
-  // it won't interfere with us interacting with the dropdown.
-  document.getAnonymousNodes(popup)[0].style.transition = "none";
-
-  notification.button.focus();
-
-  popup.addEventListener("popupshown", function () {
-    popup.removeEventListener("popupshown", arguments.callee, false);
-
-    // Press down until the desired command is selected
-    for (let i = 0; i <= index; i++)
-      EventUtils.synthesizeKey("VK_DOWN", {});
-
-    // Activate
-    EventUtils.synthesizeKey("VK_RETURN", {});
-  }, false);
-
-  // One down event to open the popup
-  EventUtils.synthesizeKey("VK_DOWN", { altKey: !navigator.platform.contains("Mac") });
-}
-
-function loadURI(uri, callback) {
-  if (callback) {
-    gBrowser.addEventListener("load", function() {
-      // Ignore the about:blank load
-      if (gBrowser.currentURI.spec == "about:blank")
-        return;
-
-      gBrowser.removeEventListener("load", arguments.callee, true);
-
-      callback();
-    }, true);
-  }
-  gBrowser.loadURI(uri);
-}
-
-function dismissNotification(popup) {
-  info("[Test #" + gTestIndex + "] dismissing notification");
-  executeSoon(function () {
-    EventUtils.synthesizeKey("VK_ESCAPE", {});
-  });
-}
copy from browser/base/content/test/general/head.js
copy to browser/base/content/test/popupNotifications/head.js
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/popupNotifications/head.js
@@ -11,464 +11,303 @@ function whenDelayedStartupFinished(aWin
   Services.obs.addObserver(function observer(aSubject, aTopic) {
     if (aWindow == aSubject) {
       Services.obs.removeObserver(observer, aTopic);
       executeSoon(aCallback);
     }
   }, "browser-delayed-startup-finished", false);
 }
 
-function findChromeWindowByURI(aURI) {
-  let windows = Services.wm.getEnumerator(null);
-  while (windows.hasMoreElements()) {
-    let win = windows.getNext();
-    if (win.location.href == aURI)
-      return win;
-  }
-  return null;
-}
-
-function updateTabContextMenu(tab) {
-  let menu = document.getElementById("tabContextMenu");
-  if (!tab)
-    tab = gBrowser.selectedTab;
-  var evt = new Event("");
-  tab.dispatchEvent(evt);
-  menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
-  is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
-  menu.hidePopup();
-}
-
-function openToolbarCustomizationUI(aCallback, aBrowserWin) {
-  if (!aBrowserWin)
-    aBrowserWin = window;
-
-  aBrowserWin.gCustomizeMode.enter();
-
-  aBrowserWin.gNavToolbox.addEventListener("customizationready", function UI_loaded() {
-    aBrowserWin.gNavToolbox.removeEventListener("customizationready", UI_loaded);
-    executeSoon(function() {
-      aCallback(aBrowserWin)
-    });
-  });
-}
-
-function closeToolbarCustomizationUI(aCallback, aBrowserWin) {
-  aBrowserWin.gNavToolbox.addEventListener("aftercustomization", function unloaded() {
-    aBrowserWin.gNavToolbox.removeEventListener("aftercustomization", unloaded);
-    executeSoon(aCallback);
-  });
-
-  aBrowserWin.gCustomizeMode.exit();
-}
-
-function waitForCondition(condition, nextTest, errorMsg) {
-  var tries = 0;
-  var interval = setInterval(function() {
-    if (tries >= 30) {
-      ok(false, errorMsg);
-      moveOn();
-    }
-    var conditionPassed;
-    try {
-      conditionPassed = condition();
-    } catch (e) {
-      ok(false, e + "\n" + e.stack);
-      conditionPassed = false;
-    }
-    if (conditionPassed) {
-      moveOn();
-    }
-    tries++;
-  }, 100);
-  var moveOn = function() { clearInterval(interval); nextTest(); };
-}
-
-function getTestPlugin(aName) {
-  var pluginName = aName || "Test Plug-in";
-  var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-  var tags = ph.getPluginTags();
-
-  // Find the test plugin
-  for (var i = 0; i < tags.length; i++) {
-    if (tags[i].name == pluginName)
-      return tags[i];
-  }
-  ok(false, "Unable to find plugin");
-  return null;
-}
-
-// call this to set the test plugin(s) initially expected enabled state.
-// it will automatically be reset to it's previous value after the test
-// ends
-function setTestPluginEnabledState(newEnabledState, pluginName) {
-  var plugin = getTestPlugin(pluginName);
-  var oldEnabledState = plugin.enabledState;
-  plugin.enabledState = newEnabledState;
-  SimpleTest.registerCleanupFunction(function() {
-    getTestPlugin(pluginName).enabledState = oldEnabledState;
-  });
-}
-
-// after a test is done using the plugin doorhanger, we should just clear
-// any permissions that may have crept in
-function clearAllPluginPermissions() {
-  let perms = Services.perms.enumerator;
-  while (perms.hasMoreElements()) {
-    let perm = perms.getNext();
-    if (perm.type.startsWith('plugin')) {
-      Services.perms.remove(perm.host, perm.type);
-    }
-  }
-}
-
-function updateBlocklist(aCallback) {
-  var blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"]
-                          .getService(Ci.nsITimerCallback);
-  var observer = function() {
-    Services.obs.removeObserver(observer, "blocklist-updated");
-    SimpleTest.executeSoon(aCallback);
-  };
-  Services.obs.addObserver(observer, "blocklist-updated", false);
-  blocklistNotifier.notify(null);
-}
-
-var _originalTestBlocklistURL = null;
-function setAndUpdateBlocklist(aURL, aCallback) {
-  if (!_originalTestBlocklistURL)
-    _originalTestBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url");
-  Services.prefs.setCharPref("extensions.blocklist.url", aURL);
-  updateBlocklist(aCallback);
-}
-
-function resetBlocklist() {
-  Services.prefs.setCharPref("extensions.blocklist.url", _originalTestBlocklistURL);
-}
-
-function whenNewWindowLoaded(aOptions, aCallback) {
-  let win = OpenBrowserWindow(aOptions);
-  win.addEventListener("load", function onLoad() {
-    win.removeEventListener("load", onLoad, false);
-    aCallback(win);
-  }, false);
-}
-
-/**
- * Waits for all pending async statements on the default connection, before
- * proceeding with aCallback.
- *
- * @param aCallback
- *        Function to be called when done.
- * @param aScope
- *        Scope for the callback.
- * @param aArguments
- *        Arguments array for the callback.
- *
- * @note The result is achieved by asynchronously executing a query requiring
- *       a write lock.  Since all statements on the same connection are
- *       serialized, the end of this write operation means that all writes are
- *       complete.  Note that WAL makes so that writers don't block readers, but
- *       this is a problem only across different connections.
- */
-function waitForAsyncUpdates(aCallback, aScope, aArguments) {
-  let scope = aScope || this;
-  let args = aArguments || [];
-  let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
-                              .DBConnection;
-  let begin = db.createAsyncStatement("BEGIN EXCLUSIVE");
-  begin.executeAsync();
-  begin.finalize();
-
-  let commit = db.createAsyncStatement("COMMIT");
-  commit.executeAsync({
-    handleResult: function() {},
-    handleError: function() {},
-    handleCompletion: function(aReason) {
-      aCallback.apply(scope, args);
-    }
-  });
-  commit.finalize();
-}
-
-/**
- * Asynchronously check a url is visited.
-
- * @param aURI The URI.
- * @param aExpectedValue The expected value.
- * @return {Promise}
- * @resolves When the check has been added successfully.
- * @rejects JavaScript exception.
- */
-function promiseIsURIVisited(aURI, aExpectedValue) {
-  let deferred = Promise.defer();
-  PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
-    deferred.resolve(aIsVisited);
-  });
-
-  return deferred.promise;
-}
-
-function whenNewTabLoaded(aWindow, aCallback) {
-  aWindow.BrowserOpenTab();
-
-  let browser = aWindow.gBrowser.selectedBrowser;
-  if (browser.contentDocument.readyState === "complete") {
-    aCallback();
-    return;
-  }
-
-  whenTabLoaded(aWindow.gBrowser.selectedTab, aCallback);
-}
-
-function whenTabLoaded(aTab, aCallback) {
-  let browser = aTab.linkedBrowser;
-  browser.addEventListener("load", function onLoad() {
-    browser.removeEventListener("load", onLoad, true);
-    executeSoon(aCallback);
-  }, true);
-}
-
-function promiseTabLoaded(aTab) {
-  let deferred = Promise.defer();
-  whenTabLoaded(aTab, deferred.resolve);
-  return deferred.promise;
-}
-
-function addVisits(aPlaceInfo, aCallback) {
-  let places = [];
-  if (aPlaceInfo instanceof Ci.nsIURI) {
-    places.push({ uri: aPlaceInfo });
-  } else if (Array.isArray(aPlaceInfo)) {
-    places = places.concat(aPlaceInfo);
-  } else {
-    places.push(aPlaceInfo);
-   }
-
-  // Create mozIVisitInfo for each entry.
-  let now = Date.now();
-  for (let i = 0; i < places.length; i++) {
-    if (!places[i].title) {
-      places[i].title = "test visit for " + places[i].uri.spec;
-    }
-    places[i].visits = [{
-      transitionType: places[i].transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
-                                                         : places[i].transition,
-      visitDate: places[i].visitDate || (now++) * 1000,
-      referrerURI: places[i].referrer
-    }];
-  }
-
-  PlacesUtils.asyncHistory.updatePlaces(
-    places,
-    {
-      handleError: function AAV_handleError() {
-        throw("Unexpected error in adding visit.");
-      },
-      handleResult: function () {},
-      handleCompletion: function UP_handleCompletion() {
-        if (aCallback)
-          aCallback();
-      }
-    }
-  );
-}
-
-/**
- * Ensures that the specified URIs are either cleared or not.
- *
- * @param aURIs
- *        Array of page URIs
- * @param aShouldBeCleared
- *        True if each visit to the URI should be cleared, false otherwise
- */
-function promiseHistoryClearedState(aURIs, aShouldBeCleared) {
-  let deferred = Promise.defer();
-  let callbackCount = 0;
-  let niceStr = aShouldBeCleared ? "no longer" : "still";
-  function callbackDone() {
-    if (++callbackCount == aURIs.length)
-      deferred.resolve();
-  }
-  aURIs.forEach(function (aURI) {
-    PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
-      is(aIsVisited, !aShouldBeCleared,
-         "history visit " + aURI.spec + " should " + niceStr + " exist");
-      callbackDone();
-    });
-  });
-
-  return deferred.promise;
-}
-
 /**
  * Allows waiting for an observer notification once.
  *
  * @param topic
  *        Notification topic to observe.
  *
  * @return {Promise}
  * @resolves The array [subject, data] from the observed notification.
  * @rejects Never.
  */
 function promiseTopicObserved(topic)
 {
   let deferred = Promise.defer();
+  info("Waiting for observer topic " + topic);
   Services.obs.addObserver(function PTO_observe(subject, topic, data) {
     Services.obs.removeObserver(PTO_observe, topic);
     deferred.resolve([subject, data]);
   }, topic, false);
   return deferred.promise;
 }
 
-/**
- * Clears history asynchronously.
- *
- * @return {Promise}
- * @resolves When history has been cleared.
- * @rejects Never.
- */
-function promiseClearHistory() {
-  let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
-  PlacesUtils.bhistory.removeAllPages();
-  return promise;
-}
 
 /**
- * Waits for the next top-level document load in the current browser.  The URI
- * of the document is compared against aExpectedURL.  The load is then stopped
- * before it actually starts.
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
  *
- * @param aExpectedURL
- *        The URL of the document that is expected to load.
- * @return promise
+ * @param tab
+ *        The tab to load into.
+ * @param [optional] url
+ *        The url to load, or the current url.
+ * @param [optional] event
+ *        The load event type to wait for.  Defaults to "load".
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
  */
-function waitForDocLoadAndStopIt(aExpectedURL) {
+function promiseTabLoadEvent(tab, url, eventType="load")
+{
   let deferred = Promise.defer();
-  let progressListener = {
-    onStateChange: function (webProgress, req, flags, status) {
-      info("waitForDocLoadAndStopIt: onStateChange: " + req.name);
-      let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
-                     Ci.nsIWebProgressListener.STATE_START;
-      if ((flags & docStart) && webProgress.isTopLevel) {
-        info("waitForDocLoadAndStopIt: Document start: " +
-             req.QueryInterface(Ci.nsIChannel).URI.spec);
-        is(req.originalURI.spec, aExpectedURL,
-           "waitForDocLoadAndStopIt: The expected URL was loaded");
-        req.cancel(Components.results.NS_ERROR_FAILURE);
-        gBrowser.removeProgressListener(progressListener);
-        deferred.resolve();
-      }
-    },
-  };
-  gBrowser.addProgressListener(progressListener);
-  info("waitForDocLoadAndStopIt: Waiting for URL: " + aExpectedURL);
+  info("Wait tab event: " + eventType);
+
+  function handle(event) {
+    if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+        event.target.location.href == "about:blank" ||
+        (url && event.target.location.href != url)) {
+      info("Skipping spurious '" + eventType + "'' event" +
+           " for " + event.target.location.href);
+      return;
+    }
+    clearTimeout(timeout);
+    tab.linkedBrowser.removeEventListener(eventType, handle, true);
+    info("Tab event received: " + eventType);
+    deferred.resolve(event);
+  }
+
+  let timeout = setTimeout(() => {
+    if (tab.linkedBrowser)
+      tab.linkedBrowser.removeEventListener(eventType, handle, true);
+    deferred.reject(new Error("Timed out while waiting for a '" + eventType + "'' event"));
+  }, 30000);
+
+  tab.linkedBrowser.addEventListener(eventType, handle, true, true);
+  if (url)
+    tab.linkedBrowser.loadURI(url);
   return deferred.promise;
 }
 
-let FullZoomHelper = {
+const PREF_SECURITY_DELAY_INITIAL = Services.prefs.getIntPref("security.notification_enable_delay");
+
+function setup() {
+  // Disable transitions as they slow the test down and we want to click the
+  // mouse buttons in a predictable location.
+  PopupNotifications.transitionsEnabled = false;
 
-  selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange(tab) {
-    if (!tab)
-      throw new Error("tab must be given.");
-    if (gBrowser.selectedTab == tab)
-      return Promise.resolve();
-    gBrowser.selectedTab = tab;
-    return this.waitForLocationChange();
-  },
+  registerCleanupFunction(() => {
+    PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
+    PopupNotifications.transitionsEnabled = true;
+  });
+}
 
-  removeTabAndWaitForLocationChange: function removeTabAndWaitForLocationChange(tab) {
-    tab = tab || gBrowser.selectedTab;
-    let selected = gBrowser.selectedTab == tab;
-    gBrowser.removeTab(tab);
-    if (selected)
-      return this.waitForLocationChange();
-    return Promise.resolve();
-  },
+function goNext() {
+  executeSoon(() => executeSoon(Task.async(runNextTest)));
+}
+
+function* runNextTest() {
+  if (tests.length == 0) {
+    executeSoon(finish);
+    return;
+  }
 
-  waitForLocationChange: function waitForLocationChange() {
-    let deferred = Promise.defer();
-    Services.obs.addObserver(function obs(subj, topic, data) {
-      Services.obs.removeObserver(obs, topic);
-      deferred.resolve();
-    }, "browser-fullZoom:location-change", false);
-    return deferred.promise;
-  },
-
-  load: function load(tab, url) {
-    let deferred = Promise.defer();
-    let didLoad = false;
-    let didZoom = false;
+  let nextTest = tests.shift();
+  if (nextTest.onShown) {
+    let shownState = false;
+    onPopupEvent("popupshowing", function () {
+      info("[" + nextTest.id + "] popup showing");
+    });
+    onPopupEvent("popupshown", function () {
+      shownState = true;
+      info("[" + nextTest.id + "] popup shown");
+      Task.spawn(() => nextTest.onShown(this))
+          .then(undefined , ex => Assert.ok(false, "onShown failed: " + ex));
+    });
+    onPopupEvent("popuphidden", function () {
+      info("[" + nextTest.id + "] popup hidden");
+      nextTest.onHidden(this);
+      goNext();
+    }, () => shownState);
+    info("[" + nextTest.id + "] added listeners; panel is open: " + PopupNotifications.isPanelOpen);
+  }
 
-    tab.linkedBrowser.addEventListener("load", function (event) {
-      event.currentTarget.removeEventListener("load", arguments.callee, true);
-      didLoad = true;
-      if (didZoom)
-        deferred.resolve();
-    }, true);
-
-    this.waitForLocationChange().then(function () {
-      didZoom = true;
-      if (didLoad)
-        deferred.resolve();
-    });
-
-    tab.linkedBrowser.loadURI(url);
-
-    return deferred.promise;
-  },
+  info("[" + nextTest.id + "] running test");
+  yield nextTest.run();
+}
 
-  zoomTest: function zoomTest(tab, val, msg) {
-    is(ZoomManager.getZoomForBrowser(tab.linkedBrowser), val, msg);
-  },
-
-  enlarge: function enlarge() {
-    let deferred = Promise.defer();
-    FullZoom.enlarge(function () deferred.resolve());
-    return deferred.promise;
-  },
+function showNotification(notifyObj) {
+  info("Showing notification " + notifyObj.id);
+  return PopupNotifications.show(notifyObj.browser,
+                                 notifyObj.id,
+                                 notifyObj.message,
+                                 notifyObj.anchorID,
+                                 notifyObj.mainAction,
+                                 notifyObj.secondaryActions,
+                                 notifyObj.options);
+}
 
-  reduce: function reduce() {
-    let deferred = Promise.defer();
-    FullZoom.reduce(function () deferred.resolve());
-    return deferred.promise;
-  },
-
-  reset: function reset() {
-    let deferred = Promise.defer();
-    FullZoom.reset(function () deferred.resolve());
-    return deferred.promise;
-  },
+function dismissNotification(popup) {
+  info("Dismissing notification " + popup.childNodes[0].id);
+  executeSoon(() => EventUtils.synthesizeKey("VK_ESCAPE", {}));
+}
 
-  BACK: 0,
-  FORWARD: 1,
-  navigate: function navigate(direction) {
-    let deferred = Promise.defer();
-    let didPs = false;
-    let didZoom = false;
-
-    gBrowser.addEventListener("pageshow", function (event) {
-      gBrowser.removeEventListener("pageshow", arguments.callee, true);
-      didPs = true;
-      if (didZoom)
-        deferred.resolve();
-    }, true);
+function BasicNotification(testId) {
+  this.browser = gBrowser.selectedBrowser;
+  this.id = "test-notification-" + testId;
+  this.message = "This is popup notification for " + testId;
+  this.anchorID = null;
+  this.mainAction = {
+    label: "Main Action",
+    accessKey: "M",
+    callback: () => this.mainActionClicked = true
+  };
+  this.secondaryActions = [
+    {
+      label: "Secondary Action",
+      accessKey: "S",
+      callback: () => this.secondaryActionClicked = true
+    }
+  ];
+  this.options = {
+    eventCallback: eventName => {
+      switch (eventName) {
+        case "dismissed":
+          this.dismissalCallbackTriggered = true;
+          break;
+        case "showing":
+          this.showingCallbackTriggered = true;
+          break;
+        case "shown":
+          this.shownCallbackTriggered = true;
+          break;
+        case "removed":
+          this.removedCallbackTriggered = true;
+          break;
+        case "swapping":
+          this.swappingCallbackTriggered = true;
+          break;
+      }
+    }
+  };
+}
 
-    if (direction == this.BACK)
-      gBrowser.goBack();
-    else if (direction == this.FORWARD)
-      gBrowser.goForward();
-
-    this.waitForLocationChange().then(function () {
-      didZoom = true;
-      if (didPs)
-        deferred.resolve();
-    });
-    return deferred.promise;
-  },
-
-  failAndContinue: function failAndContinue(func) {
-    return function (err) {
-      ok(false, err);
-      func();
-    };
-  },
+BasicNotification.prototype.addOptions = function(options) {
+  for (let [name, value] in Iterator(options))
+    this.options[name] = value;
 };
 
+function ErrorNotification() {
+  this.mainAction.callback = () => {
+    this.mainActionClicked = true;
+    throw new Error("Oops!");
+  };
+  this.secondaryActions[0].callback = () => {
+    this.secondaryActionClicked = true;
+    throw new Error("Oops!");
+  };
+}
+
+ErrorNotification.prototype = new BasicNotification();
+ErrorNotification.prototype.constructor = ErrorNotification;
+
+function checkPopup(popup, notifyObj) {
+  info("Checking notification " + notifyObj.id);
+
+  ok(notifyObj.showingCallbackTriggered, "showing callback was triggered");
+  ok(notifyObj.shownCallbackTriggered, "shown callback was triggered");
+
+  let notifications = popup.childNodes;
+  is(notifications.length, 1, "one notification displayed");
+  let notification = notifications[0];
+  if (!notification)
+    return;
+  let icon = document.getAnonymousElementByAttribute(notification, "class",
+                                                     "popup-notification-icon");
+  if (notifyObj.id == "geolocation") {
+    isnot(icon.boxObject.width, 0, "icon for geo displayed");
+    is(popup.anchorNode.className, "notification-anchor-icon",
+       "notification anchored to icon");
+  }
+  is(notification.getAttribute("label"), notifyObj.message, "message matches");
+  is(notification.id, notifyObj.id + "-notification", "id matches");
+  if (notifyObj.mainAction) {
+    is(notification.getAttribute("buttonlabel"), notifyObj.mainAction.label,
+       "main action label matches");
+    is(notification.getAttribute("buttonaccesskey"),
+       notifyObj.mainAction.accessKey, "main action accesskey matches");
+  }
+  let actualSecondaryActions =
+    Array.filter(notification.childNodes, child => child.nodeName == "menuitem");
+  let secondaryActions = notifyObj.secondaryActions || [];
+  let actualSecondaryActionsCount = actualSecondaryActions.length;
+  if (notifyObj.options.hideNotNow) {
+    is(notification.getAttribute("hidenotnow"), "true", "'Not Now' item hidden");
+    if (secondaryActions.length)
+      is(notification.lastChild.tagName, "menuitem", "no menuseparator");
+  }
+  else if (secondaryActions.length) {
+    is(notification.lastChild.tagName, "menuseparator", "menuseparator exists");
+  }
+  is(actualSecondaryActionsCount, secondaryActions.length,
+    actualSecondaryActions.length + " secondary actions");
+  secondaryActions.forEach(function (a, i) {
+    is(actualSecondaryActions[i].getAttribute("label"), a.label,
+       "label for secondary action " + i + " matches");
+    is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey,
+       "accessKey for secondary action " + i + " matches");
+  });
+}
+
+XPCOMUtils.defineLazyGetter(this, "gActiveListeners", () => {
+  let listeners = new Map();
+  registerCleanupFunction(() => {
+    for (let [listener, eventName] of listeners) {
+      PopupNotifications.panel.removeEventListener(eventName, listener, false);
+    }
+  });
+  return listeners;
+});
+
+function onPopupEvent(eventName, callback, condition) {
+  let listener = event => {
+    if (event.target != PopupNotifications.panel ||
+        (condition && !condition()))
+      return;
+    PopupNotifications.panel.removeEventListener(eventName, listener, false);
+    gActiveListeners.delete(listener);
+    executeSoon(() => callback.call(PopupNotifications.panel));
+  }
+  gActiveListeners.set(listener, eventName);
+  PopupNotifications.panel.addEventListener(eventName, listener, false);
+}
+
+function triggerMainCommand(popup) {
+  let notifications = popup.childNodes;
+  ok(notifications.length > 0, "at least one notification displayed");
+  let notification = notifications[0];
+  info("Triggering main command for notification " + notification.id);
+  // 20, 10 so that the inner button is hit
+  EventUtils.synthesizeMouse(notification.button, 20, 10, {});
+}
+
+function triggerSecondaryCommand(popup, index) {
+  let notifications = popup.childNodes;
+  ok(notifications.length > 0, "at least one notification displayed");
+  let notification = notifications[0];
+  info("Triggering secondary command for notification " + notification.id);
+  // Cancel the arrow panel slide-in transition (bug 767133) such that
+  // it won't interfere with us interacting with the dropdown.
+  document.getAnonymousNodes(popup)[0].style.transition = "none";
+
+  notification.button.focus();
+
+  popup.addEventListener("popupshown", function handle() {
+    popup.removeEventListener("popupshown", handle, false);
+    info("Command popup open for notification " + notification.id);
+    // Press down until the desired command is selected
+    for (let i = 0; i <= index; i++) {
+      EventUtils.synthesizeKey("VK_DOWN", {});
+    }
+    // Activate
+    EventUtils.synthesizeKey("VK_RETURN", {});
+  }, false);
+
+  // One down event to open the popup
+  info("Open the popup to trigger secondary command for notification " + notification.id);
+  EventUtils.synthesizeKey("VK_DOWN", { altKey: !navigator.platform.contains("Mac") });
+}
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -12,16 +12,17 @@ MOCHITEST_CHROME_MANIFESTS += [
     'content/test/chrome/chrome.ini',
 ]
 
 BROWSER_CHROME_MANIFESTS += [
     'content/test/chat/browser.ini',
     'content/test/general/browser.ini',
     'content/test/newtab/browser.ini',
     'content/test/plugins/browser.ini',
+    'content/test/popupNotifications/browser.ini',
     'content/test/social/browser.ini',
 ]
 
 DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 DEFINES['E10S_TESTING_ONLY'] = CONFIG['E10S_TESTING_ONLY']
 DEFINES['APP_LICENSE_BLOCK'] = '%s/content/overrides/app-license.html' % SRCDIR
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3', 'cocoa'):
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
 const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "self-hosted", "XStringBundle"];
 const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
+const EDITOR_BREAKPOINTS_UPDATE_DELAY = 200; // ms
 const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
 const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
 const FRAME_STEP_CLEAR_DELAY = 100; // ms
 const CALL_STACK_PAGE_SIZE = 25; // frames
 
 // The panel's window global is an EventEmitter firing the following events:
 const EVENTS = {
   // When the debugger's source editor instance finishes loading or unloading.
@@ -1162,18 +1163,20 @@ SourceScripts.prototype = {
         if (!DebuggerView.Sources.selectedValue) {
           DebuggerView.Sources.selectedIndex = 0;
         }
       });
     }
 
     // If there are any stored breakpoints for this source, display them again,
     // both in the editor and the breakpoints pane.
-    DebuggerController.Breakpoints.updateEditorBreakpoints();
     DebuggerController.Breakpoints.updatePaneBreakpoints();
+    setNamedTimeout("update-editor-bp", EDITOR_BREAKPOINTS_UPDATE_DELAY, () => {
+      DebuggerController.Breakpoints.updateEditorBreakpoints();
+    });
 
     // Make sure the events listeners are up to date.
     if (DebuggerView.instrumentsPaneTab == "events-tab") {
       DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
     }
 
     // Signal that a new source has been added.
     window.emit(EVENTS.NEW_SOURCE);
@@ -1214,18 +1217,18 @@ SourceScripts.prototype = {
     }
     // ..or the first entry if there's no one selected yet.
     else if (!DebuggerView.Sources.selectedValue) {
       DebuggerView.Sources.selectedIndex = 0;
     }
 
     // If there are any stored breakpoints for the sources, display them again,
     // both in the editor and the breakpoints pane.
+    DebuggerController.Breakpoints.updatePaneBreakpoints();
     DebuggerController.Breakpoints.updateEditorBreakpoints();
-    DebuggerController.Breakpoints.updatePaneBreakpoints();
 
     // Signal that sources have been added.
     window.emit(EVENTS.SOURCES_ADDED);
   },
 
   /**
    * Handler for the debugger client's 'blackboxchange' notification.
    */
@@ -1706,16 +1709,20 @@ EventListeners.prototype = {
    *        Invoked once the event listeners are fetched and displayed.
    */
   _getListeners: function(aCallback) {
     gThreadClient.eventListeners(Task.async(function*(aResponse) {
       if (aResponse.error) {
         throw "Error getting event listeners: " + aResponse.message;
       }
 
+      // Make sure all the listeners are sorted by the event type, since
+      // they're not guaranteed to be clustered together.
+      aResponse.listeners.sort((a, b) => a.type > b.type ? 1 : -1);
+
       // Add all the listeners in the debugger view event linsteners container.
       for (let listener of aResponse.listeners) {
         let definitionSite = yield this._getDefinitionSite(listener.function);
         listener.function.url = definitionSite;
         DebuggerView.EventListeners.addListener(listener, { staged: true });
       }
 
       // Flushes all the prepared events into the event listeners container.
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -73,16 +73,17 @@ support-files =
   doc_random-javascript.html
   doc_recursion-stack.html
   doc_scope-variable.html
   doc_scope-variable-2.html
   doc_scope-variable-3.html
   doc_scope-variable-4.html
   doc_script-switching-01.html
   doc_script-switching-02.html
+  doc_split-console-paused-reload.html
   doc_step-out.html
   doc_terminate-on-tab-close.html
   doc_tracing-01.html
   doc_watch-expressions.html
   doc_watch-expression-button.html
   doc_with-frame.html
   head.js
   sjs_random-javascript.sjs
@@ -232,16 +233,17 @@ skip-if = os == "linux" || e10s # Bug 88
 [browser_dbg_searchbox-parse.js]
 [browser_dbg_source-maps-01.js]
 [browser_dbg_source-maps-02.js]
 [browser_dbg_source-maps-03.js]
 [browser_dbg_source-maps-04.js]
 [browser_dbg_sources-cache.js]
 [browser_dbg_sources-labels.js]
 [browser_dbg_sources-sorting.js]
+[browser_dbg_split-console-paused-reload.js]
 [browser_dbg_stack-01.js]
 [browser_dbg_stack-02.js]
 [browser_dbg_stack-03.js]
 [browser_dbg_stack-04.js]
 [browser_dbg_stack-05.js]
 [browser_dbg_stack-06.js]
 [browser_dbg_stack-07.js]
 [browser_dbg_step-out.js]
--- a/browser/devtools/debugger/test/browser_dbg_break-on-dom-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_break-on-dom-03.js
@@ -25,26 +25,26 @@ function test() {
       is(gEvents.widget._parent.querySelectorAll(".side-menu-widget-group-checkbox").length, 3,
         "There should be a checkbox for each group shown in the view.");
 
       is(gEvents.widget._parent.querySelectorAll(".side-menu-widget-item").length, 4,
         "There should be 4 items shown in the view.");
       is(gEvents.widget._parent.querySelectorAll(".side-menu-widget-item-checkbox").length, 4,
         "There should be a checkbox for each item shown in the view.");
 
-      testEventItem(0, "doc_event-listeners-02.html", "keydown", ["window", "body"], false);
+      testEventItem(0, "doc_event-listeners-02.html", "change", ["body > input:nth-child(2)"], false);
       testEventItem(1, "doc_event-listeners-02.html", "click", ["body > button:nth-child(1)"], false);
-      testEventItem(2, "doc_event-listeners-02.html", "change", ["body > input:nth-child(2)"], false);
+      testEventItem(2, "doc_event-listeners-02.html", "keydown", ["window", "body"], false);
       testEventItem(3, "doc_event-listeners-02.html", "keyup", ["body > input:nth-child(2)"], false);
 
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
 
-      is(gEvents.getAllEvents().toString(), "keydown,click,change,keyup",
+      is(gEvents.getAllEvents().toString(), "change,click,keydown,keyup",
         "The getAllEvents() method returns the correct stuff.");
       is(gEvents.getCheckedEvents().toString(), "",
         "The getCheckedEvents() method returns the correct stuff.");
 
       yield ensureThreadClientState(aPanel, "resumed");
       yield closeDebuggerAndFinish(aPanel);
     });
 
--- a/browser/devtools/debugger/test/browser_dbg_break-on-dom-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_break-on-dom-04.js
@@ -25,43 +25,43 @@ function test() {
 
       testEventItem(0, false);
       testEventItem(1, false);
       testEventItem(2, false);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "");
+      testEventArrays("change,click,keydown,keyup", "");
 
       let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
       EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
       yield updated;
 
       testEventItem(0, true);
       testEventItem(1, false);
       testEventItem(2, false);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "keydown");
+      testEventArrays("change,click,keydown,keyup", "change");
 
       let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
       EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
       yield updated;
 
       testEventItem(0, false);
       testEventItem(1, false);
       testEventItem(2, false);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "");
+      testEventArrays("change,click,keydown,keyup", "");
 
       yield ensureThreadClientState(aPanel, "resumed");
       yield closeDebuggerAndFinish(aPanel);
     });
 
     function getItemCheckboxNode(index) {
       return gEvents.items[index].target.parentNode
         .querySelector(".side-menu-widget-item-checkbox");
--- a/browser/devtools/debugger/test/browser_dbg_break-on-dom-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_break-on-dom-05.js
@@ -26,69 +26,69 @@ function test() {
 
       testEventItem(0, false);
       testEventItem(1, false);
       testEventItem(2, false);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "");
+      testEventArrays("change,click,keydown,keyup", "");
 
       let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
       EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("interactionEvents"), gDebugger);
       yield updated;
 
-      testEventItem(0, false);
+      testEventItem(0, true);
       testEventItem(1, false);
-      testEventItem(2, true);
+      testEventItem(2, false);
       testEventItem(3, false);
       testEventGroup("interactionEvents", true);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "change");
+      testEventArrays("change,click,keydown,keyup", "change");
 
       let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
       EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("interactionEvents"), gDebugger);
       yield updated;
 
       testEventItem(0, false);
       testEventItem(1, false);
       testEventItem(2, false);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "");
+      testEventArrays("change,click,keydown,keyup", "");
 
       let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
       EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("keyboardEvents"), gDebugger);
       yield updated;
 
-      testEventItem(0, true);
+      testEventItem(0, false);
       testEventItem(1, false);
-      testEventItem(2, false);
+      testEventItem(2, true);
       testEventItem(3, true);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", true);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "keydown,keyup");
+      testEventArrays("change,click,keydown,keyup", "keydown,keyup");
 
       let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
       EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("keyboardEvents"), gDebugger);
       yield updated;
 
       testEventItem(0, false);
       testEventItem(1, false);
       testEventItem(2, false);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "");
+      testEventArrays("change,click,keydown,keyup", "");
 
       yield ensureThreadClientState(aPanel, "resumed");
       yield closeDebuggerAndFinish(aPanel);
     });
 
     function getItemCheckboxNode(index) {
       return gEvents.items[index].target.parentNode
         .querySelector(".side-menu-widget-item-checkbox");
--- a/browser/devtools/debugger/test/browser_dbg_break-on-dom-06.js
+++ b/browser/devtools/debugger/test/browser_dbg_break-on-dom-06.js
@@ -25,69 +25,69 @@ function test() {
 
       testEventItem(0, false);
       testEventItem(1, false);
       testEventItem(2, false);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "");
+      testEventArrays("change,click,keydown,keyup", "");
 
       let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
       EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
       EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
       EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(2), gDebugger);
       yield updated;
 
       testEventItem(0, true);
       testEventItem(1, true);
       testEventItem(2, true);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "keydown,click,change");
+      testEventArrays("change,click,keydown,keyup", "change,click,keydown");
 
       yield reloadActiveTab(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
 
       testEventItem(0, true);
       testEventItem(1, true);
       testEventItem(2, true);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "keydown,click,change");
+      testEventArrays("change,click,keydown,keyup", "change,click,keydown");
 
       let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
       EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
       EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
       EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(2), gDebugger);
       yield updated;
 
       testEventItem(0, false);
       testEventItem(1, false);
       testEventItem(2, false);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "");
+      testEventArrays("change,click,keydown,keyup", "");
 
       yield reloadActiveTab(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
 
       testEventItem(0, false);
       testEventItem(1, false);
       testEventItem(2, false);
       testEventItem(3, false);
       testEventGroup("interactionEvents", false);
       testEventGroup("keyboardEvents", false);
       testEventGroup("mouseEvents", false);
-      testEventArrays("keydown,click,change,keyup", "");
+      testEventArrays("change,click,keydown,keyup", "");
 
       yield ensureThreadClientState(aPanel, "resumed");
       yield closeDebuggerAndFinish(aPanel);
     });
 
     function getItemCheckboxNode(index) {
       return gEvents.items[index].target.parentNode
         .querySelector(".side-menu-widget-item-checkbox");
--- a/browser/devtools/debugger/test/browser_dbg_parser-06.js
+++ b/browser/devtools/debugger/test/browser_dbg_parser-06.js
@@ -55,17 +55,23 @@ function test() {
   verify("\nlet\nfoo\n=\nbar\n", e => e.name == "bar", [5, 0], [5, 3]);
 
   // Just to be sure, check AssignmentExpreesions as well.
   verify("foo = bar", e => e.name == "foo", [1, 0], [1, 3]);
   verify("\nfoo\n=\nbar\n", e => e.name == "foo", [2, 0], [2, 3]);
   verify("foo = bar", e => e.name == "bar", [1, 6], [1, 9]);
   verify("\nfoo\n=\nbar\n", e => e.name == "bar", [4, 0], [4, 3]);
 
-  // LabeledStatement and ContinueStatement, because it's 1968 again
+  // LabeledStatement, BreakStatement and ContinueStatement, because it's 1968 again
+
+  verify("foo: bar", e => e.name == "foo", [1, 0], [1, 3]);
+  verify("\nfoo\n:\nbar\n", e => e.name == "foo", [2, 0], [2, 3]);
+
+  verify("foo: for(;;) break foo", e => e.name == "foo", [1, 19], [1, 22]);
+  verify("\nfoo\n:\nfor(\n;\n;\n)\nbreak\nfoo\n", e => e.name == "foo", [9, 0], [9, 3]);
 
   verify("foo: bar", e => e.name == "foo", [1, 0], [1, 3]);
   verify("\nfoo\n:\nbar\n", e => e.name == "foo", [2, 0], [2, 3]);
 
   verify("foo: for(;;) continue foo", e => e.name == "foo", [1, 22], [1, 25]);
   verify("\nfoo\n:\nfor(\n;\n;\n)\ncontinue\nfoo\n", e => e.name == "foo", [9, 0], [9, 3]);
 
   finish();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_split-console-paused-reload.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Hitting ESC to open the split console when paused on reload should not stop
+ * the pending navigation.
+ */
+
+function test() {
+  Task.spawn(runTests);
+}
+
+function* runTests() {
+  const TAB_URL = EXAMPLE_URL + "doc_split-console-paused-reload.html";
+  let [,, panel] = yield initDebugger(TAB_URL);
+  let dbgWin = panel.panelWin;
+  let frames = dbgWin.DebuggerView.StackFrames;
+  let toolbox = gDevTools.getToolbox(panel.target);
+
+  yield panel.addBreakpoint({ url: TAB_URL, line: 16 });
+  info("Breakpoint was set.");
+  dbgWin.DebuggerController._target.activeTab.reload();
+  info("Page reloaded.");
+  yield waitForSourceAndCaretAndScopes(panel, ".html", 16);
+  yield ensureThreadClientState(panel, "paused");
+  info("Breakpoint was hit.");
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    frames.selectedItem.target,
+    dbgWin);
+  info("The breadcrumb received focus.");
+
+  // This is the meat of the test.
+  let result = toolbox.once("webconsole-ready", () => {
+    ok(toolbox.splitConsole, "Split console is shown.");
+    is(dbgWin.gThreadClient.state, "paused", "Execution is still paused.");
+  });
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, dbgWin);
+  yield result;
+  yield resumeDebuggerThenCloseAndFinish(panel);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/doc_split-console-paused-reload.html
@@ -0,0 +1,20 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Test page for opening a split-console when execution is paused</title>
+  </head>
+
+  <body>
+    <script type="text/javascript">
+      function runDebuggerStatement() {
+        debugger;
+      }
+      window.foobar = 1;
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -289,16 +289,22 @@ Toolbox.prototype = {
     }
     return responsiveModeActive;
   },
 
   _splitConsoleOnKeypress: function(e) {
     let responsiveModeActive = this._isResponsiveModeActive();
     if (e.keyCode === e.DOM_VK_ESCAPE && !responsiveModeActive) {
       this.toggleSplitConsole();
+      // If the debugger is paused, don't let the ESC key stop any pending
+      // navigation.
+      let jsdebugger = this.getPanel("jsdebugger");
+      if (jsdebugger && jsdebugger.panelWin.gThreadClient.state == "paused") {
+        e.preventDefault();
+      }
     }
   },
 
   _addReloadKeys: function() {
     [
       ["toolbox-reload-key", false],
       ["toolbox-reload-key2", false],
       ["toolbox-force-reload-key", true],
--- a/browser/devtools/shared/Parser.jsm
+++ b/browser/devtools/shared/Parser.jsm
@@ -445,18 +445,18 @@ let ParserHelpers = {
       if (parentType == "LabeledStatement") {
         // e.g. label: ...
         // The location is unavailable for the identifier node "label".
         let loc = Cu.cloneInto(parentLocation, {});
         loc.end.line = loc.start.line;
         loc.end.column = loc.start.column + aNode.name.length;
         return loc;
       }
-      if (parentType == "ContinueStatement") {
-        // e.g. continue label
+      if (parentType == "ContinueStatement" || parentType == "BreakStatement") {
+        // e.g. continue label; or break label;
         // The location is unavailable for the identifier node "label".
         let loc = Cu.cloneInto(parentLocation, {});
         loc.start.line = loc.end.line;
         loc.start.column = loc.end.column - aNode.name.length;
         return loc;
       }
     } else {
       if (parentType == "VariableDeclarator") {
--- a/browser/devtools/styleinspector/test/browser_ruleview_completion-new-property_02.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_completion-new-property_02.js
@@ -34,28 +34,28 @@ let testData = [
   ["VK_DOWN", {}, "rosybrown", 4, 6],
   ["VK_DOWN", {}, "royalblue", 5, 6],
   ["VK_RIGHT", {}, "royalblue", -1, 0],
   [" ", {}, "royalblue !important", 0, 10],
   ["!", {}, "royalblue !important", 0, 0],
   ["VK_ESCAPE", {}, null, -1, 0]
 ];
 
-let TEST_URL = "data:text/html,<h1 style='border: 1px solid red'>Filename:"+
-               " browser_bug894376_css_value_completion_new_property_value_pair.js</h1>";
+let TEST_URL = "data:text/html,<style>h1{border: 1px solid red}</style>" +
+  "<h1>Test element</h1>";
 
 let test = asyncTest(function*() {
   yield addTab(TEST_URL);
   let {toolbox, inspector, view} = yield openRuleView();
 
   info("Selecting the test node");
   yield selectNode("h1", inspector);
 
   info("Focusing a new css property editable property");
-  let brace = view.doc.querySelectorAll(".ruleview-ruleclose")[0];
+  let brace = view.doc.querySelectorAll(".ruleview-ruleclose")[1];
   let editor = yield focusEditableField(brace);
 
   info("Starting to test for css property completion");
   for (let i = 0; i < testData.length; i ++) {
     // Re-define the editor at each iteration, because the focus may have moved
     // from property to value and back
     editor = inplaceEditor(view.doc.activeElement);
     yield testCompletion(testData[i], editor, view);
@@ -65,17 +65,17 @@ let test = asyncTest(function*() {
 function* testCompletion([key, modifiers, completion, index, total], editor, view) {
   info("Pressing key " + key);
   info("Expecting " + completion + ", " + index + ", " + total);
 
   let onKeyPress;
 
   if (/tab/ig.test(key)) {
     info("Waiting for the new property or value editor to get focused");
-    let brace = view.doc.querySelector(".ruleview-ruleclose");
+    let brace = view.doc.querySelectorAll(".ruleview-ruleclose")[1];
     onKeyPress = once(brace.parentNode, "focus", true);
   } else if (/(right|back_space|escape|return)/ig.test(key) ||
              (modifiers.accelKey || modifiers.ctrlKey)) {
     info("Adding event listener for right|escape|back_space|return keys");
     onKeyPress = once(editor.input, "keypress");
   } else {
     info("Waiting for after-suggest event on the editor");
     onKeyPress = editor.once("after-suggest");
--- a/browser/devtools/webaudioeditor/test/browser.ini
+++ b/browser/devtools/webaudioeditor/test/browser.ini
@@ -1,10 +1,9 @@
 [DEFAULT]
-skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
 subsuite = devtools
 support-files =
   doc_simple-context.html
   doc_complex-context.html
   doc_simple-node-creation.html
   doc_buffer-and-array.html
   doc_media-node-creation.html
   doc_destroy-nodes.html
--- a/browser/themes/shared/devtools/debugger.inc.css
+++ b/browser/themes/shared/devtools/debugger.inc.css
@@ -603,23 +603,20 @@
 #sources-toolbar .devtools-toolbarbutton:not([label]) > .toolbarbutton-icon {
   width: 16px;
   height: 16px;
 }
 
 #resume {
   list-style-image: url(debugger-pause.png);
   -moz-image-region: rect(0px,16px,16px,0px);
-  transition: background 0.15s ease-in-out;
 }
 
 #resume[checked] {
-  background: none;
   list-style-image: url(debugger-play.png);
-  -moz-image-region: rect(0px,32px,16px,16px);
 }
 
 @media (min-resolution: 2dppx) {
   #resume {
     list-style-image: url(debugger-pause@2x.png);
     -moz-image-region: rect(0px,32px,32px,0px);
   }
 
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -51,16 +51,17 @@
   min-width: 78px;
   min-height: 18px;
   padding: 1px;
   text-shadow: none;
   border: none;
   border-radius: 0;
   margin: 2px 3px;
   color: inherit;
+  transition: background 0.05s ease-in-out;
 }
 
 .devtools-menulist:-moz-focusring,
 .devtools-toolbarbutton:-moz-focusring {
   outline: 1px dotted hsla(210,30%,85%,0.7);
   outline-offset: -4px;
 }
 
@@ -171,34 +172,42 @@
 }
 .theme-light .devtools-toolbarbutton:hover:active,
 .theme-light #toolbox-buttons .devtools-toolbarbutton[text-as-image]:hover:active,
 .theme-light .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]):hover:active {
   background: rgba(170, 170, 170, .4); /* Splitters */
 }
 
 /* Menu type buttons and checked states */
+.theme-dark .devtools-toolbarbutton[checked=true],
+.theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image][checked] {
+  background: rgba(29, 79, 115, .7); /* Select highlight blue */
+  color: #f5f7fa;
+}
+
+.theme-light .devtools-toolbarbutton[checked=true],
+.theme-light #toolbox-buttons .devtools-toolbarbutton[text-as-image][checked] {
+  background: rgba(76, 158, 217, .2); /* Select highlight blue */
+}
+
 .theme-dark .devtools-menulist[open=true],
 .theme-dark .devtools-toolbarbutton[open=true],
 .theme-dark .devtools-toolbarbutton[open=true]:hover,
 .theme-dark .devtools-toolbarbutton[open=true]:hover:active,
-.theme-dark .devtools-toolbarbutton[checked=true],
-.theme-dark .devtools-toolbarbutton[checked=true]:hover,
-.theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image][checked] {
-  background: rgba(29, 79, 115, .7); /* Select highlight blue */
+.theme-dark .devtools-toolbarbutton[checked=true]:hover {
+  background: rgba(29, 79, 115, .8); /* Select highlight blue */
   color: #f5f7fa;
 }
+
 .theme-light .devtools-menulist[open=true],
 .theme-light .devtools-toolbarbutton[open=true],
 .theme-light .devtools-toolbarbutton[open=true]:hover,
 .theme-light .devtools-toolbarbutton[open=true]:hover:active,
-.theme-light .devtools-toolbarbutton[checked=true],
-.theme-light .devtools-toolbarbutton[checked=true]:hover,
-.theme-light #toolbox-buttons .devtools-toolbarbutton[text-as-image][checked] {
-  background: rgba(76, 158, 217, .2); /* Select highlight blue */
+.theme-light .devtools-toolbarbutton[checked=true]:hover {
+  background: rgba(76, 158, 217, .4); /* Select highlight blue */
 }
 
 .devtools-option-toolbarbutton {
   -moz-appearance: none;
   list-style-image: url("chrome://browser/skin/devtools/tool-options.svg");
   background: none;
   opacity: .8;
   border: none;
@@ -822,17 +831,16 @@
   filter: url(filters.svg#invert);
 }
 
 /* Since selected backgrounds are blue, we want to use the normal
  * (light) icons. */
 .theme-light .command-button-invertable[checked=true]:not(:active) > image,
 .theme-light .devtools-tab[icon-invertable][selected] > image,
 .theme-light .devtools-tab[icon-invertable][highlighted] > image,
-.theme-light #resume[checked] > image,
 .theme-light #record-snapshot[checked] > image,
 .theme-light #profiler-start[checked] > image {
   filter: none !important;
 }
 
 .theme-light .command-button:hover {
   background-color: inherit;
 }
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -376,21 +376,24 @@ case "$target" in
         if ! test -e $ANDROID_MEDIAROUTER_LIB ; then
             AC_MSG_ERROR([You must download the v7 media router Android support library when targeting Android with native video casting support enabled.  Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_MEDIAROUTER_LIB)])
         fi
         AC_MSG_RESULT([$ANDROID_MEDIAROUTER_LIB])
         AC_SUBST(ANDROID_MEDIAROUTER_LIB)
         AC_SUBST(ANDROID_MEDIAROUTER_RES)
     fi
 
-    MOZ_PATH_PROG(ZIPALIGN, zipalign, :, [$ANDROID_TOOLS])
-    MOZ_PATH_PROG(DX, dx, :, [$ANDROID_BUILD_TOOLS])
-    MOZ_PATH_PROG(AAPT, aapt, :, [$ANDROID_BUILD_TOOLS])
-    MOZ_PATH_PROG(AIDL, aidl, :, [$ANDROID_BUILD_TOOLS])
-    MOZ_PATH_PROG(ADB, adb, :, [$ANDROID_PLATFORM_TOOLS])
+    dnl Google has a history of moving the Android tools around.  We don't
+    dnl care where they are, so let's try to find them anywhere we can.
+    ALL_ANDROID_TOOLS_PATHS="$ANDROID_TOOLS:$ANDROID_BUILD_TOOLS:$ANDROID_PLATFORM_TOOLS"
+    MOZ_PATH_PROG(ZIPALIGN, zipalign, :, [$ALL_ANDROID_TOOLS_PATHS])
+    MOZ_PATH_PROG(DX, dx, :, [$ALL_ANDROID_TOOLS_PATHS])
+    MOZ_PATH_PROG(AAPT, aapt, :, [$ALL_ANDROID_TOOLS_PATHS])
+    MOZ_PATH_PROG(AIDL, aidl, :, [$ALL_ANDROID_TOOLS_PATHS])
+    MOZ_PATH_PROG(ADB, adb, :, [$ALL_ANDROID_TOOLS_PATHS])
 
     if test -z "$ZIPALIGN" -o "$ZIPALIGN" = ":"; then
       AC_MSG_ERROR([The program zipalign was not found.  Use --with-android-sdk={android-sdk-dir}.])
     fi
     if test -z "$DX" -o "$DX" = ":"; then
       AC_MSG_ERROR([The program dx was not found.  Use --with-android-sdk={android-sdk-dir}.])
     fi
     if test -z "$AAPT" -o "$AAPT" = ":"; then
--- a/mobile/android/base/distribution/ReferrerReceiver.java
+++ b/mobile/android/base/distribution/ReferrerReceiver.java
@@ -4,28 +4,34 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.distribution;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.mozglue.RobocopTarget;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
 
 public class ReferrerReceiver extends BroadcastReceiver {
     private static final String LOGTAG = "GeckoReferrerReceiver";
 
     private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER";
 
+    // Sent when we're done.
+    @RobocopTarget
+    public static final String ACTION_REFERRER_RECEIVED = "org.mozilla.fennec.REFERRER_RECEIVED";
+
     /**
      * If the install intent has this source, we'll track the campaign ID.
      */
     private static final String MOZILLA_UTM_SOURCE = "mozilla";
 
     /**
      * If the install intent has this campaign, we'll load the specified distribution.
      */
@@ -47,16 +53,20 @@ public class ReferrerReceiver extends Br
         } else {
             Log.d(LOGTAG, "Not downloading distribution: non-matching campaign.");
         }
 
         // If this is a Mozilla campaign, pass the campaign along to Gecko.
         if (TextUtils.equals(referrer.source, MOZILLA_UTM_SOURCE)) {
             propagateMozillaCampaign(referrer);
         }
+
+        // Broadcast a secondary, local intent to allow test code to respond.
+        final Intent receivedIntent = new Intent(ACTION_REFERRER_RECEIVED);
+        LocalBroadcastManager.getInstance(context).sendBroadcast(receivedIntent);
     }
 
 
     private void propagateMozillaCampaign(ReferrerDescriptor referrer) {
         if (referrer.campaign == null) {
             return;
         }
 
--- a/mobile/android/base/tests/testDistribution.java
+++ b/mobile/android/base/tests/testDistribution.java
@@ -12,23 +12,27 @@ import java.util.jar.JarInputStream;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.Actions;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.distribution.ReferrerDescriptor;
+import org.mozilla.gecko.distribution.ReferrerReceiver;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Activity;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.SharedPreferences;
+import android.support.v4.content.LocalBroadcastManager;
 import android.util.Log;
 
 /**
  * Tests distribution customization.
  * mock-package.zip should contain the following directory structure:
  *
  *   distribution/
  *     preferences.json
@@ -134,90 +138,102 @@ public class testDistribution extends Co
         setTestLocale("en-US");
         clearDistributionPref();
         doTestValidReferrerIntent();
 
         clearDistributionPref();
         doTestInvalidReferrerIntent();
     }
 
+    private void doReferrerTest(String ref, final TestableDistribution distribution, final Runnable distributionReady) throws InterruptedException {
+        final Intent intent = new Intent(ACTION_INSTALL_REFERRER);
+        intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER);
+        intent.putExtra("referrer", ref);
+
+        final BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                Log.i(LOGTAG, "Test received " + intent.getAction());
+
+                ThreadUtils.postToBackgroundThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        distribution.addOnDistributionReadyCallback(distributionReady);
+                        distribution.go();
+                    }
+                });
+            }
+        };
+
+        IntentFilter intentFilter = new IntentFilter(ReferrerReceiver.ACTION_REFERRER_RECEIVED);
+        final LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(mActivity);
+        localBroadcastManager.registerReceiver(receiver, intentFilter);
+
+        Log.i(LOGTAG, "Broadcasting referrer intent.");
+        try {
+            mActivity.sendBroadcast(intent, null);
+            synchronized (distribution) {
+                distribution.wait(WAIT_TIMEOUT_MSEC);
+            }
+        } finally {
+            localBroadcastManager.unregisterReceiver(receiver);
+        }
+    }
+
     public void doTestValidReferrerIntent() throws Exception {
-        // Send the faux-download intent.
         // Equivalent to
         // am broadcast -a com.android.vending.INSTALL_REFERRER \
         //              -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \
         //              --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution"
         final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution";
-        final Intent intent = new Intent(ACTION_INSTALL_REFERRER);
-        intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER);
-        intent.putExtra("referrer", ref);
-        mActivity.sendBroadcast(intent);
-
-        // Wait for the intent to be processed.
         final TestableDistribution distribution = new TestableDistribution(mActivity);
-
-        final Object wait = new Object();
-        distribution.addOnDistributionReadyCallback(new Runnable() {
+        final Runnable distributionReady = new Runnable() {
             @Override
             public void run() {
+                Log.i(LOGTAG, "Test told distribution is ready.");
                 mAsserter.ok(!distribution.exists(), "Not processed.", "No download because we're offline.");
                 ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
                 mAsserter.dumpLog("Referrer was " + referrerValue);
                 mAsserter.is(referrerValue.content, "testcontent", "Referrer content");
                 mAsserter.is(referrerValue.medium, "testmedium", "Referrer medium");
                 mAsserter.is(referrerValue.campaign, "distribution", "Referrer campaign");
-                synchronized (wait) {
-                    wait.notifyAll();
+                synchronized (distribution) {
+                    distribution.notifyAll();
                 }
             }
-        });
+        };
 
-        distribution.go();
-        synchronized (wait) {
-            wait.wait(WAIT_TIMEOUT_MSEC);
-        }
+        doReferrerTest(ref, distribution, distributionReady);
     }
 
     /**
      * Test processing if the campaign isn't "distribution". The intent shouldn't
      * result in a download, and won't be saved as the temporary referrer,
      * even if we *do* include it in a Campaign:Set message.
      */
     public void doTestInvalidReferrerIntent() throws Exception {
-        // Send the faux-download intent.
         // Equivalent to
         // am broadcast -a com.android.vending.INSTALL_REFERRER \
         //              -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \
         //              --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname"
         final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname";
-        final Intent intent = new Intent(ACTION_INSTALL_REFERRER);
-        intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER);
-        intent.putExtra("referrer", ref);
-        mActivity.sendBroadcast(intent);
-
-        // Wait for the intent to be processed.
         final TestableDistribution distribution = new TestableDistribution(mActivity);
-
-        final Object wait = new Object();
-        distribution.addOnDistributionReadyCallback(new Runnable() {
+        final Runnable distributionReady = new Runnable() {
             @Override
             public void run() {
                 mAsserter.ok(!distribution.exists(), "Not processed.", "No download because campaign was wrong.");
                 ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
                 mAsserter.is(referrerValue, null, "No referrer.");
-                synchronized (wait) {
-                    wait.notifyAll();
+                synchronized (distribution) {
+                    distribution.notifyAll();
                 }
             }
-        });
+        };
 
-        distribution.go();
-        synchronized (wait) {
-            wait.wait(WAIT_TIMEOUT_MSEC);
-        }
+        doReferrerTest(ref, distribution, distributionReady);
     }
 
     // Initialize the distribution from the mock package.
     private void initDistribution(String aPackagePath) {
         // Call Distribution.init with the mock package.
         Actions.EventExpecter distributionSetExpecter = mActions.expectGeckoEvent("Distribution:Set:OK");
         Distribution.init(mActivity, aPackagePath, "prefs-" + System.currentTimeMillis());
         distributionSetExpecter.blockForEvent();
--- a/mobile/android/base/util/ThreadUtils.java
+++ b/mobile/android/base/util/ThreadUtils.java
@@ -149,20 +149,26 @@ public final class ThreadUtils {
         final Thread currentThread = Thread.currentThread();
         final long currentThreadId = currentThread.getId();
         final long expectedThreadId = expectedThread.getId();
 
         if ((currentThreadId == expectedThreadId) == expected) {
             return;
         }
 
-        final String message = "Expected thread " +
-                               expectedThreadId + " (\"" + expectedThread.getName() +
-                               "\"), but running on thread " +
-                               currentThreadId + " (\"" + currentThread.getName() + ")";
+        final String message;
+        if (expected) {
+            message = "Expected thread " + expectedThreadId +
+                      " (\"" + expectedThread.getName() + "\"), but running on thread " +
+                      currentThreadId + " (\"" + currentThread.getName() + "\")";
+        } else {
+            message = "Expected anything but " + expectedThreadId +
+                      " (\"" + expectedThread.getName() + "\"), but running there.";
+        }
+
         final IllegalThreadStateException e = new IllegalThreadStateException(message);
 
         switch (behavior) {
         case THROW:
             throw e;
         default:
             Log.e(LOGTAG, "Method called on wrong thread!", e);
         }
--- a/mobile/android/base/widget/GeckoSwipeRefreshLayout.java
+++ b/mobile/android/base/widget/GeckoSwipeRefreshLayout.java
@@ -191,16 +191,22 @@ public class GeckoSwipeRefreshLayout ext
         a.recycle();
     }
 
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
         removeCallbacks(mCancel);
         removeCallbacks(mReturnToStartPosition);
+
+        // Sometimes the inner view doesn't get a proper layout
+        // pass when re-attached to the view tree (see bug 1010986).
+        if (getChildCount() > 0) {
+            getChildAt(0).forceLayout();
+        }
     }
 
     @Override
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         removeCallbacks(mReturnToStartPosition);
         removeCallbacks(mCancel);
     }
--- a/toolkit/devtools/event-emitter.js
+++ b/toolkit/devtools/event-emitter.js
@@ -148,17 +148,21 @@ EventEmitter.prototype = {
   logEvent: function(aEvent, args) {
     let logging = isWorker ? true : Services.prefs.getBoolPref("devtools.dump.emit");
 
     if (logging) {
       let caller, func, path;
       if (!isWorker) {
         caller = components.stack.caller.caller;
         func = caller.name;
-        path = caller.filename.split(/ -> /)[1] + ":" + caller.lineNumber;
+        let file = caller.filename;
+        if (file.contains(" -> ")) {
+          file = caller.filename.split(/ -> /)[1];
+        }
+        path = file + ":" + caller.lineNumber;
       }
 
       let argOut = "(";
       if (args.length === 1) {
         argOut += aEvent;
       }
 
       let out = "EMITTING: ";