Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Mon, 07 Jul 2014 15:20:32 +0200
changeset 192678 b2fd5a214d2ae521b60a1acb4ff7c3f648da2d52
parent 192677 f81fe2b04fefb76f88bbce9952ad80112968c4f6 (current diff)
parent 192595 085eea991bb9fdd51221b20e46f3f51156eda1d7 (diff)
child 192679 63654e6fa9f90dd00658df47d6f274790962069b
push idunknown
push userunknown
push dateunknown
milestone33.0a1
Merge mozilla-central to mozilla-inbound
browser/base/content/test/general/browser_popupNotification.js
browser/devtools/framework/toolbox.js
dom/bluetooth2/tests/marionette/test_dom_BluetoothAdapter_discovery.js
dom/bluetooth2/tests/marionette/test_dom_BluetoothAdapter_getters.js
dom/bluetooth2/tests/marionette/test_dom_BluetoothAdapter_setters.js
dom/bluetooth2/tests/marionette/test_dom_BluetoothManager_adapteradded.js
dom/bluetooth2/tests/marionette/test_dom_BluetoothManager_enabled.js
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="93daa354671a698634a3dc661c8c9dcb7d824c31"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="99f56d9db3cd37c684b01de6fed786421f47e2b7"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="bf9aaf39dd5a6491925a022db167c460f8207d34"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="93daa354671a698634a3dc661c8c9dcb7d824c31"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="99f56d9db3cd37c684b01de6fed786421f47e2b7"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="276ce45e78b09c4a4ee643646f691d22804754c1">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="93daa354671a698634a3dc661c8c9dcb7d824c31"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="99f56d9db3cd37c684b01de6fed786421f47e2b7"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="93daa354671a698634a3dc661c8c9dcb7d824c31"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="99f56d9db3cd37c684b01de6fed786421f47e2b7"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="bf9aaf39dd5a6491925a022db167c460f8207d34"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="93daa354671a698634a3dc661c8c9dcb7d824c31"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="99f56d9db3cd37c684b01de6fed786421f47e2b7"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "333f877ae4029d7cb1a0893f89da484d8e3cc14f", 
+    "revision": "278dd1b102a39cf2c48f11fe3038eaf8f0779d7d", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="93daa354671a698634a3dc661c8c9dcb7d824c31"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="99f56d9db3cd37c684b01de6fed786421f47e2b7"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="93daa354671a698634a3dc661c8c9dcb7d824c31"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="99f56d9db3cd37c684b01de6fed786421f47e2b7"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="93daa354671a698634a3dc661c8c9dcb7d824c31"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="99f56d9db3cd37c684b01de6fed786421f47e2b7"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="93daa354671a698634a3dc661c8c9dcb7d824c31"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="99f56d9db3cd37c684b01de6fed786421f47e2b7"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -400,16 +400,18 @@
 @BINPATH@/components/nsSidebar.js
 
 ; WiFi, NetworkManager, NetworkStats
 #ifdef MOZ_WIDGET_GONK
 @BINPATH@/components/DOMWifiManager.js
 @BINPATH@/components/DOMWifiManager.manifest
 @BINPATH@/components/DOMWifiP2pManager.js
 @BINPATH@/components/DOMWifiP2pManager.manifest
+@BINPATH@/components/EthernetManager.js
+@BINPATH@/components/EthernetManager.manifest
 @BINPATH@/components/NetworkInterfaceListService.js
 @BINPATH@/components/NetworkInterfaceListService.manifest
 @BINPATH@/components/NetworkManager.js
 @BINPATH@/components/NetworkManager.manifest
 @BINPATH@/components/NetworkService.js
 @BINPATH@/components/NetworkService.manifest
 @BINPATH@/components/NetworkStatsManager.js
 @BINPATH@/components/NetworkStatsManager.manifest
--- 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/components/preferences/applications.js
+++ b/browser/components/preferences/applications.js
@@ -1401,17 +1401,17 @@ var gApplicationsPane = {
       internalMenuItem.setAttribute("label", label);
       internalMenuItem.setAttribute("tooltiptext", label);
       internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask");
       menuPopup.appendChild(internalMenuItem);
     }
 
     {
       var askMenuItem = document.createElement("menuitem");
-      askMenuItem.setAttribute("alwaysAsk", "true");
+      askMenuItem.setAttribute("action", Ci.nsIHandlerInfo.alwaysAsk);
       let label;
       if (isFeedType(handlerInfo.type))
         label = this._prefsBundle.getFormattedString("previewInApp",
                                                      [this._brandShortName]);
       else
         label = this._prefsBundle.getString("alwaysAsk");
       askMenuItem.setAttribute("label", label);
       askMenuItem.setAttribute("tooltiptext", label);
--- a/browser/components/preferences/in-content/applications.js
+++ b/browser/components/preferences/in-content/applications.js
@@ -1393,17 +1393,17 @@ var gApplicationsPane = {
       internalMenuItem.setAttribute("label", label);
       internalMenuItem.setAttribute("tooltiptext", label);
       internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask");
       menuPopup.appendChild(internalMenuItem);
     }
 
     {
       var askMenuItem = document.createElement("menuitem");
-      askMenuItem.setAttribute("alwaysAsk", "true");
+      askMenuItem.setAttribute("action", Ci.nsIHandlerInfo.alwaysAsk);
       let label;
       if (isFeedType(handlerInfo.type))
         label = this._prefsBundle.getFormattedString("previewInApp",
                                                      [this._brandShortName]);
       else
         label = this._prefsBundle.getString("alwaysAsk");
       askMenuItem.setAttribute("label", label);
       askMenuItem.setAttribute("tooltiptext", label);
--- 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
@@ -290,16 +290,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/dom/apps/src/AppsServiceChild.jsm
+++ b/dom/apps/src/AppsServiceChild.jsm
@@ -3,76 +3,345 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
-// This module exposes a subset of the functionnalities of the parent DOM
-// Registry to content processes, to be be used from the AppsService component.
+// This module exposes a subset of the functionalities of the parent DOM
+// Registry to content processes, to be used from the AppsService component.
 
-this.EXPORTED_SYMBOLS = ["DOMApplicationRegistry"];
+this.EXPORTED_SYMBOLS = ["DOMApplicationRegistry", "WrappedManifestCache"];
 
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 function debug(s) {
   //dump("-*- AppsServiceChild.jsm: " + s + "\n");
 }
 
+const APPS_IPC_MSG_NAMES = [
+  "Webapps:AddApp",
+  "Webapps:RemoveApp",
+  "Webapps:UpdateApp",
+  "Webapps:CheckForUpdate:Return:KO",
+  "Webapps:FireEvent",
+  "Webapps:UpdateState"
+];
+
+// A simple cache for the wrapped manifests.
+this.WrappedManifestCache = {
+  _cache: { },
+
+  // Gets an entry from the cache, and populates the cache if needed.
+  get: function mcache_get(aManifestURL, aManifest, aWindow, aInnerWindowID) {
+    if (!aManifest) {
+      return;
+    }
+
+    if (!(aManifestURL in this._cache)) {
+      this._cache[aManifestURL] = { };
+    }
+
+    let winObjs = this._cache[aManifestURL];
+    if (!(aInnerWindowID in winObjs)) {
+      winObjs[aInnerWindowID] = Cu.cloneInto(aManifest, aWindow);
+    }
+
+    return winObjs[aInnerWindowID];
+  },
+
+  // Invalidates an entry in the cache.
+  evict: function mcache_evict(aManifestURL, aInnerWindowID) {
+    debug("Evicting manifest " + aManifestURL + " window ID " +
+          aInnerWindowID);
+    if (aManifestURL in this._cache) {
+      let winObjs = this._cache[aManifestURL];
+      if (aInnerWindowID in winObjs) {
+        delete winObjs[aInnerWindowID];
+      }
+
+      if (Object.keys(winObjs).length == 0) {
+        delete this._cache[aManifestURL];
+      }
+    }
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    // Clear the cache on memory pressure.
+    this._cache = { };
+    Cu.forceGC();
+  },
+
+  init: function() {
+    Services.obs.addObserver(this, "memory-pressure", false);
+  }
+};
+
+this.WrappedManifestCache.init();
+
+
+// DOMApplicationRegistry keeps a cache containing a list of apps in the device.
+// This information is updated with the data received from the main process and
+// it is queried by the DOM objects to set their state.
+// This module handle all the messages broadcasted from the parent process,
+// including DOM events, which are dispatched to the corresponding DOM objects.
+
 this.DOMApplicationRegistry = {
+  // DOMApps will hold a list of arrays of weak references to
+  // mozIDOMApplication objects indexed by manifest URL.
+  DOMApps: {},
+
+  ready: false,
+  webapps: null,
+
   init: function init() {
-    debug("init");
     this.cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
                   .getService(Ci.nsISyncMessageSender);
 
-    ["Webapps:AddApp", "Webapps:RemoveApp"].forEach((function(aMsgName) {
+    APPS_IPC_MSG_NAMES.forEach((function(aMsgName) {
       this.cpmm.addMessageListener(aMsgName, this);
     }).bind(this));
 
+    this.cpmm.sendAsyncMessage("Webapps:RegisterForMessages", {
+      messages: APPS_IPC_MSG_NAMES
+    });
+
     // We need to prime the cache with the list of apps.
-    // XXX shoud we do this async and block callers if it's not yet there?
-    this.webapps = this.cpmm.sendSyncMessage("Webapps:GetList", { })[0];
-
+    let list = this.cpmm.sendSyncMessage("Webapps:GetList", { })[0];
+    this.webapps = list.webapps;
     // We need a fast mapping from localId -> app, so we add an index.
+    // We also add the manifest to the app object.
     this.localIdIndex = { };
     for (let id in this.webapps) {
       let app = this.webapps[id];
       this.localIdIndex[app.localId] = app;
+      app.manifest = list.manifests[id];
     }
 
     Services.obs.addObserver(this, "xpcom-shutdown", false);
   },
 
   observe: function(aSubject, aTopic, aData) {
-    // cpmm.addMessageListener causes the DOMApplicationRegistry object to live
-    // forever if we don't clean up properly.
+    // cpmm.addMessageListener causes the DOMApplicationRegistry object to
+    // live forever if we don't clean up properly.
     this.webapps = null;
-    ["Webapps:AddApp", "Webapps:RemoveApp"].forEach((function(aMsgName) {
+    this.DOMApps = null;
+
+    APPS_IPC_MSG_NAMES.forEach((aMsgName) => {
       this.cpmm.removeMessageListener(aMsgName, this);
-    }).bind(this));
+    });
+
+    this.cpmm.sendAsyncMessage("Webapps:UnregisterForMessages",
+                               APPS_IPC_MSG_NAMES)
   },
 
   receiveMessage: function receiveMessage(aMessage) {
     debug("Received " + aMessage.name + " message.");
-    let msg = aMessage.json;
+    let msg = aMessage.data;
     switch (aMessage.name) {
       case "Webapps:AddApp":
         this.webapps[msg.id] = msg.app;
         this.localIdIndex[msg.app.localId] = msg.app;
+        if (msg.manifest) {
+          this.webapps[msg.id].manifest = msg.manifest;
+        }
         break;
       case "Webapps:RemoveApp":
+        delete this.DOMApps[this.webapps[msg.id].manifestURL];
         delete this.localIdIndex[this.webapps[msg.id].localId];
         delete this.webapps[msg.id];
         break;
+      case "Webapps:UpdateApp":
+        let app = this.webapps[msg.oldId];
+        if (!app) {
+          return;
+        }
+
+        if (msg.app) {
+          for (let prop in msg.app) {
+            app[prop] = msg.app[prop];
+          }
+        }
+
+        this.webapps[msg.newId] = app;
+        this.localIdIndex[app.localId] = app;
+        delete this.webapps[msg.oldId];
+
+        let apps = this.DOMApps[msg.app.manifestURL];
+        if (!apps) {
+          return;
+        }
+        for (let i = 0; i < apps.length; i++) {
+          let domApp = apps[i].get();
+          if (!domApp) {
+            apps.splice(i);
+            continue;
+          }
+          domApp._proxy = new Proxy(domApp, {
+            get: function(target, prop) {
+              if (!DOMApplicationRegistry.webapps[msg.newId]) {
+                return;
+              }
+              return DOMApplicationRegistry.webapps[msg.newId][prop];
+            },
+            set: function(target, prop, val) {
+              if (!DOMApplicationRegistry.webapps[msg.newId]) {
+                return;
+              }
+              DOMApplicationRegistry.webapps[msg.newId][prop] = val;
+              return;
+            },
+          });
+        }
+        break;
+      case "Webapps:FireEvent":
+        this._fireEvent(aMessage);
+        break;
+      case "Webapps:UpdateState":
+        this._updateState(msg);
+        break;
+      case "Webapps:CheckForUpdate:Return:KO":
+        let DOMApps = this.DOMApps[msg.manifestURL];
+        if (!DOMApps || !msg.requestID) {
+          return;
+        }
+        DOMApps.forEach((DOMApp) => {
+          let domApp = DOMApp.get();
+          if (domApp && msg.requestID) {
+            domApp._fireRequestResult(aMessage, true /* aIsError */);
+          }
+        });
+        break;
     }
   },
 
+  /**
+   * mozIDOMApplication management
+   */
+
+  // Every time a DOM app is created, we save a weak reference to it that will
+  // be used to dispatch events and fire request results.
+  addDOMApp: function(aApp, aManifestURL, aId) {
+    let weakRef = Cu.getWeakReference(aApp);
+
+    if (!this.DOMApps[aManifestURL]) {
+      this.DOMApps[aManifestURL] = [];
+    }
+
+    let apps = this.DOMApps[aManifestURL];
+
+    // Get rid of dead weak references.
+    for (let i = 0; i < apps.length; i++) {
+      if (!apps[i].get()) {
+        apps.splice(i);
+      }
+    }
+
+    apps.push(weakRef);
+
+    // Each DOM app contains a proxy object used to build their state. We
+    // return the handler for this proxy object with traps to get and set
+    // app properties kept in the DOMApplicationRegistry app cache.
+    return {
+      get: function(target, prop) {
+        if (!DOMApplicationRegistry.webapps[aId]) {
+          return;
+        }
+        return DOMApplicationRegistry.webapps[aId][prop];
+      },
+      set: function(target, prop, val) {
+        if (!DOMApplicationRegistry.webapps[aId]) {
+          return;
+        }
+        DOMApplicationRegistry.webapps[aId][prop] = val;
+        return;
+      },
+    };
+  },
+
+  _fireEvent: function(aMessage) {
+    let msg = aMessage.data;
+    debug("_fireEvent " + JSON.stringify(msg));
+    if (!this.DOMApps || !msg.manifestURL || !msg.eventType) {
+      return;
+    }
+
+    let DOMApps = this.DOMApps[msg.manifestURL];
+    if (!DOMApps) {
+      return;
+    }
+
+    // The parent might ask childs to trigger more than one event in one
+    // shot, so in order to avoid needless IPC we allow an array for the
+    // 'eventType' IPC message field.
+    if (!Array.isArray(msg.eventType)) {
+      msg.eventType = [msg.eventType];
+    }
+
+    DOMApps.forEach((DOMApp) => {
+      let domApp = DOMApp.get();
+      if (!domApp) {
+        return;
+      }
+      msg.eventType.forEach((aEventType) => {
+        if ('on' + aEventType in domApp) {
+          domApp._fireEvent(aEventType);
+        }
+      });
+
+      if (msg.requestID) {
+        aMessage.data.result = msg.manifestURL;
+        domApp._fireRequestResult(aMessage);
+      }
+    });
+  },
+
+  _updateState: function(aMessage) {
+    if (!this.DOMApps || !aMessage.id) {
+      return;
+    }
+
+    let app = this.webapps[aMessage.id];
+    if (!app) {
+      return;
+    }
+
+    if (aMessage.app) {
+      for (let prop in aMessage.app) {
+        app[prop] = aMessage.app[prop];
+      }
+    }
+
+    if ("error" in aMessage) {
+      app.downloadError = aMessage.error;
+    }
+
+    if (aMessage.manifest) {
+      app.manifest = aMessage.manifest;
+      // Evict the wrapped manifest cache for all the affected DOM objects.
+      let DOMApps = this.DOMApps[app.manifestURL];
+      if (!DOMApps) {
+        return;
+      }
+      DOMApps.forEach((DOMApp) => {
+        let domApp = DOMApp.get();
+        if (!domApp) {
+          return;
+        }
+        WrappedManifestCache.evict(app.manifestURL, domApp.innerWindowID);
+      });
+    }
+  },
+
+  /**
+   * nsIAppsService API
+   */
   getAppByManifestURL: function getAppByManifestURL(aManifestURL) {
     debug("getAppByManifestURL " + aManifestURL);
     return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL);
   },
 
   getAppLocalIdByManifestURL: function getAppLocalIdByManifestURL(aManifestURL) {
     debug("getAppLocalIdByManifestURL " + aManifestURL);
     return AppsUtils.getAppLocalIdByManifestURL(this.webapps, aManifestURL);
@@ -84,17 +353,17 @@ this.DOMApplicationRegistry = {
   },
 
   getAppLocalIdByStoreId: function(aStoreId) {
     debug("getAppLocalIdByStoreId:" + aStoreId);
     return AppsUtils.getAppLocalIdByStoreId(this.webapps, aStoreId);
   },
 
   getAppByLocalId: function getAppByLocalId(aLocalId) {
-    debug("getAppByLocalId " + aLocalId);
+    debug("getAppByLocalId " + aLocalId + " - ready: " + this.ready);
     let app = this.localIdIndex[aLocalId];
     if (!app) {
       debug("Ouch, No app!");
       return null;
     }
 
     return new mozIApplication(app);
   },
--- a/dom/apps/src/Webapps.js
+++ b/dom/apps/src/Webapps.js
@@ -7,16 +7,17 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import("resource://gre/modules/BrowserElementPromptService.jsm");
+Cu.import("resource://gre/modules/AppsServiceChild.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
 function convertAppsArray(aApps, aWindow) {
   let apps = new aWindow.Array();
   for (let i = 0; i < aApps.length; i++) {
@@ -273,134 +274,121 @@ WebappsRegistry.prototype = {
                                     flags: Ci.nsIClassInfo.DOM_OBJECT,
                                     classDescription: "Webapps Registry"})
 }
 
 /**
   * mozIDOMApplication object
   */
 
-// A simple cache for the wrapped manifests.
-let manifestCache = {
-  _cache: { },
-
-  // Gets an entry from the cache, and populates the cache if needed.
-  get: function mcache_get(aManifestURL, aManifest, aWindow, aInnerWindowID) {
-    if (!(aManifestURL in this._cache)) {
-      this._cache[aManifestURL] = { };
-    }
-
-    let winObjs = this._cache[aManifestURL];
-    if (!(aInnerWindowID in winObjs)) {
-      winObjs[aInnerWindowID] = Cu.cloneInto(aManifest, aWindow);
-    }
-
-    return winObjs[aInnerWindowID];
-  },
-
-  // Invalidates an entry in the cache.
-  evict: function mcache_evict(aManifestURL, aInnerWindowID) {
-    if (aManifestURL in this._cache) {
-      let winObjs = this._cache[aManifestURL];
-      if (aInnerWindowID in winObjs) {
-        delete winObjs[aInnerWindowID];
-      }
-
-      if (Object.keys(winObjs).length == 0) {
-        delete this._cache[aManifestURL];
-      }
-    }
-  },
-
-  observe: function(aSubject, aTopic, aData) {
-    // Clear the cache on memory pressure.
-    this._cache = { };
-  },
-
-  init: function() {
-    Services.obs.addObserver(this, "memory-pressure", false);
-  }
-};
-
 function createApplicationObject(aWindow, aApp) {
-  let app = Cc["@mozilla.org/webapps/application;1"].createInstance(Ci.mozIDOMApplication);
+  let app = Cc["@mozilla.org/webapps/application;1"]
+              .createInstance(Ci.mozIDOMApplication);
   app.wrappedJSObject.init(aWindow, aApp);
   return app;
 }
 
 function WebappsApplication() {
   this.wrappedJSObject = this;
 }
 
 WebappsApplication.prototype = {
   __proto__: DOMRequestIpcHelper.prototype,
 
   init: function(aWindow, aApp) {
+    let proxyHandler = DOMApplicationRegistry.addDOMApp(this,
+                                                        aApp.manifestURL,
+                                                        aApp.id);
+    this._proxy = new Proxy(this, proxyHandler);
+
     this._window = aWindow;
-    let principal = this._window.document.nodePrincipal;
-    this._appStatus = principal.appStatus;
-    this.origin = aApp.origin;
-    this._manifest = aApp.manifest;
-    this._updateManifest = aApp.updateManifest;
-    this.manifestURL = aApp.manifestURL;
-    this.receipts = aApp.receipts;
-    this.installOrigin = aApp.installOrigin;
-    this.installTime = aApp.installTime;
-    this.installState = aApp.installState || "installed";
-    this.removable = aApp.removable;
-    this.lastUpdateCheck = aApp.lastUpdateCheck ? aApp.lastUpdateCheck
-                                                : Date.now();
-    this.updateTime = aApp.updateTime ? aApp.updateTime
-                                      : aApp.installTime;
-    this.progress = NaN;
-    this.downloadAvailable = aApp.downloadAvailable;
-    this.downloading = aApp.downloading;
-    this.readyToApplyDownload = aApp.readyToApplyDownload;
-    this.downloadSize = aApp.downloadSize || 0;
 
     this._onprogress = null;
     this._ondownloadsuccess = null;
     this._ondownloaderror = null;
     this._ondownloadavailable = null;
     this._ondownloadapplied = null;
 
-    this._downloadError = null;
+    this.initDOMRequestHelper(aWindow);
+  },
+
+  get _appStatus() {
+    return this._proxy.appStatus;
+  },
+
+  get downloadAvailable() {
+    return this._proxy.downloadAvailable;
+  },
+
+  get downloading() {
+    return this._proxy.downloading;
+  },
 
-    this.initDOMRequestHelper(aWindow, [
-      { name: "Webapps:CheckForUpdate:Return:KO", weakRef: true },
-      { name: "Webapps:Connect:Return:OK", weakRef: true },
-      { name: "Webapps:Connect:Return:KO", weakRef: true },
-      { name: "Webapps:FireEvent", weakRef: true },
-      { name: "Webapps:GetConnections:Return:OK", weakRef: true },
-      { name: "Webapps:UpdateState", weakRef: true }
-    ]);
+  get downloadSize() {
+    return this._proxy.downloadSize;
+  },
+
+  get installOrigin() {
+    return this._proxy.installOrigin;
+  },
+
+  get installState() {
+    return this._proxy.installState;
+  },
+
+  get installTime() {
+    return this._proxy.installTime;
+  },
 
-    cpmm.sendAsyncMessage("Webapps:RegisterForMessages", {
-      messages: ["Webapps:FireEvent",
-                 "Webapps:UpdateState"],
-      app: {
-        id: this.id,
-        manifestURL: this.manifestURL,
-        installState: this.installState,
-        downloading: this.downloading
-      }
-    });
+  get lastUpdateCheck() {
+    return this._proxy.lastUpdateCheck;
+  },
+
+  get manifestURL() {
+    return this._proxy.manifestURL;
+  },
+
+  get origin() {
+    return this._proxy.origin;
+  },
+
+  get progress() {
+    return this._proxy.progress;
+  },
+
+  get readyToApplyDownload() {
+    return this._proxy.readyToApplyDownload;
+  },
+
+  get receipts() {
+    return this._proxy.receipts;
+  },
+
+  set receipts(aReceipts) {
+    this._proxy.receipts = aReceipts;
+  },
+
+  get removable() {
+    return this._proxy.removable;
+  },
+
+  get updateTime() {
+    return this._proxy.updateTime;
   },
 
   get manifest() {
-    return manifestCache.get(this.manifestURL,
-                             this._manifest,
-                             this._window,
-                             this.innerWindowID);
+    return WrappedManifestCache.get(this.manifestURL,
+                                    this._proxy.manifest,
+                                    this._window,
+                                    this.innerWindowID);
   },
 
   get updateManifest() {
-    return this.updateManifest =
-      this._updateManifest ? Cu.cloneInto(this._updateManifest, this._window)
-                           : null;
+    return this._proxy.updateManifest ?
+      Cu.cloneInto(this._proxy.updateManifest, this._window) : null;
   },
 
   set onprogress(aCallback) {
     this._onprogress = aCallback;
   },
 
   get onprogress() {
     return this._onprogress;
@@ -435,20 +423,20 @@ WebappsApplication.prototype = {
   },
 
   get ondownloadapplied() {
     return this._ondownloadapplied;
   },
 
   get downloadError() {
     // Only return DOMError when we have an error.
-    if (!this._downloadError) {
+    if (!this._proxy.downloadError) {
       return null;
     }
-    return new this._window.DOMError(this._downloadError);
+    return new this._window.DOMError(this._proxy.downloadError);
   },
 
   download: function() {
     cpmm.sendAsyncMessage("Webapps:Download",
                           { manifestURL: this.manifestURL });
   },
 
   cancelDownload: function() {
@@ -480,51 +468,55 @@ WebappsApplication.prototype = {
   },
 
   clearBrowserData: function() {
     let request = this.createRequest();
     let browserChild =
       BrowserElementPromptService.getBrowserElementChildForWindow(this._window);
     if (browserChild) {
       this.addMessageListeners("Webapps:ClearBrowserData:Return");
-      browserChild.messageManager.sendAsyncMessage(
-        "Webapps:ClearBrowserData",
-        { manifestURL: this.manifestURL,
-          oid: this._id,
-          requestID: this.getRequestId(request) }
-      );
+      browserChild.messageManager.sendAsyncMessage("Webapps:ClearBrowserData", {
+        manifestURL: this.manifestURL,
+        oid: this._id,
+        requestID: this.getRequestId(request)
+      });
     } else {
       Services.DOMRequest.fireErrorAsync(request, "NO_CLEARABLE_BROWSER");
     }
     return request;
   },
 
   connect: function(aKeyword, aRules) {
+    this.addMessageListeners(["Webapps:Connect:Return:OK",
+                              "Webapps:Connect:Return:KO"]);
     return this.createPromise(function (aResolve, aReject) {
-      cpmm.sendAsyncMessage("Webapps:Connect",
-                            { keyword: aKeyword,
-                              rules: aRules,
-                              manifestURL: this.manifestURL,
-                              outerWindowID: this._id,
-                              requestID: this.getPromiseResolverId({
-                                resolve: aResolve,
-                                reject: aReject
-                              })});
+      cpmm.sendAsyncMessage("Webapps:Connect", {
+        keyword: aKeyword,
+        rules: aRules,
+        manifestURL: this.manifestURL,
+        outerWindowID: this._id,
+        requestID: this.getPromiseResolverId({
+          resolve: aResolve,
+          reject: aReject
+        })
+      });
     }.bind(this));
   },
 
   getConnections: function() {
+    this.addMessageListeners("Webapps:GetConnections:Return:OK");
     return this.createPromise(function (aResolve, aReject) {
-      cpmm.sendAsyncMessage("Webapps:GetConnections",
-                            { manifestURL: this.manifestURL,
-                              outerWindowID: this._id,
-                              requestID: this.getPromiseResolverId({
-                                resolve: aResolve,
-                                reject: aReject
-                              })});
+      cpmm.sendAsyncMessage("Webapps:GetConnections", {
+        manifestURL: this.manifestURL,
+        outerWindowID: this._id,
+        requestID: this.getPromiseResolverId({
+          resolve: aResolve,
+          reject: aReject
+        })
+      });
     }.bind(this));
   },
 
   addReceipt: function(receipt) {
     let request = this.createRequest();
 
     this.addMessageListeners(["Webapps:AddReceipt:Return:OK",
                               "Webapps:AddReceipt:Return:KO"]);
@@ -563,141 +555,92 @@ WebappsApplication.prototype = {
                                                       oid: this._id,
                                                       requestID: this.getRequestId(request) });
 
     return request;
   },
 
   uninit: function() {
     this._onprogress = null;
-    cpmm.sendAsyncMessage("Webapps:UnregisterForMessages", [
-      "Webapps:FireEvent",
-      "Webapps:UpdateState"
-    ]);
-
-    manifestCache.evict(this.manifestURL, this.innerWindowID);
+    WrappedManifestCache.evict(this.manifestURL, this.innerWindowID);
   },
 
   _fireEvent: function(aName) {
     let handler = this["_on" + aName];
     if (handler) {
       let event = new this._window.MozApplicationEvent(aName, {
         application: this
       });
       try {
         handler.handleEvent(event);
       } catch (ex) {
         dump("Event handler expection " + ex + "\n");
       }
     }
   },
 
-  _updateState: function(aMsg) {
-    if (aMsg.app) {
-      for (let prop in aMsg.app) {
-        this[prop] = aMsg.app[prop];
-      }
+  _fireRequestResult: function(aMessage, aIsError) {
+    let req;
+    let msg = aMessage.data;
+    req = this.takeRequest(msg.requestID);
+    if (!req) {
+      return;
     }
 
-    // Intentional use of 'in' so we unset the error if this is explicitly null.
-    if ('error' in aMsg) {
-      this._downloadError = aMsg.error;
-    }
-
-    if (aMsg.manifest) {
-      this._manifest = aMsg.manifest;
-      manifestCache.evict(this.manifestURL, this.innerWindowID);
-    }
+    aIsError ? Services.DOMRequest.fireError(req, msg.error)
+             : Services.DOMRequest.fireSuccess(req, msg.result);
   },
 
   receiveMessage: function(aMessage) {
     let msg = aMessage.json;
     let req;
     if (aMessage.name == "Webapps:Connect:Return:OK" ||
         aMessage.name == "Webapps:Connect:Return:KO" ||
         aMessage.name == "Webapps:GetConnections:Return:OK") {
       req = this.takePromiseResolver(msg.requestID);
     } else {
       req = this.takeRequest(msg.requestID);
     }
 
-    // ondownload* callbacks should be triggered on all app instances
-    if ((msg.oid != this._id || !req) &&
-        aMessage.name !== "Webapps:FireEvent" &&
-        aMessage.name !== "Webapps:UpdateState") {
+    if (msg.oid !== this._id || !req) {
       return;
     }
 
     switch (aMessage.name) {
       case "Webapps:Launch:Return:KO":
         this.removeMessageListeners(["Webapps:Launch:Return:OK",
                                      "Webapps:Launch:Return:KO"]);
         Services.DOMRequest.fireError(req, "APP_INSTALL_PENDING");
         break;
       case "Webapps:Launch:Return:OK":
         this.removeMessageListeners(["Webapps:Launch:Return:OK",
                                      "Webapps:Launch:Return:KO"]);
         Services.DOMRequest.fireSuccess(req, null);
         break;
-      case "Webapps:CheckForUpdate:Return:KO":
-        Services.DOMRequest.fireError(req, msg.error);
-        break;
-      case "Webapps:FireEvent":
-        if (msg.manifestURL != this.manifestURL) {
-           return;
-        }
-
-        // The parent might ask childs to trigger more than one event in one
-        // shot, so in order to avoid needless IPC we allow an array for the
-        // 'eventType' IPC message field.
-        if (!Array.isArray(msg.eventType)) {
-          msg.eventType = [msg.eventType];
-        }
-
-        msg.eventType.forEach((aEventType) => {
-          // If we are in a successful state clear any past errors.
-          if (aEventType === 'downloadapplied' ||
-              aEventType === 'downloadsuccess') {
-            this._downloadError = null;
-          }
-
-          if ("_on" + aEventType in this) {
-            this._fireEvent(aEventType);
-          } else {
-            dump("Unsupported event type " + aEventType + "\n");
-          }
-        });
-
-        if (req) {
-          Services.DOMRequest.fireSuccess(req, this.manifestURL);
-        }
-        break;
-      case "Webapps:UpdateState":
-        if (msg.manifestURL != this.manifestURL) {
-          return;
-        }
-
-        this._updateState(msg);
-        break;
       case "Webapps:ClearBrowserData:Return":
         this.removeMessageListeners(aMessage.name);
         Services.DOMRequest.fireSuccess(req, null);
         break;
       case "Webapps:Connect:Return:OK":
+        this.removeMessageListeners(["Webapps:Connect:Return:OK",
+                                     "Webapps:Connect:Return:KO"]);
         let messagePorts = [];
         msg.messagePortIDs.forEach((aPortID) => {
           let port = new this._window.MozInterAppMessagePort(aPortID);
           messagePorts.push(port);
         });
         req.resolve(messagePorts);
         break;
       case "Webapps:Connect:Return:KO":
+        this.removeMessageListeners(["Webapps:Connect:Return:OK",
+                                     "Webapps:Connect:Return:KO"]);
         req.reject("No connections registered");
         break;
       case "Webapps:GetConnections:Return:OK":
+        this.removeMessageListeners(aMessage.name);
         let connections = [];
         msg.connections.forEach((aConnection) => {
           let connection =
             new this._window.MozInterAppConnection(aConnection.keyword,
                                                    aConnection.pubAppManifestURL,
                                                    aConnection.subAppManifestURL);
           connections.push(connection);
         });
@@ -869,22 +812,18 @@ WebappsApplicationMgmt.prototype = {
           let app = msg.app;
           let event = new this._window.MozApplicationEvent("applicationinstall",
                            { application : createApplicationObject(this._window, app) });
           this._oninstall.handleEvent(event);
         }
         break;
       case "Webapps:Uninstall:Broadcast:Return:OK":
         if (this._onuninstall) {
-          let detail = {
-            manifestURL: msg.manifestURL,
-            origin: msg.origin
-          };
           let event = new this._window.MozApplicationEvent("applicationuninstall",
-                           { application : createApplicationObject(this._window, detail) });
+                           { application : createApplicationObject(this._window, msg) });
           this._onuninstall.handleEvent(event);
         }
         break;
       case "Webapps:Uninstall:Return:OK":
         Services.DOMRequest.fireSuccess(req, msg.origin);
         break;
       case "Webapps:Uninstall:Return:KO":
         Services.DOMRequest.fireError(req, "NOT_INSTALLED");
@@ -903,12 +842,10 @@ WebappsApplicationMgmt.prototype = {
 
   classInfo: XPCOMUtils.generateCI({classID: Components.ID("{8c1bca96-266f-493a-8d57-ec7a95098c15}"),
                                     contractID: "@mozilla.org/webapps/application-mgmt;1",
                                     interfaces: [Ci.mozIDOMApplicationMgmt],
                                     flags: Ci.nsIClassInfo.DOM_OBJECT,
                                     classDescription: "Webapps Application Mgmt"})
 }
 
-manifestCache.init();
-
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebappsRegistry,
                                                      WebappsApplication]);
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -1136,18 +1136,18 @@ this.DOMApplicationRegistry = {
         break;
       case "Webapps:UnregisterForMessages":
         this.removeMessageListener(msg, mm);
         break;
       case "child-process-shutdown":
         this.removeMessageListener(["Webapps:Internal:AllMessages"], mm);
         break;
       case "Webapps:GetList":
-        this.addMessageListener(["Webapps:AddApp", "Webapps:RemoveApp"], null, mm);
-        return this.webapps;
+        return this.doGetList();
+        break;
       case "Webapps:Download":
         this.startDownload(msg.manifestURL);
         break;
       case "Webapps:CancelDownload":
         this.cancelDownload(msg.manifestURL);
         break;
       case "Webapps:CheckForUpdate":
         this.checkForUpdate(msg, mm);
@@ -1240,16 +1240,48 @@ this.DOMApplicationRegistry = {
       } else {
         deferred.resolve();
       }
     });
 
     return deferred.promise;
   },
 
+  /**
+    * Returns the full list of apps and manifests.
+    */
+  doGetList: function() {
+    let tmp = [];
+
+    for (let id in this.webapps) {
+      tmp.push({ id: id });
+    }
+
+    let res = {};
+    let done = false;
+
+    this._readManifests(tmp).then(
+      function(manifests) {
+        manifests.forEach((item) => {
+          res[item.id] = item.manifest;
+        });
+        done = true;
+      }
+    );
+
+    let thread = Services.tm.currentThread;
+    while (!done) {
+      //debug("before processNextEvent");
+      thread.processNextEvent(/* mayWait */ true);
+      //after("before processNextEvent");
+    }
+    return { webapps: this.webapps, manifests: res };
+  },
+
+
   doLaunch: function (aData, aMm) {
     this.launch(
       aData.manifestURL,
       aData.startPoint,
       aData.timestamp,
       function onsuccess() {
         aMm.sendAsyncMessage("Webapps:Launch:Return:OK", aData);
       },
@@ -1325,17 +1357,17 @@ this.DOMApplicationRegistry = {
     this._saveApps().then(() => {
       this.broadcastMessage("Webapps:UpdateState", {
         app: {
           progress: 0,
           installState: download.previousState,
           downloading: false
         },
         error: error,
-        manifestURL: app.manifestURL,
+        id: app.id
       })
       this.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloaderror",
         manifestURL: app.manifestURL
       });
     });
     AppDownloadManager.remove(aManifestURL);
   },
@@ -1356,17 +1388,17 @@ this.DOMApplicationRegistry = {
       throw new Error("APP_IS_DOWNLOADING");
     }
 
     // If the caller is trying to start a download but we have nothing to
     // download, send an error.
     if (!app.downloadAvailable) {
       this.broadcastMessage("Webapps:UpdateState", {
         error: "NO_DOWNLOAD_AVAILABLE",
-        manifestURL: app.manifestURL
+        id: app.id
       });
       this.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloaderror",
         manifestURL: app.manifestURL
       });
       throw new Error("NO_DOWNLOAD_AVAILABLE");
     }
 
@@ -1404,17 +1436,17 @@ this.DOMApplicationRegistry = {
         debug("No appcache found, sending 'downloaded' for " + aManifestURL);
         app.downloadAvailable = false;
 
         yield this._saveApps();
 
         this.broadcastMessage("Webapps:UpdateState", {
           app: app,
           manifest: jsonManifest,
-          manifestURL: aManifestURL
+          id: app.id
         });
         this.broadcastMessage("Webapps:FireEvent", {
           eventType: "downloadsuccess",
           manifestURL: aManifestURL
         });
       }
 
       return;
@@ -1448,17 +1480,17 @@ this.DOMApplicationRegistry = {
     app.downloadAvailable = false;
     app.readyToApplyDownload = true;
     app.updateTime = Date.now();
 
     yield this._saveApps();
 
     this.broadcastMessage("Webapps:UpdateState", {
       app: app,
-      manifestURL: aManifestURL
+      id: app.id
     });
     this.broadcastMessage("Webapps:FireEvent", {
       eventType: "downloadsuccess",
       manifestURL: aManifestURL
     });
     if (app.installState == "pending") {
       // We restarted a failed download, apply it automatically.
       this.applyDownload(aManifestURL);
@@ -1550,17 +1582,17 @@ this.DOMApplicationRegistry = {
           manifestURL: app.manifestURL },
         true);
     }
     this.updateDataStore(this.webapps[id].localId, app.origin,
                          app.manifestURL, newManifest);
     this.broadcastMessage("Webapps:UpdateState", {
       app: app,
       manifest: newManifest,
-      manifestURL: app.manifestURL
+      id: app.id
     });
     this.broadcastMessage("Webapps:FireEvent", {
       eventType: "downloadapplied",
       manifestURL: app.manifestURL
     });
   }),
 
   startOfflineCacheDownload: function(aManifest, aApp, aProfileDir, aIsUpdate) {
@@ -1589,17 +1621,17 @@ this.DOMApplicationRegistry = {
       DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
         // Clear any previous errors.
         error: null,
         app: {
           downloading: true,
           installState: aApp.installState,
           progress: 0
         },
-        manifestURL: aApp.manifestURL
+        id: aApp.id
       });
       let cacheUpdate = updateSvc.scheduleAppUpdate(
         appcacheURI, docURI, aApp.localId, false, aProfileDir);
 
       // We save the download details for potential further usage like
       // cancelling it.
       let download = {
         cacheUpdate: cacheUpdate,
@@ -1639,16 +1671,17 @@ this.DOMApplicationRegistry = {
       this.notifyAppsRegistryReady();
     }
   },
 
   checkForUpdate: function(aData, aMm) {
     debug("checkForUpdate for " + aData.manifestURL);
 
     function sendError(aError) {
+      debug("checkForUpdate error " + aError);
       aData.error = aError;
       aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
     }
 
     let id = this._appIdForManifestURL(aData.manifestURL);
     let app = this.webapps[id];
 
     // We cannot update an app that does not exists.
@@ -1668,71 +1701,67 @@ this.DOMApplicationRegistry = {
       sendError("APP_IS_DOWNLOADING");
       return;
     }
 
     // If the app is packaged and its manifestURL has an app:// scheme,
     // then we can't have an update.
     if (app.origin.startsWith("app://") &&
         app.manifestURL.startsWith("app://")) {
-      aData.error = "NOT_UPDATABLE";
-      aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
+      sendError("NOT_UPDATABLE");
       return;
     }
 
     // For non-removable hosted apps that lives in the core apps dir we
     // only check the appcache because we can't modify the manifest even
     // if it has changed.
     let onlyCheckAppCache = false;
 
 #ifdef MOZ_WIDGET_GONK
     let appDir = FileUtils.getDir("coreAppsDir", ["webapps"], false);
     onlyCheckAppCache = (app.basePath == appDir.path);
 #endif
 
     if (onlyCheckAppCache) {
       // Bail out for packaged apps.
       if (app.origin.startsWith("app://")) {
-        aData.error = "NOT_UPDATABLE";
-        aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
+        sendError("NOT_UPDATABLE");
         return;
       }
 
       // We need the manifest to check if we have an appcache.
       this._readManifests([{ id: id }]).then((aResult) => {
         let manifest = aResult[0].manifest;
         if (!manifest.appcache_path) {
-          aData.error = "NOT_UPDATABLE";
-          aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
+          sendError("NOT_UPDATABLE");
           return;
         }
 
         debug("Checking only appcache for " + aData.manifestURL);
         // Check if the appcache is updatable, and send "downloadavailable" or
         // "downloadapplied".
         let updateObserver = {
           observe: function(aSubject, aTopic, aObsData) {
             debug("onlyCheckAppCache updateSvc.checkForUpdate return for " +
                   app.manifestURL + " - event is " + aTopic);
             if (aTopic == "offline-cache-update-available") {
               app.downloadAvailable = true;
               this._saveApps().then(() => {
                 this.broadcastMessage("Webapps:UpdateState", {
                   app: app,
-                  manifestURL: app.manifestURL
+                  id: app.id
                 });
                 this.broadcastMessage("Webapps:FireEvent", {
                   eventType: "downloadavailable",
                   manifestURL: app.manifestURL,
                   requestID: aData.requestID
                 });
               });
             } else {
-              aData.error = "NOT_UPDATABLE";
-              aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
+              sendError("NOT_UPDATABLE");
             }
           }
         };
         let helper = new ManifestHelper(manifest, aData.manifestURL);
         debug("onlyCheckAppCache - launch updateSvc.checkForUpdate for " +
               helper.fullAppcachePath());
         updateSvc.checkForUpdate(Services.io.newURI(helper.fullAppcachePath(), null, null),
                                  app.localId, false, updateObserver);
@@ -1782,17 +1811,17 @@ this.DOMApplicationRegistry = {
             } else {
               this._saveApps().then(() => {
                 // Like if we got a 304, just send a 'downloadapplied'
                 // or downloadavailable event.
                 let eventType = app.downloadAvailable ? "downloadavailable"
                                                       : "downloadapplied";
                 aMm.sendAsyncMessage("Webapps:UpdateState", {
                   app: app,
-                  manifestURL: app.manifestURL
+                  id: app.id
                 });
                 aMm.sendAsyncMessage("Webapps:FireEvent", {
                   eventType: eventType,
                   manifestURL: app.manifestURL,
                   requestID: aData.requestID
                 });
               });
             }
@@ -1809,17 +1838,17 @@ this.DOMApplicationRegistry = {
           app.lastCheckedUpdate = Date.now();
           this._saveApps().then(() => {
             // If the app is a packaged app, we just send a 'downloadapplied'
             // or downloadavailable event.
             let eventType = app.downloadAvailable ? "downloadavailable"
                                                   : "downloadapplied";
             aMm.sendAsyncMessage("Webapps:UpdateState", {
               app: app,
-              manifestURL: app.manifestURL
+              id: app.id
             });
             aMm.sendAsyncMessage("Webapps:FireEvent", {
               eventType: eventType,
               manifestURL: app.manifestURL,
               requestID: aData.requestID
             });
           });
         } else {
@@ -1918,17 +1947,17 @@ this.DOMApplicationRegistry = {
     // event.
     aApp.downloadAvailable = true;
     aApp.downloadSize = manifest.size;
     aApp.updateManifest = aNewManifest;
     yield this._saveApps();
 
     this.broadcastMessage("Webapps:UpdateState", {
       app: aApp,
-      manifestURL: aApp.manifestURL
+      id: aApp.id
     });
     this.broadcastMessage("Webapps:FireEvent", {
       eventType: "downloadavailable",
       manifestURL: aApp.manifestURL,
       requestID: aData.requestID
     });
   }),
 
@@ -1984,17 +2013,17 @@ this.DOMApplicationRegistry = {
     // Update the registry.
     this.webapps[aId] = aApp;
     yield this._saveApps();
 
     if (!manifest.appcache_path) {
       this.broadcastMessage("Webapps:UpdateState", {
         app: aApp,
         manifest: aApp.manifest,
-        manifestURL: aApp.manifestURL
+        id: aApp.id
       });
       this.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloadapplied",
         manifestURL: aApp.manifestURL,
         requestID: aData.requestID
       });
     } else {
       // Check if the appcache is updatable, and send "downloadavailable" or
@@ -2018,17 +2047,17 @@ this.DOMApplicationRegistry = {
                                                   : "downloadapplied";
 
       aApp.downloadAvailable = (eventType == "downloadavailable");
       yield this._saveApps();
 
       this.broadcastMessage("Webapps:UpdateState", {
         app: aApp,
         manifest: aApp.manifest,
-        manifestURL: aApp.manifestURL
+        id: aApp.id
       });
       this.broadcastMessage("Webapps:FireEvent", {
         eventType: eventType,
         manifestURL: aApp.manifestURL,
         requestID: aData.requestID
       });
     }
 
@@ -2445,17 +2474,18 @@ this.DOMApplicationRegistry = {
 
     // Store the manifest and the updateManifest.
     this._writeManifestFile(app.id, false, aManifest);
     if (aUpdateManifest) {
       this._writeManifestFile(app.id, true, aUpdateManifest);
     }
 
     this._saveApps().then(() => {
-      this.broadcastMessage("Webapps:AddApp", { id: app.id, app: app });
+      this.broadcastMessage("Webapps:AddApp",
+                            { id: app.id, app: app, manifest: aManifest });
     });
   }),
 
   confirmInstall: Task.async(function*(aData, aProfileDir, aInstallSuccessCallback) {
     debug("confirmInstall");
 
     let origin = Services.io.newURI(aData.app.origin, null, null);
     let id = this._appIdForManifestURL(aData.app.manifestURL);
@@ -2545,16 +2575,18 @@ this.DOMApplicationRegistry = {
       };
     }
 
     // We notify about the successful installation via mgmt.oninstall and the
     // corresponding DOMRequest.onsuccess event as soon as the app is properly
     // saved in the registry.
     yield this._saveApps();
 
+    aData.isPackage ? appObject.updateManifest = jsonManifest :
+                      appObject.manifest = jsonManifest;
     this.broadcastMessage("Webapps:AddApp", { id: id, app: appObject });
 
     // The presence of a requestID means that we have a page to update.
     if (aData.isPackage && aData.apkInstall && !aData.requestID) {
       // Skip directly to onInstallSuccessAck, since there isn't
       // a WebappsRegistry to receive Webapps:Install:Return:OK and respond
       // Webapps:Install:Return:Ack when an app is being auto-installed.
       this.onInstallSuccessAck(app.manifestURL);
@@ -2621,17 +2653,18 @@ this.DOMApplicationRegistry = {
     yield this._saveApps();
 
     this.updateAppHandlers(null, aManifest, aNewApp);
     // Clear the manifest cache in case it holds the update manifest.
     if (aId in this._manifestCache) {
       delete this._manifestCache[aId];
     }
 
-    this.broadcastMessage("Webapps:AddApp", { id: aId, app: aNewApp });
+    this.broadcastMessage("Webapps:AddApp",
+                          { id: aId, app: aNewApp, manifest: aManifest });
     Services.obs.notifyObservers(null, "webapps-installed",
       JSON.stringify({ manifestURL: aNewApp.manifestURL }));
 
     if (supportUseCurrentProfile()) {
       // Update the permissions for this app.
       PermissionsInstaller.installPermissions({
         manifest: aManifest,
         origin: aNewApp.origin,
@@ -2755,24 +2788,21 @@ this.DOMApplicationRegistry = {
 
       debug("About to download " + fullPackagePath);
 
       let requestChannel = this._getRequestChannel(fullPackagePath,
                                                    isLocalFileInstall,
                                                    oldApp,
                                                    aNewApp);
 
-      AppDownloadManager.add(
-        aNewApp.manifestURL,
-        {
-          channel: requestChannel,
-          appId: id,
-          previousState: aIsUpdate ? "installed" : "pending"
-        }
-      );
+      AppDownloadManager.add(aNewApp.manifestURL, {
+        channel: requestChannel,
+        appId: id,
+        previousState: aIsUpdate ? "installed" : "pending"
+      });
 
       // We set the 'downloading' flag to true right before starting the fetch.
       oldApp.downloading = true;
 
       // We determine the app's 'installState' according to its previous
       // state. Cancelled download should remain as 'pending'. Successfully
       // installed apps should morph to 'updating'.
       oldApp.installState = aIsUpdate ? "updating" : "pending";
@@ -2782,32 +2812,32 @@ this.DOMApplicationRegistry = {
 
       // Save the current state of the app to handle cases where we may be
       // retrying a past download.
       yield DOMApplicationRegistry._saveApps();
       DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
         // Clear any previous download errors.
         error: null,
         app: oldApp,
-        manifestURL: aNewApp.manifestURL
+        id: id
       });
 
       let zipFile = yield this._getPackage(requestChannel, id, oldApp, aNewApp);
       let hash = yield this._computeFileHash(zipFile.path);
 
       let responseStatus = requestChannel.responseStatus;
       let oldPackage = (responseStatus == 304 || hash == oldApp.packageHash);
 
       if (oldPackage) {
         debug("package's etag or hash unchanged; sending 'applied' event");
         // The package's Etag or hash has not changed.
         // We send an "applied" event right away so code awaiting that event
         // can proceed to access the app.  We also throw an error to alert
         // the caller that the package wasn't downloaded.
-        this._sendAppliedEvent(aNewApp, oldApp, id);
+        this._sendAppliedEvent(oldApp);
         throw new Error("PACKAGE_UNCHANGED");
       }
 
       let newManifest = yield this._openAndReadPackage(zipFile, oldApp, aNewApp,
               isLocalFileInstall, aIsUpdate, aManifest, requestChannel, hash);
 
       AppDownloadManager.remove(aNewApp.manifestURL);
 
@@ -2937,17 +2967,17 @@ this.DOMApplicationRegistry = {
     return requestChannel;
   },
 
   _sendDownloadProgressEvent: function(aNewApp, aProgress) {
     this.broadcastMessage("Webapps:UpdateState", {
       app: {
         progress: aProgress
       },
-      manifestURL: aNewApp.manifestURL
+      id: aNewApp.id
     });
     this.broadcastMessage("Webapps:FireEvent", {
       eventType: "progress",
       manifestURL: aNewApp.manifestURL
     });
   },
 
   _getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) {
@@ -3054,56 +3084,53 @@ this.DOMApplicationRegistry = {
   /**
    * Send an "applied" event right away for the package being installed.
    *
    * XXX We use this to exit the app update process early when the downloaded
    * package is identical to the last one we installed.  Presumably we do
    * something similar after updating the app, and we could refactor both cases
    * to use the same code to send the "applied" event.
    *
-   * @param aNewApp {Object} the new app data
-   * @param aOldApp {Object} the currently stored app data
-   * @param aId {String} the unique id of the app
+   * @param aApp {Object} app data
    */
-  _sendAppliedEvent: function(aNewApp, aOldApp, aId) {
-    aOldApp.downloading = false;
-    aOldApp.downloadAvailable = false;
-    aOldApp.downloadSize = 0;
-    aOldApp.installState = "installed";
-    aOldApp.readyToApplyDownload = false;
-    if (aOldApp.staged && aOldApp.staged.manifestHash) {
+  _sendAppliedEvent: function(aApp) {
+    aApp.downloading = false;
+    aApp.downloadAvailable = false;
+    aApp.downloadSize = 0;
+    aApp.installState = "installed";
+    aApp.readyToApplyDownload = false;
+    if (aApp.staged && aApp.staged.manifestHash) {
       // If we're here then the manifest has changed but the package
       // hasn't. Let's clear this, so we don't keep offering
       // a bogus update to the user
-      aOldApp.manifestHash = aOldApp.staged.manifestHash;
-      aOldApp.etag = aOldApp.staged.etag || aOldApp.etag;
-      aOldApp.staged = {};
-
-      // Move the staged update manifest to a non staged one.
+      aApp.manifestHash = aApp.staged.manifestHash;
+      aApp.etag = aApp.staged.etag || aApp.etag;
+      aApp.staged = {};
+     // Move the staged update manifest to a non staged one.
       try {
-        let staged = this._getAppDir(aId);
+        let staged = this._getAppDir(aApp.id);
         staged.append("staged-update.webapp");
         staged.moveTo(staged.parent, "update.webapp");
       } catch (ex) {
         // We don't really mind much if this fails.
       }
     }
 
     // Save the updated registry, and cleanup the tmp directory.
     this._saveApps().then(() => {
       this.broadcastMessage("Webapps:UpdateState", {
-        app: aOldApp,
-        manifestURL: aNewApp.manifestURL
+        app: aApp,
+        id: aApp.id
       });
       this.broadcastMessage("Webapps:FireEvent", {
-        manifestURL: aNewApp.manifestURL,
+        manifestURL: aApp.manifestURL,
         eventType: ["downloadsuccess", "downloadapplied"]
       });
     });
-    let file = FileUtils.getFile("TmpD", ["webapps", aId], false);
+    let file = FileUtils.getFile("TmpD", ["webapps", aApp.id], false);
     if (file && file.exists()) {
       file.remove(true);
     }
   },
 
   _openAndReadPackage: function(aZipFile, aOldApp, aNewApp, aIsLocalFileInstall,
                                 aIsUpdate, aManifest, aRequestChannel, aHash) {
     return Task.spawn((function*() {
@@ -3398,19 +3425,20 @@ this.DOMApplicationRegistry = {
         delete this.webapps[oldId];
         // Rename the directories where the files are installed.
         [DIRECTORY_NAME, "TmpD"].forEach(function(aDir) {
           let parent = FileUtils.getDir(aDir, ["webapps"], true, true);
           let dir = FileUtils.getDir(aDir, ["webapps", oldId], true, true);
           dir.moveTo(parent, newId);
         });
         // Signals that we need to swap the old id with the new app.
-        this.broadcastMessage("Webapps:RemoveApp", { id: oldId });
-        this.broadcastMessage("Webapps:AddApp", { id: newId,
-                                                  app: aOldApp });
+        this.broadcastMessage("Webapps:UpdateApp", { oldId: oldId,
+                                                     newId: newId,
+                                                     app: aOldApp });
+
       }
     }
   },
 
   _getIds: function(aIsSigned, aZipReader, aConverter, aNewApp, aOldApp,
                     aIsUpdate) {
     // Get ids.json if the file is signed
     if (aIsSigned) {
@@ -3503,17 +3531,17 @@ this.DOMApplicationRegistry = {
     if (aOldApp.staged) {
       delete aOldApp.staged;
     }
 
     this._saveApps().then(() => {
       this.broadcastMessage("Webapps:UpdateState", {
         app: aOldApp,
         error: aError,
-        manifestURL: aNewApp.manifestURL
+        id: aNewApp.id
       });
       this.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloaderror",
         manifestURL:  aNewApp.manifestURL
       });
     });
     AppDownloadManager.remove(aNewApp.manifestURL);
 
@@ -4055,17 +4083,17 @@ let AppcacheObserver = function(aApp) {
 };
 
 AppcacheObserver.prototype = {
   // nsIOfflineCacheUpdateObserver implementation
   _sendProgressEvent: function() {
     let app = this.app;
     DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
       app: app,
-      manifestURL: app.manifestURL
+      id: app.id
     });
     DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
       eventType: "progress",
       manifestURL: app.manifestURL
     });
   },
 
   updateStateChanged: function appObs_Update(aUpdate, aState) {
@@ -4087,17 +4115,17 @@ AppcacheObserver.prototype = {
         return;
       }
 
       app.updateTime = Date.now();
       app.downloading = false;
       app.downloadAvailable = false;
       DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
         app: app,
-        manifestURL: app.manifestURL
+        id: app.id
       });
       DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
         eventType: ["downloadsuccess", "downloadapplied"],
         manifestURL: app.manifestURL
       });
     }
 
     let setError = function appObs_setError(aError) {
@@ -4110,17 +4138,17 @@ AppcacheObserver.prototype = {
       if (app.isCanceling) {
         delete app.isCanceling;
         return;
       }
 
       DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
         app: app,
         error: aError,
-        manifestURL: app.manifestURL
+        id: app.id
       });
       DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloaderror",
         manifestURL: app.manifestURL
       });
     }
 
     switch (aState) {
--- a/dom/apps/tests/test_packaged_app_common.js
+++ b/dom/apps/tests/test_packaged_app_common.js
@@ -93,16 +93,17 @@ var PackagedTestHelper = (function Packa
       ok(false, "Got unexpected " + evt.target.error.name);
       finish();
     };
 
     navigator.mozApps.mgmt.oninstall = function(evt) {
       var aApp = evt.application;
       aApp.ondownloaderror = function(evt) {
         var error = aApp.downloadError.name;
+        ok(true, "Got downloaderror " + error);
         if (error == aExpectedError) {
           ok(true, "Got expected " + aExpectedError);
           var expected = {
             name: aName,
             manifestURL: aMiniManifestURL,
             installOrigin: gInstallOrigin,
             progress: 0,
             installState: "pending",
--- a/dom/apps/tests/test_packaged_app_update.html
+++ b/dom/apps/tests/test_packaged_app_update.html
@@ -74,25 +74,25 @@ function checkLastAppState(aMiniManifest
 
 function updateApp(aExpectedReady, aPreviousVersion, aNextVersion) {
   var lApp = PackagedTestHelper.gApp;
 
   var ondownloadappliedhandler =
     checkLastAppState.bind(PackagedTestHelper, miniManifestURL, false, false,
                            aNextVersion, PackagedTestHelper.next);
 
-    var ondownloadsuccesshandler =
-      checkLastAppState.bind(undefined, miniManifestURL,
-                             aExpectedReady, false, aPreviousVersion,
-                             function() {
-        navigator.mozApps.mgmt.applyDownload(lApp);
-    });
+  var ondownloadsuccesshandler =
+    checkLastAppState.bind(undefined, miniManifestURL,
+                           aExpectedReady, false, aPreviousVersion,
+                           function() {
+      navigator.mozApps.mgmt.applyDownload(lApp);
+  });
 
-    checkForUpdate(true, ondownloadsuccesshandler, ondownloadappliedhandler, null,
-                   true);
+  checkForUpdate(true, ondownloadsuccesshandler, ondownloadappliedhandler,
+                 null, true);
 
 }
 
 var initialPermissionState = {
   "geolocation": "prompt",
   "audio-capture": "prompt",
   "video-capture": "prompt",
   "downloads": "deny"
@@ -246,17 +246,17 @@ var steps = [
   },
   function() {
     info("== TEST == Update packaged app - Updating a pending app");
     miniManifestURL = PackagedTestHelper.gSJS +
                       "?getManifest=true" +
                       "&appName=arandomname" +
                       "&appToFail1";
     PackagedTestHelper.checkAppDownloadError(miniManifestURL,
-                                            "MANIFEST_MISMATCH", 2, false, true,
+                                            "MANIFEST_MISMATCH", 1, false, true,
                                              "arandomname",
                                              function () {
       checkForUpdate(false, null, null, null, false,
                      function (request) {
         if (request.error.name === "PENDING_APP_NOT_UPDATABLE") {
           info("Got expected PENDING_APP_NOT_UPDATEABLE");
         } else {
           ok(false, "Got unexpected " + request.error.name);
--- a/dom/apps/tests/test_receipt_operations.html
+++ b/dom/apps/tests/test_receipt_operations.html
@@ -238,9 +238,9 @@ function runTest() {
   ok(true, "App uninstalled");
 }
 
 addLoadEvent(go);
 
 </script>
 </pre>
 </body>
-</html>
\ No newline at end of file
+</html>
--- a/dom/bluetooth2/tests/marionette/head.js
+++ b/dom/bluetooth2/tests/marionette/head.js
@@ -1,11 +1,9 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
- * vim: sw=2 ts=2 sts=2 et filetype=javascript
- * This Source Code Form is subject to the terms of the Mozilla Public
+/* 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/. */
 
 // https://github.com/mozilla-b2g/platform_external_qemu/blob/master/vl-android.c#L765
 // static int bt_hci_parse(const char *str) {
 //   ...
 //   bdaddr.b[0] = 0x52;
 //   bdaddr.b[1] = 0x54;
@@ -31,26 +29,78 @@ const EMULATOR_CLASS = 0x58020c;
 
 // Use same definition in QEMU for special bluetooth address,
 // which were defined at external/qemu/hw/bt.h:
 const BDADDR_ANY   = "00:00:00:00:00:00";
 const BDADDR_ALL   = "ff:ff:ff:ff:ff:ff";
 const BDADDR_LOCAL = "ff:ff:ff:00:00:00";
 
 // A user friendly name for remote BT device.
-const REMOTE_DEVICE_NAME = "Remote BT Device";
+const REMOTE_DEVICE_NAME = "Remote_BT_Device";
 
 let Promise =
   SpecialPowers.Cu.import("resource://gre/modules/Promise.jsm").Promise;
 
 let bluetoothManager;
 
 let pendingEmulatorCmdCount = 0;
 
 /**
+ * Push required permissions and test if |navigator.mozBluetooth| exists.
+ * Resolve if it does, reject otherwise.
+ *
+ * Fulfill params:
+ *   bluetoothManager -- an reference to navigator.mozBluetooth.
+ * Reject params: (none)
+ *
+ * @param aPermissions
+ *        Additional permissions to push before any test cases.  Could be either
+ *        a string or an array of strings.
+ *
+ * @return A deferred promise.
+ */
+function ensureBluetoothManager(aPermissions) {
+  let deferred = Promise.defer();
+
+  let permissions = ["bluetooth"];
+  if (aPermissions) {
+    if (Array.isArray(aPermissions)) {
+      permissions = permissions.concat(aPermissions);
+    } else if (typeof aPermissions == "string") {
+      permissions.push(aPermissions);
+    }
+  }
+
+  let obj = [];
+  for (let perm of permissions) {
+    obj.push({
+      "type": perm,
+      "allow": 1,
+      "context": document,
+    });
+  }
+
+  SpecialPowers.pushPermissions(obj, function() {
+    ok(true, "permissions pushed: " + JSON.stringify(permissions));
+
+    bluetoothManager = window.navigator.mozBluetooth;
+    log("navigator.mozBluetooth is " +
+        (bluetoothManager ? "available" : "unavailable"));
+
+    if (bluetoothManager instanceof BluetoothManager) {
+      deferred.resolve(bluetoothManager);
+    } else {
+      deferred.reject();
+    }
+  });
+
+  return deferred.promise;
+}
+
+/**
  * Send emulator command with safe guard.
  *
  * We should only call |finish()| after all emulator command transactions
  * end, so here comes with the pending counter.  Resolve when the emulator
  * gives positive response, and reject otherwise.
  *
  * Fulfill params:
  *   result -- an array of emulator response lines.
@@ -189,81 +239,16 @@ function getEmulatorDeviceProperty(aAddr
   let cmd = "bt property " + aAddress + " " + aPropertyName;
   return runEmulatorCmdSafe(cmd)
     .then(function(aResults) {
       return aResults[0];
     });
 }
 
 /**
- * Start dicovering Bluetooth devices.
- *
- * Allows the device's adapter to start seeking for remote devices.
- *
- * Fulfill params: (none)
- * Reject params: a DOMError
- *
- * @param aAdapter
- *        A BluetoothAdapter which is used to interact with local BT dev
- *
- * @return A deferred promise.
- */
-function startDiscovery(aAdapter) {
-  let deferred = Promise.defer();
-
-  let request = aAdapter.startDiscovery();
-  request.onsuccess = function () {
-    log("  Start discovery - Success");
-    // TODO (bug 892207): Make Bluetooth APIs available for 3rd party apps.
-    //     Currently, discovering state wouldn't change immediately here.
-    //     We would turn on this check when the redesigned API are landed.
-    // is(aAdapter.discovering, true, "BluetoothAdapter.discovering");
-    deferred.resolve();
-  }
-  request.onerror = function (aEvent) {
-    ok(false, "Start discovery - Fail");
-    deferred.reject(aEvent.target.error);
-  }
-
-  return deferred.promise;
-}
-
-/**
- * Stop dicovering Bluetooth devices.
- *
- * Allows the device's adapter to stop seeking for remote devices.
- *
- * Fulfill params: (none)
- * Reject params: a DOMError
- *
- * @param aAdapter
- *        A BluetoothAdapter which is used to interact with local BT device.
- *
- * @return A deferred promise.
- */
-function stopDiscovery(aAdapter) {
-  let deferred = Promise.defer();
-
-  let request = aAdapter.stopDiscovery();
-  request.onsuccess = function () {
-    log("  Stop discovery - Success");
-    // TODO (bug 892207): Make Bluetooth APIs available for 3rd party apps.
-    //     Currently, discovering state wouldn't change immediately here.
-    //     We would turn on this check when the redesigned API are landed.
-    // is(aAdapter.discovering, false, "BluetoothAdapter.discovering");
-    deferred.resolve();
-  }
-  request.onerror = function (aEvent) {
-    ok(false, "Stop discovery - Fail");
-    deferred.reject(aEvent.target.error);
-  }
-  return deferred.promise;
-}
-
-/**
  * Get mozSettings value specified by @aKey.
  *
  * Resolve if that mozSettings value is retrieved successfully, reject
  * otherwise.
  *
  * Fulfill params:
  *   The corresponding mozSettings value of the key.
  * Reject params: (none)
@@ -314,29 +299,30 @@ function setSettings(aSettings) {
     ok(false, "setSettings(" + JSON.stringify(aSettings) + ")");
     deferred.reject();
   });
 
   return deferred.promise;
 }
 
 /**
- * Get mozSettings value of 'bluetooth.enabled'.
+ * Get the boolean value which indicates defaultAdapter of bluetooth is enabled.
  *
- * Resolve if that mozSettings value is retrieved successfully, reject
- * otherwise.
+ * Resolve if that defaultAdapter is enabled
  *
  * Fulfill params:
  *   A boolean value.
  * Reject params: (none)
  *
  * @return A deferred promise.
  */
 function getBluetoothEnabled() {
-  return getSettings("bluetooth.enabled");
+  log("bluetoothManager.defaultAdapter.state: " + bluetoothManager.defaultAdapter.state);
+
+  return (bluetoothManager.defaultAdapter.state == "enabled");
 }
 
 /**
  * Set mozSettings value of 'bluetooth.enabled'.
  *
  * Resolve if that mozSettings value is set successfully, reject otherwise.
  *
  * Fulfill params: (none)
@@ -349,68 +335,16 @@ function getBluetoothEnabled() {
  */
 function setBluetoothEnabled(aEnabled) {
   let obj = {};
   obj["bluetooth.enabled"] = aEnabled;
   return setSettings(obj);
 }
 
 /**
- * Push required permissions and test if |navigator.mozBluetooth| exists.
- * Resolve if it does, reject otherwise.
- *
- * Fulfill params:
- *   bluetoothManager -- an reference to navigator.mozBluetooth.
- * Reject params: (none)
- *
- * @param aPermissions
- *        Additional permissions to push before any test cases.  Could be either
- *        a string or an array of strings.
- *
- * @return A deferred promise.
- */
-function ensureBluetoothManager(aPermissions) {
-  let deferred = Promise.defer();
-
-  let permissions = ["bluetooth"];
-  if (aPermissions) {
-    if (Array.isArray(aPermissions)) {
-      permissions = permissions.concat(aPermissions);
-    } else if (typeof aPermissions == "string") {
-      permissions.push(aPermissions);
-    }
-  }
-
-  let obj = [];
-  for (let perm of permissions) {
-    obj.push({
-      "type": perm,
-      "allow": 1,
-      "context": document,
-    });
-  }
-
-  SpecialPowers.pushPermissions(obj, function() {
-    ok(true, "permissions pushed: " + JSON.stringify(permissions));
-
-    bluetoothManager = window.navigator.mozBluetooth;
-    log("navigator.mozBluetooth is " +
-        (bluetoothManager ? "available" : "unavailable"));
-
-    if (bluetoothManager instanceof BluetoothManager) {
-      deferred.resolve(bluetoothManager);
-    } else {
-      deferred.reject();
-    }
-  });
-
-  return deferred.promise;
-}
-
-/**
  * Wait for one named BluetoothManager event.
  *
  * Resolve if that named event occurs.  Never reject.
  *
  * Fulfill params: the DOMEvent passed.
  *
  * @param aEventName
  *        The name of the EventHandler.
@@ -453,83 +387,16 @@ function waitForAdapterEvent(aAdapter, a
     ok(true, "BluetoothAdapter event '" + aEventName + "' got.");
     deferred.resolve(aEvent);
   });
 
   return deferred.promise;
 }
 
 /**
- * Convenient function for setBluetoothEnabled and waitForManagerEvent
- * combined.
- *
- * Resolve if that named event occurs.  Reject if we can't set settings.
- *
- * Fulfill params: the DOMEvent passed.
- * Reject params: (none)
- *
- * @return A deferred promise.
- */
-function setBluetoothEnabledAndWait(aEnabled) {
-  let promises = [];
-
-  // Bug 969109 -  Intermittent test_dom_BluetoothManager_adapteradded.js
-  //
-  // Here we want to wait for two events coming up -- Bluetooth "settings-set"
-  // event and one of "enabled"/"disabled" events.  Special care is taken here
-  // to ensure that we can always receive that "enabled"/"disabled" event by
-  // installing the event handler *before* we ever enable/disable Bluetooth. Or
-  // we might just miss those events and get a timeout error.
-  promises.push(waitForManagerEvent(aEnabled ? "enabled" : "disabled"));
-  promises.push(setBluetoothEnabled(aEnabled));
-
-  return Promise.all(promises);
-}
-
-/**
- * Get default adapter.
- *
- * Resolve if that default adapter is got, reject otherwise.
- *
- * Fulfill params: a BluetoothAdapter instance.
- * Reject params: a DOMError, or null if if there is no adapter ready yet.
- *
- * @return A deferred promise.
- */
-function getDefaultAdapter() {
-  let deferred = Promise.defer();
-
-  let request = bluetoothManager.getDefaultAdapter();
-  request.onsuccess = function(aEvent) {
-    let adapter = aEvent.target.result;
-    if (!(adapter instanceof BluetoothAdapter)) {
-      ok(false, "no BluetoothAdapter ready yet.");
-      deferred.reject(null);
-      return;
-    }
-
-    ok(true, "BluetoothAdapter got.");
-    // TODO: We have an adapter instance now, but some of its attributes may
-    // still remain unassigned/out-dated.  Here we waste a few seconds to
-    // wait for the property changed events.
-    //
-    // See https://bugzilla.mozilla.org/show_bug.cgi?id=932914
-    window.setTimeout(function() {
-      deferred.resolve(adapter);
-    }, 3000);
-  };
-  request.onerror = function(aEvent) {
-    ok(false, "Failed to get default adapter.");
-    deferred.reject(aEvent.target.error);
-  };
-
-  return deferred.promise;
-}
-
-/**
  * Flush permission settings and call |finish()|.
  */
 function cleanUp() {
   waitFor(function() {
     SpecialPowers.flushPermissions(function() {
       // Use ok here so that we have at least one test run.
       ok(true, "permissions flushed");
 
@@ -547,42 +414,46 @@ function startBluetoothTestBase(aPermiss
       ok(false, "Unhandled rejected promise.");
       cleanUp();
     });
 }
 
 function startBluetoothTest(aReenable, aTestCaseMain) {
   startBluetoothTestBase(["settings-read", "settings-write"], function() {
     let origEnabled, needEnable;
+    return Promise.resolve()
+      .then(function() {
+        origEnabled = getBluetoothEnabled();
 
-    return getBluetoothEnabled()
-      .then(function(aEnabled) {
-        origEnabled = aEnabled;
-        needEnable = !aEnabled;
-        log("Original 'bluetooth.enabled' is " + origEnabled);
+        needEnable = !origEnabled;
+        log("Original state of bluetooth is " + bluetoothManager.defaultAdapter.state);
 
-        if (aEnabled && aReenable) {
-          log("  Disable 'bluetooth.enabled' ...");
+        if (origEnabled && aReenable) {
+          log("Disable Bluetooth ...");
           needEnable = true;
-          return setBluetoothEnabledAndWait(false);
+
+          isnot(bluetoothManager.defaultAdapter, null,
+            "bluetoothManager.defaultAdapter")
+
+          return bluetoothManager.defaultAdapter.disable();
         }
       })
       .then(function() {
         if (needEnable) {
-          log("  Enable 'bluetooth.enabled' ...");
+          log("Enable Bluetooth ...");
 
-          // See setBluetoothEnabledAndWait().  We must install all event
-          // handlers *before* enabling Bluetooth.
-          let promises = [];
-          promises.push(waitForManagerEvent("adapteradded"));
-          promises.push(setBluetoothEnabledAndWait(true));
-          return Promise.all(promises);
+          isnot(bluetoothManager.defaultAdapter, null,
+            "bluetoothManager.defaultAdapter")
+
+          return bluetoothManager.defaultAdapter.enable();
         }
       })
-      .then(getDefaultAdapter)
+      .then(() => bluetoothManager.defaultAdapter)
       .then(aTestCaseMain)
       .then(function() {
         if (!origEnabled) {
-          return setBluetoothEnabledAndWait(false);
+          log("Disable Bluetooth ...");
+
+          return bluetoothManager.defaultAdapter.disable();
         }
       });
   });
 }
--- a/dom/bluetooth2/tests/marionette/manifest.ini
+++ b/dom/bluetooth2/tests/marionette/manifest.ini
@@ -1,10 +1,6 @@
 [DEFAULT]
 b2g = true
 browser = false
-qemu = true
+qemu = false
 
-[test_dom_BluetoothManager_enabled.js]
-[test_dom_BluetoothManager_adapteradded.js]
-[test_dom_BluetoothAdapter_setters.js]
-[test_dom_BluetoothAdapter_getters.js]
-[test_dom_BluetoothAdapter_discovery.js]
+[test_dom_BluetoothManager_API2.js]
deleted file mode 100644
--- a/dom/bluetooth2/tests/marionette/test_dom_BluetoothAdapter_discovery.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
- * vim: sw=2 ts=2 sts=2 et filetype=javascript
- * 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/. */
-
-///////////////////////////////////////////////////////////////////////////////
-// Test Purpose:
-//   To verify that discovery process of BluetoothAdapter is correct.
-//   Use B2G emulator commands to add/remote remote devices to simulate
-//   discovering behavior.
-//
-// Test Coverage:
-//   - BluetoothAdapter.startDiscovery()
-//   - BluetoothAdapter.stopDiscovery()
-//   - BluetoothAdapter.ondevicefound()
-//   - BluetoothAdapter.discovering [Temporarily turned off until BT API update]
-//
-///////////////////////////////////////////////////////////////////////////////
-
-MARIONETTE_TIMEOUT = 60000;
-MARIONETTE_HEAD_JS = 'head.js';
-
-startBluetoothTest(true, function testCaseMain(aAdapter) {
-  log("Testing the discovery process of BluetoothAdapter ...");
-
-  // The properties of remote device.
-  let theProperties = {
-    "name": REMOTE_DEVICE_NAME,
-    "discoverable": true
-  };
-
-  return Promise.resolve()
-    .then(() => removeEmulatorRemoteDevice(BDADDR_ALL))
-    .then(() => addEmulatorRemoteDevice(/*theProperties*/ null))
-    .then(function(aRemoteAddress) {
-      let promises = [];
-      promises.push(waitForAdapterEvent(aAdapter, "devicefound"));
-      promises.push(startDiscovery(aAdapter));
-      return Promise.all(promises)
-        .then(function(aResults) {
-          is(aResults[0].device.address, aRemoteAddress, "BluetoothDevice.address");
-        });
-    })
-    .then(() => stopDiscovery(aAdapter))
-    .then(() => removeEmulatorRemoteDevice(BDADDR_ALL));
-});
deleted file mode 100644
--- a/dom/bluetooth2/tests/marionette/test_dom_BluetoothAdapter_getters.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
- * vim: sw=2 ts=2 sts=2 et filetype=javascript
- * 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/. */
-
-///////////////////////////////////////////////////////////////////////////////
-// Test Purpose:
-//   To verify that the properties of BluetoothAdapter can be updated and
-//   retrieved correctly. Use B2G emulator commands to set properties for this
-//   test case.
-//
-// Test Coverage:
-//   - BluetoothAdapter.name
-//   - BluetoothAdapter.address
-//   - BluetoothAdapter.class
-//   - BluetoothAdapter.discoverable
-//   - BluetoothAdapter.discovering
-//   ( P.S. Don't include [BluetoothAdapter.uuids], [BluetoothAdapter.devices] )
-//
-///////////////////////////////////////////////////////////////////////////////
-
-MARIONETTE_TIMEOUT = 60000;
-MARIONETTE_HEAD_JS = 'head.js';
-
-function testAdapterGetter(aAdapter, aPropertyName, aParamName, aExpected) {
-  let cmd = "bt property " + BDADDR_LOCAL + " " + aParamName;
-  return runEmulatorCmdSafe(cmd)
-    .then(function(aResults) {
-      is(aResults[1], "OK", "The status report from emulator command.");
-      log("  Got adapter " + aResults[0]);
-      is(aResults[0], aParamName + ": " + aExpected, "BluetoothAdapter." + aPropertyName);
-    });
-}
-
-startBluetoothTest(true, function testCaseMain(aAdapter) {
-  log("Checking the correctness of BluetoothAdapter properties ...");
-
-  return Promise.resolve()
-    .then(() => testAdapterGetter(aAdapter, "name",         "name",         aAdapter.name))
-    .then(() => testAdapterGetter(aAdapter, "address",      "address",      aAdapter.address))
-    .then(() => testAdapterGetter(aAdapter, "class",        "cod",          "0x" + aAdapter.class.toString(16)))
-    .then(() => testAdapterGetter(aAdapter, "discoverable", "discoverable", aAdapter.discoverable.toString()))
-    .then(() => testAdapterGetter(aAdapter, "discovering",  "discovering",  aAdapter.discovering.toString()));
-});
deleted file mode 100644
--- a/dom/bluetooth2/tests/marionette/test_dom_BluetoothAdapter_setters.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
- * vim: sw=2 ts=2 sts=2 et filetype=javascript
- * 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/. */
-
-///////////////////////////////////////////////////////////////////////////////
-// Test Purpose:
-//   To verify that all setters of BluetoothAdapter (except for pairing related
-//   APIs) can change properties correctly.
-//
-// Test Coverage:
-//   - BluetoothAdapter.setName()
-//   - BluetoothAdapter.setDiscoverable()
-//   - BluetoothAdapter.setDiscoverableTimeout()
-//
-///////////////////////////////////////////////////////////////////////////////
-
-MARIONETTE_TIMEOUT = 60000;
-MARIONETTE_HEAD_JS = 'head.js';
-
-const BT_DEVICE_NAME = "User friendly name of local BT device";
-
-function setName(aAdapter, aName) {
-  let deferred = Promise.defer();
-
-  let request = aAdapter.setName(aName);
-  request.onsuccess = function () {
-    log("  setName succeed: " + aName);
-    is(aAdapter.name, aName, "aAdapter.name");
-    deferred.resolve();
-  }
-  request.onerror = function () {
-    ok(false, "setName failed")
-    deferred.reject();
-  }
-
-  return deferred.promise;
-}
-
-function setDiscoverable(aAdapter, aIsDiscoverable) {
-  let deferred = Promise.defer();
-
-  let request = aAdapter.setDiscoverable(aIsDiscoverable);
-  request.onsuccess = function () {
-    log("  setDiscoverable succeed: " + aIsDiscoverable);
-    is(aAdapter.discoverable, aIsDiscoverable, "aAdapter.discoverable");
-    deferred.resolve();
-  }
-  request.onerror = function () {
-    ok(false, "setDiscoverable failed")
-    deferred.reject();
-  }
-
-  return deferred.promise;
-}
-
-function setDiscoverableTimeout(aAdapter, aTimeout) {
-  let deferred = Promise.defer();
-
-  let request = aAdapter.setDiscoverableTimeout(aTimeout);
-  request.onsuccess = function () {
-    log("  setDiscoverableTimeout succeed: " + aTimeout);
-    is(aAdapter.discoverableTimeout, aTimeout, "aAdapter.discoverableTimeout");
-    deferred.resolve();
-  }
-  request.onerror = function () {
-    ok(false, "setDiscoverableTimeout failed")
-    deferred.reject();
-  }
-
-  return deferred.promise;
-}
-
-startBluetoothTest(true, function testCaseMain(aAdapter) {
-  log("Testing BluetoothAdapter setters ...");
-
-  return Promise.resolve()
-    .then( () => setName(aAdapter, BT_DEVICE_NAME) )
-    .then( () => setDiscoverableTimeout(aAdapter, 180) )
-    .then( () => setDiscoverable(aAdapter, true) )
-    .then( () => setName(aAdapter, EMULATOR_NAME) )
-    .then( () => setDiscoverable(aAdapter, false) )
-    .then( () => setDiscoverableTimeout(aAdapter, 120) );
-});
new file mode 100644
--- /dev/null
+++ b/dom/bluetooth2/tests/marionette/test_dom_BluetoothManager_API2.js
@@ -0,0 +1,48 @@
+/* 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/. */
+
+///////////////////////////////////////////////////////////////////////////////
+// Test Purpose:
+//   To verify the basic functionality of BluetoothManager.
+//
+// Test Coverage:
+//   - BluetoothManager.defaultAdapter
+//   - BluetoothManager.getAdapters()
+// TODO:
+//   - BluetoothManager.onattributechanged()
+//   - BluetoothManager.onadapteradded()
+//   - BluetoothManager.onadapterremoved()
+//
+///////////////////////////////////////////////////////////////////////////////
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = 'head.js';
+
+// TODO: Listens to 'onattributechanged' when B2G supports the feature that
+//       allows user to add/remove Bluetooth adapter.
+// Currently, B2G recognizes build-in Bluetooth chip as default adapter and
+// don't support adding additional Bluetooth dongles in gonk layer.
+// Therefore, the 'onattributechanged' would be triggered *only* when the
+// instance of BluetoothManager is created.
+function waitForManagerAttributeChanged() {
+  let deferred = Promise.defer();
+
+  bluetoothManager.onattributechanged = function(aEvent) {
+    if(aEvent.attrs.indexOf("defaultAdapter")) {
+      bluetoothManager.onattributechanged = null;
+      ok(true, "BluetoothManager event 'onattributechanged' got.");
+      deferred.resolve(aEvent);
+    }
+  };
+
+  return deferred.promise;
+}
+
+startBluetoothTestBase(["settings-read", "settings-write"],
+                       function testCaseMain() {
+  let adapters = bluetoothManager.getAdapters();
+  ok(Array.isArray(adapters), "Can not got the array of adapters");
+  ok(adapters.length, "The number of adapters should not be zero");
+  ok(bluetoothManager.defaultAdapter, "defaultAdapter should not be null.");
+});
deleted file mode 100644
--- a/dom/bluetooth2/tests/marionette/test_dom_BluetoothManager_adapteradded.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
- * vim: sw=2 ts=2 sts=2 et filetype=javascript
- * 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/. */
-
-MARIONETTE_TIMEOUT = 60000;
-MARIONETTE_HEAD_JS = 'head.js';
-
-startBluetoothTest(true, function testCaseMain(aAdapter) {
-  log("Checking adapter attributes ...");
-
-  is(aAdapter.name, EMULATOR_NAME, "adapter.name");
-  is(aAdapter.class, EMULATOR_CLASS, "adapter.class");
-  is(aAdapter.address, EMULATOR_ADDRESS, "adapter.address");
-  is(aAdapter.discovering, false, "adapter.discovering");
-  is(aAdapter.discoverable, false, "adapter.discoverable");
-  is(aAdapter.discoverableTimeout, 120, "adapter.discoverableTimeout");
-});
deleted file mode 100644
--- a/dom/bluetooth2/tests/marionette/test_dom_BluetoothManager_enabled.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
- * vim: sw=2 ts=2 sts=2 et filetype=javascript
- * 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/. */
-
-MARIONETTE_TIMEOUT = 60000;
-MARIONETTE_HEAD_JS = 'head.js';
-
-function waitEitherEnabledOrDisabled() {
-  let deferred = Promise.defer();
-
-  function onEnabledDisabled(aEvent) {
-    bluetoothManager.removeEventListener("adapteradded", onEnabledDisabled);
-    bluetoothManager.removeEventListener("disabled", onEnabledDisabled);
-
-    ok(true, "Got event " + aEvent.type);
-    deferred.resolve(aEvent.type === "adapteradded");
-  }
-
-  // Listen 'adapteradded' rather than 'enabled' since the current API can't
-  // disable BT before the BT adapter is initialized.
-  // We should listen to 'enabled' when gecko can handle the case I mentioned
-  // above, please refer to the follow-up bug 973482.
-  bluetoothManager.addEventListener("adapteradded", onEnabledDisabled);
-  bluetoothManager.addEventListener("disabled", onEnabledDisabled);
-
-  return deferred.promise;
-}
-
-function test(aEnabled) {
-  log("Testing 'bluetooth.enabled' => " + aEnabled);
-
-  let deferred = Promise.defer();
-
-  Promise.all([setBluetoothEnabled(aEnabled),
-               waitEitherEnabledOrDisabled()])
-    .then(function(aResults) {
-      /* aResults is an array of two elements:
-       *   [ <result of setBluetoothEnabled>,
-       *     <result of waitEitherEnabledOrDisabled> ]
-       */
-      log("  Examine results " + JSON.stringify(aResults));
-
-      is(bluetoothManager.enabled, aEnabled, "bluetoothManager.enabled");
-      is(aResults[1], aEnabled, "'adapteradded' event received");
-
-      if (bluetoothManager.enabled === aEnabled && aResults[1] === aEnabled) {
-        deferred.resolve();
-      } else {
-        deferred.reject();
-      }
-    });
-
-  return deferred.promise;
-}
-
-startBluetoothTestBase(["settings-read", "settings-write"],
-                       function testCaseMain() {
-  return getBluetoothEnabled()
-    .then(function(aEnabled) {
-      log("Original 'bluetooth.enabled' is " + aEnabled);
-      // Set to !aEnabled and reset back to aEnabled.
-      return test(!aEnabled).then(test.bind(null, aEnabled));
-    });
-});
--- a/dom/cellbroadcast/src/CellBroadcast.cpp
+++ b/dom/cellbroadcast/src/CellBroadcast.cpp
@@ -12,17 +12,17 @@
 #define NS_RILCONTENTHELPER_CONTRACTID "@mozilla.org/ril/content-helper;1"
 
 using namespace mozilla::dom;
 
 /**
  * CellBroadcast::Listener Implementation.
  */
 
-class CellBroadcast::Listener : public nsICellBroadcastListener
+class CellBroadcast::Listener MOZ_FINAL : public nsICellBroadcastListener
 {
 private:
   CellBroadcast* mCellBroadcast;
 
 public:
   NS_DECL_ISUPPORTS
   NS_FORWARD_SAFE_NSICELLBROADCASTLISTENER(mCellBroadcast)
 
--- a/dom/icc/src/IccListener.h
+++ b/dom/icc/src/IccListener.h
@@ -9,17 +9,17 @@
 #include "nsIIccProvider.h"
 
 namespace mozilla {
 namespace dom {
 
 class IccManager;
 class Icc;
 
-class IccListener : public nsIIccListener
+class IccListener MOZ_FINAL : public nsIIccListener
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIICCLISTENER
 
   IccListener(IccManager* aIccManager, uint32_t aClientId);
   ~IccListener();
 
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -759,21 +759,16 @@ TabChild::Observe(nsISupports *aSubject,
       nsCOMPtr<nsIDocument> doc(GetDocument());
 
       if (SameCOMIdentity(subject, doc)) {
         nsCOMPtr<nsIDOMWindowUtils> utils(GetDOMWindowUtils());
         utils->SetIsFirstPaint(true);
 
         mContentDocumentIsDisplayed = true;
 
-        // Reset CSS viewport and zoom to default on new page, then
-        // calculate them properly using the actual metadata from the
-        // page.
-        SetCSSViewport(kDefaultViewportSize);
-
         // In some cases before-first-paint gets called before
         // RecvUpdateDimensions is called and therefore before we have an
         // mInnerSize value set. In such cases defer initializing the viewport
         // until we we get an inner size.
         if (HasValidInnerSize()) {
           InitializeRootMetrics();
           utils->SetResolution(mLastRootMetrics.mResolution.scale,
                                mLastRootMetrics.mResolution.scale);
--- a/dom/network/interfaces/moz.build
+++ b/dom/network/interfaces/moz.build
@@ -13,12 +13,13 @@ XPIDL_SOURCES += [
     'nsITCPSocketChild.idl',
     'nsITCPSocketParent.idl',
     'nsIUDPSocketChild.idl',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk':
     XPIDL_SOURCES += [
         'nsIDOMNetworkStatsManager.idl',
+        'nsIEthernetManager.idl',
         'nsINetworkStatsServiceProxy.idl',
     ]
 
 XPIDL_MODULE = 'dom_network'
new file mode 100644
--- /dev/null
+++ b/dom/network/interfaces/nsIEthernetManager.idl
@@ -0,0 +1,137 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, function, uuid(2a3ad56c-edc0-439f-8aae-900b331ddf49)]
+interface nsIEthernetManagerCallback : nsISupports
+{
+  /**
+   * Callback function used to report the success of different operations.
+   *
+   * @param success
+   *        Boolean value indicates the success of an operation.
+   * @prarm message
+   *        Message reported in the end of operation.
+   */
+  void notify(in boolean success, in DOMString message);
+};
+
+[scriptable, function, uuid(1746e7dd-92d4-43fa-8ef4-bc13d0b60353)]
+interface nsIEthernetManagerScanCallback : nsISupports
+{
+  /**
+   * Callback function used to report the result of scan function.
+   *
+   * @param list
+   *        List of available ethernet interfaces.
+   */
+  void notify(in jsval list);
+};
+
+/**
+ * An internal idl provides control to ethernet interfaces.
+ */
+[scriptable, uuid(a96441dd-36b3-4f7f-963b-2c032e28a039)]
+interface nsIEthernetManager : nsISupports
+{
+  /**
+   * List of exisiting interface name.
+   */
+  readonly attribute jsval interfaceList;
+
+  /**
+   * Scan available ethernet interfaces on device.
+   *
+   * @param callback
+   *        Callback function.
+   */
+  void scan(in nsIEthernetManagerScanCallback callback);
+
+  /**
+   * Add a new interface to the interface list.
+   *
+   * @param ifname
+   *        Interface name. Should be the form of "eth*".
+   * @param callback
+   *        Callback function.
+   */
+  void addInterface(in DOMString ifname,
+                    in nsIEthernetManagerCallback callback);
+
+  /**
+   * Remove an existing interface from the interface list.
+   *
+   * @param ifname
+   *        Interface name.
+   * @param Callback
+   *        Callback function.
+   */
+  void removeInterface(in DOMString ifname,
+                       in nsIEthernetManagerCallback callback);
+
+  /**
+   * Update a conifg of an existing interface in the interface list.
+   *
+   * @param ifname
+   *        Interface name.
+   * @param config
+   *        .ip: ip address.
+   *        .prefixLength: mask length.
+   *        .gateway: gateway.
+   *        .dnses: dnses.
+   *        .httpProxyHost: http proxy host.
+   *        .httpProxyPort: http porxy port.
+   *        .ipMode: ip mode, can be 'dhcp' or 'static'.
+   * @param callback
+   *        Callback function.
+   */
+  void updateInterfaceConfig(in DOMString ifname,
+                             in jsval config,
+                             in nsIEthernetManagerCallback callback);
+
+  /**
+   * Enable networking of an existing interface in the interface list.
+   *
+   * @param ifname
+   *        Interface name.
+   * @param callback
+   *        Callback function.
+   */
+  void enable(in DOMString ifname,
+              in nsIEthernetManagerCallback callback);
+
+  /**
+   * Disable networking of an existing interface in the interface list.
+   *
+   * @param ifname
+   *        Interface name.
+   * @param callback
+   *        Callback function.
+   */
+  void disable(in DOMString ifname,
+               in nsIEthernetManagerCallback callback);
+
+  /**
+   * Make an existing interface connect to network.
+   *
+   * @param ifname
+   *        Interface name.
+   * @param callback
+   *        Callback function.
+   */
+  void connect(in DOMString ifname,
+               in nsIEthernetManagerCallback callback);
+
+  /**
+   * Disconnect a connected interface in the interface list.
+   *
+   * @param ifname
+   *        Interface name.
+   * @param callback
+   *        Callback function.
+   */
+  void disconnect(in DOMString ifname,
+                  in nsIEthernetManagerCallback callback);
+};
new file mode 100644
--- /dev/null
+++ b/dom/network/src/EthernetManager.js
@@ -0,0 +1,619 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* 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/. */
+
+"use strict";
+
+const DEBUG = false;
+function debug(s) {
+  if (DEBUG) {
+    dump("-*- EthernetManager: " + s + "\n");
+  }
+}
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const TOPIC_INTERFACE_STATE_CHANGED = "network-interface-state-changed";
+
+const ETHERNET_NETWORK_IFACE_PREFIX = "eth";
+const DEFAULT_ETHERNET_NETWORK_IFACE = "eth0";
+
+const INTERFACE_IPADDR_NULL = "0.0.0.0";