Merge mozilla-central to autoland. On a CLOSED TREE
authorDaniel Varga <dvarga@mozilla.com>
Wed, 21 Aug 2019 13:06:00 +0300
changeset 489156 a10ff596bbc9b6d7064e8f102da6957b24f43f4b
parent 489155 854e62f7f1aed9c435bceb173eba119a90dc6eba (current diff)
parent 489107 8e1e5dbe95dc0f451aa812b86c98ddfeb35fcfbc (diff)
child 489157 beeb07bef59ca1b3a0a8c399048728e476c5b3b7
push id36465
push userdvarga@mozilla.com
push dateWed, 21 Aug 2019 16:47:43 +0000
treeherdermozilla-central@4ab60925635c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to autoland. On a CLOSED TREE
build/build-clang/r355141-arm64-cfg.patch
testing/mochitest/tests/SimpleTest/ChromePowers.js
testing/specialpowers/content/SpecialPowersAPI.jsm
testing/specialpowers/content/SpecialPowersAPIParent.jsm
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -15,19 +15,16 @@
 "use strict";
 
 /* Set this to true only for debugging purpose; it makes the output noisy. */
 const kDumpAllStacks = false;
 
 const whitelist = {
   modules: new Set([
     "chrome://mochikit/content/ShutdownLeaksCollector.jsm",
-    "resource://specialpowers/SpecialPowersChild.jsm",
-    "resource://specialpowers/SpecialPowersAPI.jsm",
-    "resource://specialpowers/WrapPrivileged.jsm",
 
     "resource://gre/modules/ContentProcessSingleton.jsm",
 
     // General utilities
     "resource://gre/modules/AppConstants.jsm",
     "resource://gre/modules/AsyncShutdown.jsm",
     "resource://gre/modules/DeferredTask.jsm",
     "resource://gre/modules/PromiseUtils.jsm",
@@ -90,16 +87,19 @@ const whitelist = {
 // Items on this list are allowed to be loaded but not
 // required, as opposed to items in the main whitelist,
 // which are all required.
 const intermittently_loaded_whitelist = {
   modules: new Set([
     "resource://gre/modules/nsAsyncShutdown.jsm",
     "resource://gre/modules/sessionstore/Utils.jsm",
 
+    "resource://specialpowers/SpecialPowersChild.jsm",
+    "resource://specialpowers/WrapPrivileged.jsm",
+
     // Webcompat about:config front-end. This is presently nightly-only and
     // part of a system add-on which may not load early enough for the test.
     "resource://webcompat/AboutCompat.jsm",
   ]),
   frameScripts: new Set([]),
   processScripts: new Set([
     // Webcompat about:config front-end. This is presently nightly-only and
     // part of a system add-on which may not load early enough for the test.
--- a/browser/base/content/test/trackingUI/browser_trackingUI_socialtracking_doorhanger.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_socialtracking_doorhanger.js
@@ -28,17 +28,17 @@ add_task(async function setup() {
       ["privacy.trackingprotection.fingerprinting.enabled", false],
       ["privacy.trackingprotection.fingerprinting.annotate.enabled", false],
     ],
   });
 });
 
 async function testPopup(hasPopup, buttonToClick) {
   let numPageLoaded = gProtectionsHandler._socialTrackingSessionPageLoad;
-  let numPopupShown = SpecialPowers.getIntPref(
+  let numPopupShown = Services.prefs.getIntPref(
     "privacy.socialtracking.notification.counter",
     0
   );
 
   let promise = BrowserTestUtils.openNewForegroundTab({
     url: TRACKING_PAGE,
     gBrowser,
   });
@@ -84,44 +84,44 @@ async function testPopup(hasPopup, butto
     numPageLoaded + 1,
     "page loaded once"
   );
 
   if (hasPopup) {
     // click on the button of the popup notification
     if (typeof buttonToClick === "string") {
       is(
-        SpecialPowers.getBoolPref(
+        Services.prefs.getBoolPref(
           "privacy.socialtracking.notification.enabled",
           false
         ),
         true,
         "notification still enabled"
       );
 
       let notification = PopupNotifications.panel.firstElementChild;
       EventUtils.synthesizeMouseAtCenter(notification[buttonToClick], {});
 
       is(
-        SpecialPowers.getBoolPref(
+        Services.prefs.getBoolPref(
           "privacy.socialtracking.notification.enabled",
           true
         ),
         false,
         "notification disabled now"
       );
     }
 
-    let lastShown = SpecialPowers.getCharPref(
+    let lastShown = Services.prefs.getCharPref(
       "privacy.socialtracking.notification.lastShown",
       "0"
     );
     ok(lastShown !== "0", "last shown timestamp updated");
     is(
-      SpecialPowers.getIntPref(
+      Services.prefs.getIntPref(
         "privacy.socialtracking.notification.counter",
         0
       ),
       numPopupShown + 1,
       "notification counter increased"
     );
   }
 
@@ -216,17 +216,17 @@ add_task(async function testSocialTracki
         ["privacy.socialtracking.notification.counter", 0],
         ["privacy.socialtracking.notification.max", 999],
       ],
     },
   ];
 
   for (let config of configs) {
     ok(config.description, config.description);
-    SpecialPowers.pushPrefEnv({
+    await SpecialPowers.pushPrefEnv({
       set: config.prefs,
     });
     for (let result of config.results) {
       await testPopup(result, config.button);
     }
-    SpecialPowers.popPrefEnv();
+    await SpecialPowers.popPrefEnv();
   }
 });
--- a/browser/components/downloads/test/browser/head.js
+++ b/browser/components/downloads/test/browser/head.js
@@ -136,17 +136,17 @@ async function setDownloadDir() {
         // On Windows debug build this may fail.
       }
     });
   }
 
   await SpecialPowers.pushPrefEnv({
     set: [
       ["browser.download.folderList", 2],
-      ["browser.download.dir", tmpDir, Ci.nsIFile],
+      ["browser.download.dir", tmpDir.path],
     ],
   });
 }
 
 let gHttpServer = null;
 function startServer() {
   gHttpServer = new HttpServer();
   gHttpServer.start(-1);
--- a/browser/components/extensions/test/browser/browser-remote.ini
+++ b/browser/components/extensions/test/browser/browser-remote.ini
@@ -1,16 +1,22 @@
 [DEFAULT]
 # This is a horrible hack:
 # In order to run tests under two configurations, we create two browser test
 # manifests, and include a manifest with a common set of tests from each. In
 # order to detect which manifest we're running from, we install the tests listed
 # in this manifest to the sub-directory "test-oop-extensions", and then check
 # whether we're running from that directory from head.js
 install-to-subdir = test-oop-extensions
+prefs =
+  extensions.webextensions.remote=true
+  # We don't want to reset this at the end of the test, so that we don't have
+  # to spawn a new extension child process for each test unit.
+  dom.ipc.keepProcessesAlive.extension=1
+
 tags = webextensions remote-webextensions
 skip-if = !e10s
 support-files =
   head.js
   head_devtools.js
   file_inspectedwindow_reload_target.sjs
 
 [browser_ext_contentscript_nontab_connect.js]
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -1,11 +1,13 @@
 [DEFAULT]
 tags = webextensions in-process-webextensions
 support-files =
   head.js
+prefs =
+  extensions.webextensions.remote=false
 
 [browser_ext_autocompletepopup.js]
 [browser_ext_windows_allowScriptsToClose.js]
 
 [include:browser-common.ini]
 skip-if = os == 'win' # Windows WebExtensions always run OOP
 [parent:browser-common.ini]
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -541,16 +541,17 @@ add_task(async function testBadgeColorPe
 
   await BrowserTestUtils.closeWindow(win);
   await extension.unload();
 });
 
 add_task(async function testPropertyRemoval() {
   await runTests({
     manifest: {
+      name: "Generated extension",
       browser_action: {
         default_icon: "default.png",
         default_popup: "default.html",
         default_title: "Default Title",
       },
     },
 
     files: {
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js
@@ -1,12 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/);
+
 // Test that an error is thrown when providing invalid icon sizes
 add_task(async function testInvalidIconSizes() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       browser_action: {},
       page_action: {},
     },
 
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -1,12 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/);
+
 const { GlobalManager } = ChromeUtils.import(
   "resource://gre/modules/Extension.jsm",
   null
 );
 
 function assertViewCount(extension, count) {
   let ext = GlobalManager.extensionMap.get(extension.id);
   is(
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js
@@ -1,12 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/);
+
 function getExtension(page_action) {
   return ExtensionTestUtils.loadExtension({
     manifest: {
       page_action,
     },
     background: function() {
       browser.test.onMessage.addListener(
         async ({ method, param, expect, msg }) => {
--- a/browser/components/extensions/test/browser/browser_ext_tabs_query.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_query.js
@@ -311,23 +311,23 @@ add_task(async function() {
         });
       });
       browser.test.sendMessage("ready");
     },
   });
 
   const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
   registerCleanupFunction(() => {
-    SpecialPowers.clearUserPref(RESOLUTION_PREF);
+    Services.prefs.clearUserPref(RESOLUTION_PREF);
   });
 
   await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
 
   for (let resolution of [2, 1]) {
-    SpecialPowers.setCharPref(RESOLUTION_PREF, String(resolution));
+    Services.prefs.setCharPref(RESOLUTION_PREF, String(resolution));
     is(
       window.devicePixelRatio,
       resolution,
       "window has the required resolution"
     );
 
     let { clientHeight, clientWidth } = gBrowser.selectedBrowser;
 
@@ -337,17 +337,17 @@ add_task(async function() {
     is(dims.height, clientHeight, "tab reports expected height");
   }
 
   await extension.unload();
 
   BrowserTestUtils.removeTab(tab1);
   BrowserTestUtils.removeTab(tab2);
   BrowserTestUtils.removeTab(tab3);
-  SpecialPowers.clearUserPref(RESOLUTION_PREF);
+  Services.prefs.clearUserPref(RESOLUTION_PREF);
 });
 
 add_task(async function testQueryPermissions() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: [],
     },
 
--- a/browser/components/extensions/test/browser/browser_ext_themes_validation.js
+++ b/browser/components/extensions/test/browser/browser_ext_themes_validation.js
@@ -1,10 +1,12 @@
 "use strict";
 
+PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/);
+
 /**
  * Helper function for testing a theme with invalid properties.
  * @param {object} invalidProps The invalid properties to load the theme with.
  */
 async function testThemeWithInvalidProperties(invalidProps) {
   let manifest = {
     theme: {},
   };
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -63,30 +63,16 @@ XPCOMUtils.defineLazyGetter(this, "Manag
 if (AppConstants.ASAN) {
   SimpleTest.requestLongerTimeout(10);
 }
 
 function loadTestSubscript(filePath) {
   Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
 }
 
-// We run tests under two different configurations, from browser.ini and
-// browser-remote.ini. When running from browser-remote.ini, the tests are
-// copied to the sub-directory "test-oop-extensions", which we detect here, and
-// use to select our configuration.
-let remote = gTestPath.includes("test-oop-extensions");
-SpecialPowers.pushPrefEnv({
-  set: [["extensions.webextensions.remote", remote]],
-});
-if (remote) {
-  // We don't want to reset this at the end of the test, so that we don't have
-  // to spawn a new extension child process for each test unit.
-  SpecialPowers.setIntPref("dom.ipc.keepProcessesAlive.extension", 1);
-}
-
 // Don't try to create screenshots of sites we load during tests.
 Services.prefs
   .getDefaultBranch("browser.newtabpage.activity-stream.")
   .setBoolPref("feeds.topsites", false);
 
 {
   // Touch the recipeParentPromise lazy getter so we don't get
   // `this._recipeManager is undefined` errors during tests.
--- a/browser/components/places/tests/browser/browser_copy_query_without_tree.js
+++ b/browser/components/places/tests/browser/browser_copy_query_without_tree.js
@@ -38,17 +38,17 @@ add_task(async function copy_toolbar_sho
   is(
     library.PlacesOrganizer._places.selectedNode.type,
     Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
     "original is still a folder shortcut"
   );
 });
 
 add_task(async function copy_mobile_shortcut() {
-  SpecialPowers.pushPrefEnv({
+  await SpecialPowers.pushPrefEnv({
     set: [["browser.bookmarks.showMobileBookmarks", true]],
   });
   await promisePlacesInitComplete();
 
   let library = await promiseLibrary();
 
   registerCleanupFunction(async () => {
     library.close();
--- a/build/build-clang/clang-win64.json
+++ b/build/build-clang/clang-win64.json
@@ -6,14 +6,13 @@
     "python_path": "c:/mozilla-build/python/python.exe",
     "cc": "cl.exe",
     "cxx": "cl.exe",
     "ml": "ml64.exe",
     "patches": [
       "workaround-issue38586.patch",
       "unpoison-thread-stacks.patch",
       "downgrade-mangling-error.patch",
-      "r355141-arm64-cfg.patch",
       "r357725-asan-vs2019.patch",
       "loosen-msvc-detection.patch",
       "revert-r355311.patch"
     ]
 }
deleted file mode 100644
--- a/build/build-clang/r355141-arm64-cfg.patch
+++ /dev/null
@@ -1,112 +0,0 @@
-[COFF] Add address-taken import thunks to the fid table
-
-https://bugs.llvm.org/show_bug.cgi?id=39799
-https://reviews.llvm.org/D58739
-
---- a/lld/COFF/Writer.cpp
-+++ b/lld/COFF/Writer.cpp
-@@ -1390,19 +1390,47 @@
- // symbol in an executable section.
- static void maybeAddAddressTakenFunction(SymbolRVASet &AddressTakenSyms,
-                                          Symbol *S) {
--  auto *D = dyn_cast_or_null<DefinedCOFF>(S);
--
--  // Ignore undefined symbols and references to non-functions (e.g. globals and
--  // labels).
--  if (!D ||
--      D->getCOFFSymbol().getComplexType() != COFF::IMAGE_SYM_DTYPE_FUNCTION)
-+  if (!S)
-     return;
- 
--  // Mark the symbol as address taken if it's in an executable section.
--  Chunk *RefChunk = D->getChunk();
--  OutputSection *OS = RefChunk ? RefChunk->getOutputSection() : nullptr;
--  if (OS && OS->Header.Characteristics & IMAGE_SCN_MEM_EXECUTE)
--    addSymbolToRVASet(AddressTakenSyms, D);
-+  switch (S->kind()) {
-+  case Symbol::DefinedLocalImportKind:
-+  case Symbol::DefinedImportDataKind:
-+    // Defines an __imp_ pointer, so it is data, so it is ignored.
-+    break;
-+  case Symbol::DefinedCommonKind:
-+    // Common is always data, so it is ignored.
-+    break;
-+  case Symbol::DefinedAbsoluteKind:
-+  case Symbol::DefinedSyntheticKind:
-+    // Absolute is never code, synthetic generally isn't and usually isn't
-+    // determinable.
-+    break;
-+  case Symbol::LazyKind:
-+  case Symbol::UndefinedKind:
-+    // Undefined symbols resolve to zero, so they don't have an RVA. Lazy
-+    // symbols shouldn't have relocations.
-+    break;
-+
-+  case Symbol::DefinedImportThunkKind:
-+    // Thunks are always code, include them.
-+    addSymbolToRVASet(AddressTakenSyms, cast<Defined>(S));
-+    break;
-+
-+  case Symbol::DefinedRegularKind: {
-+    // This is a regular, defined, symbol from a COFF file. Mark the symbol as
-+    // address taken if the symbol type is function and it's in an executable
-+    // section.
-+    auto *D = cast<DefinedRegular>(S);
-+    if (D->getCOFFSymbol().getComplexType() == COFF::IMAGE_SYM_DTYPE_FUNCTION) {
-+      Chunk *RefChunk = D->getChunk();
-+      OutputSection *OS = RefChunk ? RefChunk->getOutputSection() : nullptr;
-+      if (OS && OS->Header.Characteristics & IMAGE_SCN_MEM_EXECUTE)
-+        addSymbolToRVASet(AddressTakenSyms, D);
-+    }
-+    break;
-+  }
-+  }
- }
- 
- // Visit all relocations from all section contributions of this object file and
---- a/lld/test/COFF/guardcf-thunk.s
-+++ b/lld/test/COFF/guardcf-thunk.s
-@@ -0,0 +1,43 @@
-+# REQUIRES: x86
-+
-+# Make a DLL that exports exportfn1.
-+# RUN: yaml2obj < %p/Inputs/export.yaml > %t.obj
-+# RUN: lld-link /out:%t.dll /dll %t.obj /export:exportfn1 /implib:%t.lib
-+
-+# Make an obj that takes the address of that exported function.
-+# RUN: llvm-mc -filetype=obj -triple=x86_64-windows-msvc %s -o %t2.obj
-+# RUN: lld-link -entry:main -guard:cf %t2.obj %t.lib -nodefaultlib -out:%t.exe
-+# RUN: llvm-readobj -coff-load-config %t.exe | FileCheck %s
-+
-+# Check that the gfids table contains *exactly* two entries, one for exportfn1
-+# and one for main.
-+# CHECK: GuardFidTable [
-+# CHECK-NEXT: 0x{{[0-9A-Fa-f]+0$}}
-+# CHECK-NEXT: 0x{{[0-9A-Fa-f]+0$}}
-+# CHECK-NEXT: ]
-+
-+
-+        .def     @feat.00;
-+        .scl    3;
-+        .type   0;
-+        .endef
-+        .globl  @feat.00
-+@feat.00 = 0x001
-+
-+        .section .text,"rx"
-+        .def     main; .scl    2; .type   32; .endef
-+        .global main
-+main:
-+        leaq exportfn1(%rip), %rax
-+        retq
-+
-+        .section .rdata,"dr"
-+.globl _load_config_used
-+_load_config_used:
-+        .long 256
-+        .fill 124, 1, 0
-+        .quad __guard_fids_table
-+        .quad __guard_fids_count
-+        .long __guard_flags
-+        .fill 128, 1, 0
-+
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -26,16 +26,17 @@ skip-if = (os == "win" && ccov) # Bug 14
 skip-if = ccov || debug || (verify && debug && (os == 'linux')) # Bug 1441545, 1536253 - very slow on debug
 [browser_dbg-sourcemapped-stepping.js]
 skip-if = true # original stepping is currently disabled
 [browser_dbg-sourcemapped-toggle.js]
 [browser_dbg-sourcemapped-preview.js]
 skip-if = os == "win" # Bug 1448523, Bug 1448450
 [browser_dbg-breaking.js]
 [browser_dbg-breaking-from-console.js]
+skip-if = debug # Window leaks: bug 1575332
 [browser_dbg-breakpoints.js]
 [browser_dbg-breakpoints-actions.js]
 [browser_dbg-breakpoints-columns.js]
 [browser_dbg-breakpoints-cond.js]
 [browser_dbg-breakpoints-cond-source-maps.js]
 [browser_dbg-breakpoints-duplicate-functions.js]
 [browser_dbg-browser-content-toolbox.js]
 skip-if = !e10s || verify # This test is only valid in e10s
@@ -57,16 +58,17 @@ skip-if = (verify && !debug && (os == 'l
 skip-if = (os == "win" && ccov) # Bug 1424154
 [browser_dbg-debug-line.js]
 [browser_dbg-debugger-buttons.js]
 [browser_dbg-editor-gutter.js]
 [browser_dbg-editor-scroll.js]
 [browser_dbg-editor-select.js]
 [browser_dbg-editor-highlight.js]
 [browser_dbg-ember-quickstart.js]
+skip-if = debug # Window leaks: bug 1575332
 [browser_dbg-expressions.js]
 [browser_dbg-expressions-error.js]
 [browser_dbg-expressions-focus.js]
 [browser_dbg-go-to-line.js]
 [browser_dbg-html-breakpoints.js]
 [browser_dbg-iframes.js]
 [browser_dbg-inline-cache.js]
 skip-if = ccov && os == 'win' # Bug 1443132
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -48,16 +48,20 @@ support-files =
   test_chrome_page.html
   !/devtools/client/debugger/test/mochitest/head.js
   !/devtools/client/debugger/test/mochitest/helpers.js
   !/devtools/client/debugger/test/mochitest/helpers/context.js
   !/devtools/client/shared/test/frame-script-utils.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/shared-redux-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
+# This is far from ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1565279
+# covers removing this pref flip.
+prefs =
+  security.allow_unsafe_parent_loads=true
 
 [browser_about-devtools-toolbox_load.js]
 [browser_about-devtools-toolbox_reload.js]
 [browser_browser_toolbox.js]
 skip-if = coverage # Bug 1387827
 [browser_browser_toolbox_debugger.js]
 skip-if = os == 'win' || debug # Bug 1282269, 1448084
 [browser_devtools_api.js]
--- a/devtools/client/framework/test/head.js
+++ b/devtools/client/framework/test/head.js
@@ -7,22 +7,16 @@
 // shared-head.js handles imports, constants, and utility functions
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
   this
 );
 
 const EventEmitter = require("devtools/shared/event-emitter");
 
-// This is far from ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1565279
-// covers removing this pref flip.
-SpecialPowers.pushPrefEnv({
-  set: [["security.allow_unsafe_parent_loads", true]],
-});
-
 function toggleAllTools(state) {
   for (const [, tool] of gDevTools._tools) {
     if (!tool.visibilityswitch) {
       continue;
     }
     if (state) {
       Services.prefs.setBoolPref(tool.visibilityswitch, true);
     } else {
--- a/devtools/client/jsonview/test/browser_jsonview_theme.js
+++ b/devtools/client/jsonview/test/browser_jsonview_theme.js
@@ -3,27 +3,27 @@
 
 "use strict";
 
 const TEST_JSON_URL = URL_ROOT + "valid_json.json";
 
 add_task(async function() {
   info("Test JSON theme started.");
 
-  const oldPref = SpecialPowers.getCharPref("devtools.theme");
-  SpecialPowers.setCharPref("devtools.theme", "light");
+  const oldPref = Services.prefs.getCharPref("devtools.theme");
+  Services.prefs.setCharPref("devtools.theme", "light");
 
   await addJsonViewTab(TEST_JSON_URL);
 
   is(await getTheme(), "theme-light", "The initial theme is light");
 
-  SpecialPowers.setCharPref("devtools.theme", "dark");
+  Services.prefs.setCharPref("devtools.theme", "dark");
   is(await getTheme(), "theme-dark", "Theme changed to dark");
 
-  SpecialPowers.setCharPref("devtools.theme", "light");
+  Services.prefs.setCharPref("devtools.theme", "light");
   is(await getTheme(), "theme-light", "Theme changed to light");
 
-  SpecialPowers.setCharPref("devtools.theme", oldPref);
+  Services.prefs.setCharPref("devtools.theme", oldPref);
 });
 
 function getTheme() {
   return getElementAttr(":root", "class");
 }
--- a/docshell/test/chrome/bug293235_window.xul
+++ b/docshell/test/chrome/bug293235_window.xul
@@ -3,17 +3,16 @@
 
 <window id="293235Test"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         width="600"
         height="600"
         onload="setTimeout(runTests, 0);"
         title="bug 293235 test">
 
-  <script src="chrome://mochikit/content/tests/SimpleTest/ChromePowers.js"/>
   <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
   <script type="application/javascript" src="docshell_helpers.js" />
   <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
 
   <script type="application/javascript"><![CDATA[
     var {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 
     // Return the Element object for the specified element id
--- a/docshell/test/chrome/bug396649_window.xul
+++ b/docshell/test/chrome/bug396649_window.xul
@@ -3,17 +3,16 @@
 
 <window id="396649Test"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         width="600"
         height="600"
         onload="setTimeout(nextTest, 0);"
         title="bug 396649 test">
 
-  <script src="chrome://mochikit/content/tests/SimpleTest/ChromePowers.js"/>
   <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
   <script type="application/javascript" src="docshell_helpers.js" />
   <script type="application/javascript"><![CDATA[
   
     // Define the generator-iterator for the tests.
     var tests = testIterator();
     
     // Maximum number of entries in the bfcache for this session history.
--- a/docshell/test/chrome/bug89419_window.xul
+++ b/docshell/test/chrome/bug89419_window.xul
@@ -4,17 +4,16 @@
 <window id="89419Test"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         width="600"
         height="600"
         onload="setTimeout(runTests, 0);"
         title="bug 89419 test">
 
   <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
-  <script src="chrome://mochikit/content/tests/SimpleTest/ChromePowers.js"/>
   <script type="application/javascript" src="docshell_helpers.js" />
   <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
 
   <script type="application/javascript"><![CDATA[
     ////
     // A visited link should have the :visited style applied
     // to it when displayed on a page which was fetched from
     // the bfcache.
--- a/dom/chrome-webidl/JSWindowActor.webidl
+++ b/dom/chrome-webidl/JSWindowActor.webidl
@@ -68,35 +68,38 @@ JSWindowActorChild implements JSWindowAc
  * NOTE: This isn't marked as ChromeOnly, as it has no interface object, and
  * thus cannot be conditionally exposed.
  */
 callback interface MozObserverCallback {
   void observe(nsISupports subject, ByteString topic, DOMString? data);
 };
 
 /**
- * WebIDL callback interface calling the `willDestroy` and `didDestroy`
- * method on JSWindowActors.
+ * WebIDL callback interface calling the `willDestroy`, `didDestroy`, and
+ * `actorCreated` methods on JSWindowActors.
  */
 [MOZ_CAN_RUN_SCRIPT_BOUNDARY]
-callback MozActorDestroyCallback = void();
+callback MozJSWindowActorCallback = void();
 
 /**
  * The willDestroy method, if present, will be called at the last opportunity
  * to send messages to the remote side, giving implementers the chance to clean
  * up and send final messages.
  * The didDestroy method, if present, will be called after the actor is no
  * longer able to receive any more messages.
+ * The actorCreated method, if present, will be called immediately after the
+ * actor has been created and initialized.
  *
  * NOTE: Messages may be received between willDestroy and didDestroy, but they
  * may not be sent.
  */
-dictionary MozActorDestroyCallbacks {
-  [ChromeOnly] MozActorDestroyCallback willDestroy;
-  [ChromeOnly] MozActorDestroyCallback didDestroy;
+dictionary MozJSWindowActorCallbacks {
+  [ChromeOnly] MozJSWindowActorCallback willDestroy;
+  [ChromeOnly] MozJSWindowActorCallback didDestroy;
+  [ChromeOnly] MozJSWindowActorCallback actorCreated;
 };
 
 /**
  * Used by ChromeUtils.registerWindowActor() to register JS window actor.
  */
 dictionary WindowActorOptions {
   /**
    * If this is set to `true`, allow this actor to be created for subframes,
--- a/dom/docs/Fission.rst
+++ b/dom/docs/Fission.rst
@@ -139,16 +139,21 @@ If there's something you need to do as s
 
 If you register your Actor to listen for ``nsIObserver`` notifications, implement an ``observe`` method with the above signature to handle the notification.
 
 ``handleEvent(event)``
 ``````````````````````
 
 If you register your Actor to listen for content events, implement a ``handleEvent`` method with the above signature to handle the event.
 
+``actorCreated``
+````````````````
+
+This method is called immediately after a child actor is created and initialized. Unlike the actor's constructor, it is possible to do things like access the actor's content window and send messages from this callback.
+
 ``willDestroy``
 ```````````````
 
 This method will be called when we know that the JSWindowActor pair is going to be destroyed because the associated BrowsingContext is going away. You should override this method if you have any cleanup you need to do before going away.
 
 You can also use ``willDestroy`` as a last opportunity to send messages to the other side, as the communications channel at this point is still running.
 
 .. note::
--- a/dom/ipc/JSWindowActor.cpp
+++ b/dom/ipc/JSWindowActor.cpp
@@ -33,42 +33,46 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingQueries)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(JSWindowActor)
 
 JSWindowActor::JSWindowActor() : mNextQueryId(0) {}
 
 void JSWindowActor::StartDestroy() {
-  DestroyCallback(DestroyCallbackFunction::WillDestroy);
+  InvokeCallback(CallbackFunction::WillDestroy);
 }
 
 void JSWindowActor::AfterDestroy() {
-  DestroyCallback(DestroyCallbackFunction::DidDestroy);
+  InvokeCallback(CallbackFunction::DidDestroy);
 }
 
-void JSWindowActor::DestroyCallback(DestroyCallbackFunction callback) {
+void JSWindowActor::InvokeCallback(CallbackFunction callback) {
   AutoEntryScript aes(GetParentObject(), "JSWindowActor destroy callback");
   JSContext* cx = aes.cx();
-  MozActorDestroyCallbacks callbacksHolder;
+  MozJSWindowActorCallbacks callbacksHolder;
   NS_ENSURE_TRUE_VOID(GetWrapper());
   JS::Rooted<JS::Value> val(cx, JS::ObjectValue(*GetWrapper()));
   if (NS_WARN_IF(!callbacksHolder.Init(cx, val))) {
     return;
   }
 
   // Destroy callback is optional.
-  if (callback == DestroyCallbackFunction::WillDestroy) {
+  if (callback == CallbackFunction::WillDestroy) {
     if (callbacksHolder.mWillDestroy.WasPassed()) {
       callbacksHolder.mWillDestroy.Value()->Call(this);
     }
-  } else {
+  } else if (callback == CallbackFunction::DidDestroy) {
     if (callbacksHolder.mDidDestroy.WasPassed()) {
       callbacksHolder.mDidDestroy.Value()->Call(this);
     }
+  } else {
+    if (callbacksHolder.mActorCreated.WasPassed()) {
+      callbacksHolder.mActorCreated.Value()->Call(this);
+    }
   }
 }
 
 void JSWindowActor::RejectPendingQueries() {
   // Take our queries out, in case somehow rejecting promises can trigger
   // additions or removals.
   nsRefPtrHashtable<nsUint64HashKey, Promise> pendingQueries;
   mPendingQueries.SwapElements(pendingQueries);
--- a/dom/ipc/JSWindowActor.h
+++ b/dom/ipc/JSWindowActor.h
@@ -34,17 +34,17 @@ class QueryPromiseHandler;
 class JSWindowActor : public nsISupports, public nsWrapperCache {
  public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(JSWindowActor)
 
   JSWindowActor();
 
   enum class Type { Parent, Child };
-  enum class DestroyCallbackFunction { WillDestroy, DidDestroy };
+  enum class CallbackFunction { WillDestroy, DidDestroy, ActorCreated };
 
   const nsString& Name() const { return mName; }
 
   void SendAsyncMessage(JSContext* aCx, const nsAString& aMessageName,
                         JS::Handle<JS::Value> aObj,
                         JS::Handle<JS::Value> aTransfers, ErrorResult& aRv);
 
   already_AddRefed<Promise> SendQuery(JSContext* aCx,
@@ -71,17 +71,17 @@ class JSWindowActor : public nsISupports
   virtual ~JSWindowActor() = default;
 
   void SetName(const nsAString& aName);
 
   void StartDestroy();
 
   void AfterDestroy();
 
-  void DestroyCallback(DestroyCallbackFunction willDestroy);
+  void InvokeCallback(CallbackFunction willDestroy);
 
  private:
   void ReceiveMessageOrQuery(JSContext* aCx,
                              const JSWindowActorMessageMeta& aMetadata,
                              JS::Handle<JS::Value> aData, ErrorResult& aRv);
 
   void ReceiveQueryReply(JSContext* aCx,
                          const JSWindowActorMessageMeta& aMetadata,
--- a/dom/ipc/JSWindowActorChild.cpp
+++ b/dom/ipc/JSWindowActorChild.cpp
@@ -30,16 +30,18 @@ JSObject* JSWindowActorChild::WrapObject
 
 WindowGlobalChild* JSWindowActorChild::GetManager() const { return mManager; }
 
 void JSWindowActorChild::Init(const nsAString& aName,
                               WindowGlobalChild* aManager) {
   MOZ_ASSERT(!mManager, "Cannot Init() a JSWindowActorChild twice!");
   SetName(aName);
   mManager = aManager;
+
+  InvokeCallback(CallbackFunction::ActorCreated);
 }
 
 namespace {
 
 class AsyncMessageToParent : public Runnable {
  public:
   AsyncMessageToParent(const JSWindowActorMessageMeta& aMetadata,
                        ipc::StructuredCloneData&& aData,
--- a/dom/tests/browser/browser_frame_elements.js
+++ b/dom/tests/browser/browser_frame_elements.js
@@ -76,19 +76,19 @@ function startTests() {
     objectDataUrl.contentWindow.parent,
     gWindow,
     "gWindow is parent"
   );
 }
 
 async function mozBrowserTests(browser) {
   info("Granting special powers for mozbrowser");
-  SpecialPowers.addPermission("browser", true, TEST_URI);
-  SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true);
-  SpecialPowers.setBoolPref("network.disable.ipc.security", true);
+  await SpecialPowers.addPermission("browser", true, TEST_URI);
+  Services.prefs.setBoolPref("dom.mozBrowserFramesEnabled", true);
+  Services.prefs.setBoolPref("network.disable.ipc.security", true);
 
   await ContentTask.spawn(browser, null, function() {
     info("Checking mozbrowser iframe");
     let mozBrowserFrame = content.document.createElement("iframe");
     mozBrowserFrame.setAttribute("mozbrowser", "");
     content.document.body.appendChild(mozBrowserFrame);
     Assert.equal(
       mozBrowserFrame.contentWindow.top,
@@ -98,12 +98,12 @@ async function mozBrowserTests(browser) 
     Assert.equal(
       mozBrowserFrame.contentWindow.parent,
       mozBrowserFrame.contentWindow,
       "Mozbrowser parent == iframe window"
     );
   });
 
   info("Revoking special powers for mozbrowser");
-  SpecialPowers.clearUserPref("dom.mozBrowserFramesEnabled");
-  SpecialPowers.clearUserPref("network.disable.ipc.security");
-  SpecialPowers.removePermission("browser", TEST_URI);
+  Services.prefs.clearUserPref("dom.mozBrowserFramesEnabled");
+  Services.prefs.clearUserPref("network.disable.ipc.security");
+  await SpecialPowers.removePermission("browser", TEST_URI);
 }
--- a/taskcluster/ci/fetch/toolchains.yml
+++ b/taskcluster/ci/fetch/toolchains.yml
@@ -389,21 +389,21 @@ clang-4.0:
 clang-7:
     description: clang 7 source code
     fetch:
         type: git
         repo: https://github.com/llvm/llvm-project
         revision: d0d8eb2e5415b8be29343e3c17a18e49e67b5551
 
 clang-8:
-    description: clang 8 source code
+    description: clang 8.0.1 source code
     fetch:
         type: git
         repo: https://github.com/llvm/llvm-project
-        revision: d2298e74235598f15594fe2c99bbac870a507c59
+        revision: 19a71f6bdf2dddb10764939e7f0ec2b98dba76c9
 
 ninja:
     description: ninja 1.9.0
     fetch:
         type: static-url
         url: https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-win.zip
         sha256: 2d70010633ddaacc3af4ffbd21e22fae90d158674a09e132e06424ba3ab036e9
         size: 254497
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -452,35 +452,39 @@ function Tester(aTests, structuredLogger
   Cu.permitCPOWsInScope(this.cpowSandbox);
 
   this.cpowEventUtils = new this.cpowSandbox.Object();
   this._scriptLoader.loadSubScript(
     "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
     this.cpowEventUtils
   );
 
+  // Make sure our SpecialPowers actor is instantiated, in case it was
+  // registered after our DOMWindowCreated event was fired (which it
+  // most likely was).
+  window.getWindowGlobalChild().getActor("SpecialPowers");
+
   var simpleTestScope = {};
   this._scriptLoader.loadSubScript(
-    "chrome://mochikit/content/tests/SimpleTest/ChromePowers.js",
-    simpleTestScope
-  );
-  this._scriptLoader.loadSubScript(
     "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js",
     simpleTestScope
   );
   this._scriptLoader.loadSubScript(
     "chrome://mochikit/content/tests/SimpleTest/MemoryStats.js",
     simpleTestScope
   );
   this._scriptLoader.loadSubScript(
     "chrome://mochikit/content/chrome-harness.js",
     simpleTestScope
   );
   this.SimpleTest = simpleTestScope.SimpleTest;
 
+  window.SpecialPowers.SimpleTest = this.SimpleTest;
+  window.SpecialPowers.setAsDefaultAssertHandler();
+
   var extensionUtilsScope = {
     registerCleanupFunction: fn => {
       this.currentTest.scope.registerCleanupFunction(fn);
     },
   };
   extensionUtilsScope.SimpleTest = this.SimpleTest;
   this._scriptLoader.loadSubScript(
     "chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js",
--- a/testing/mochitest/moz.build
+++ b/testing/mochitest/moz.build
@@ -42,17 +42,16 @@ FINAL_TARGET_FILES.content.dynamic += [
 FINAL_TARGET_FILES.content.static += [
     'static/harness.css',
 ]
 
 FINAL_TARGET_FILES.content.tests.SimpleTest += [
     '../../docshell/test/chrome/docshell_helpers.js',
     '../modules/StructuredLog.jsm',
     'tests/SimpleTest/AsyncUtilsContent.js',
-    'tests/SimpleTest/ChromePowers.js',
     'tests/SimpleTest/EventUtils.js',
     'tests/SimpleTest/ExtensionTestUtils.js',
     'tests/SimpleTest/iframe-between-tests.html',
     'tests/SimpleTest/LogController.js',
     'tests/SimpleTest/MemoryStats.js',
     'tests/SimpleTest/MockObjects.js',
     'tests/SimpleTest/MozillaLogger.js',
     'tests/SimpleTest/NativeKeyCodes.js',
--- a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html
+++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html
@@ -32,18 +32,21 @@ async function interceptDiagnostics(func
 
     return diags;
   } finally {
     SimpleTest.record = originalRecord;
   }
 }
 
 add_task(async function() {
+  const frameSrc = "https://example.com/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html";
+  const subframeSrc = "https://example.org/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html";
+
   let frame = document.getElementById("iframe");
-  frame.src = "https://example.com/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html";
+  frame.src = frameSrc;
 
   await new Promise(resolve => {
     frame.addEventListener("load", resolve, {once: true});
   });
 
   let expected = [
     [false, "Thing - 1 == 2", "got 1, expected 2 (operator ==)"],
     [true, "Hmm - 1 == 1", undefined],
@@ -54,23 +57,41 @@ add_task(async function() {
   // Test that a representative variety of assertions work as expected, and
   // trigger the expected calls to the harness's reporting function.
   //
   // Note: Assert.jsm has its own tests, and defers all of its reporting to a
   // single reporting function, so we don't need to test it comprehensively. We
   // just need to make sure that the general functionality works as expected.
   let tests = {
     "SpecialPowers.spawn": () => {
-      return SpecialPowers.spawn(frame, [], () => {
+      return SpecialPowers.spawn(frame, [], async () => {
         Assert.equal(1, 2, "Thing");
         Assert.equal(1, 1, "Hmm");
         Assert.ok(true, "Yay.");
         Assert.ok(false, "Boo!.");
       });
     },
+    "SpecialPowers.spawn-subframe": () => {
+      return SpecialPowers.spawn(frame, [subframeSrc], async src => {
+        let frame = this.content.document.createElement("iframe");
+        frame.src = src;
+        this.content.document.body.appendChild(frame);
+
+        await new Promise(resolve => {
+          frame.addEventListener("load", resolve, { once: true });
+        });
+
+        await SpecialPowers.spawn(frame, [], () => {
+          Assert.equal(1, 2, "Thing");
+          Assert.equal(1, 1, "Hmm");
+          Assert.ok(true, "Yay.");
+          Assert.ok(false, "Boo!.");
+        });
+      });
+    },
     "SpecialPowers.loadChromeScript": async () => {
       let script = SpecialPowers.loadChromeScript(() => {
         this.addMessageListener("ping", () => "pong");
 
         Assert.equal(1, 2, "Thing");
         Assert.equal(1, 1, "Hmm");
         Assert.ok(true, "Yay.");
         Assert.ok(false, "Boo!.");
deleted file mode 100644
--- a/testing/mochitest/tests/SimpleTest/ChromePowers.js
+++ /dev/null
@@ -1,120 +0,0 @@
-/* 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/. */
-
-const {SpecialPowersAPI, bindDOMWindowUtils} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPI.jsm");
-const {SpecialPowersAPIParent} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPIParent.jsm");
-
-class ChromePowers extends SpecialPowersAPI {
-  constructor(window) {
-    super();
-
-    this.window = Cu.getWeakReference(window);
-
-    this.chromeWindow = window;
-
-    this.DOMWindowUtils = bindDOMWindowUtils(window);
-
-    this.parentActor = new SpecialPowersAPIParent();
-    this.parentActor.sendAsyncMessage = this.sendReply.bind(this);
-
-    this.listeners = new Map();
-  }
-
-  toString() { return "[ChromePowers]"; }
-  sanityCheck() { return "foo"; }
-
-  get contentWindow() {
-    return window;
-  }
-
-  get document() {
-    return window.document;
-  }
-
-  get docShell() {
-    return window.docShell;
-  }
-
-  sendReply(aType, aMsg) {
-    var msg = {name: aType, json: aMsg, data: aMsg};
-    if (!this.listeners.has(aType)) {
-      throw new Error(`No listener for ${aType}`);
-    }
-    this.listeners.get(aType)(msg);
-  }
-
-  sendAsyncMessage(aType, aMsg) {
-    var msg = {name: aType, json: aMsg, data: aMsg};
-    this.receiveMessage(msg);
-  }
-
-  async sendQuery(aType, aMsg) {
-    var msg = {name: aType, json: aMsg, data: aMsg};
-    return this.receiveMessage(msg);
-  }
-
-  _addMessageListener(aType, aCallback) {
-    if (this.listeners.has(aType)) {
-      throw new Error(`unable to handle multiple listeners for ${aType}`);
-    }
-    this.listeners.set(aType, aCallback);
-  }
-  _removeMessageListener(aType, aCallback) {
-    this.listeners.delete(aType);
-  }
-
-  registerProcessCrashObservers() {
-    this._sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
-  }
-
-  unregisterProcessCrashObservers() {
-    this._sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
-  }
-
-  receiveMessage(aMessage) {
-    switch (aMessage.name) {
-      case "SpecialPowers.Quit":
-        let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
-        appStartup.quit(Ci.nsIAppStartup.eForceQuit);
-        break;
-      case "SPProcessCrashService":
-        if (aMessage.json.op == "register-observer" || aMessage.json.op == "unregister-observer") {
-          // Hack out register/unregister specifically for browser-chrome leaks
-          break;
-        }
-      default:
-        // All calls go here, because we need to handle SPProcessCrashService calls as well
-        return this.parentActor.receiveMessage(aMessage);
-    }
-    return undefined;
-  }
-
-  quit() {
-    // We come in here as SpecialPowers.quit, but SpecialPowers is really ChromePowers.
-    // For some reason this.<func> resolves to TestRunner, so using SpecialPowers
-    // allows us to use the ChromePowers object which we defined below.
-    SpecialPowers._sendSyncMessage("SpecialPowers.Quit", {});
-  }
-
-  focus(aWindow) {
-    // We come in here as SpecialPowers.focus, but SpecialPowers is really ChromePowers.
-    // For some reason this.<func> resolves to TestRunner, so using SpecialPowers
-    // allows us to use the ChromePowers object which we defined below.
-    if (aWindow)
-      aWindow.focus();
-  }
-
-  executeAfterFlushingMessageQueue(aCallback) {
-    aCallback();
-  }
-}
-
-if (window.parent.SpecialPowers && !window.SpecialPowers) {
-  window.SpecialPowers = window.parent.SpecialPowers;
-} else {
-  ChromeUtils.import("resource://specialpowers/SpecialPowersAPIParent.jsm", this);
-
-  window.SpecialPowers = new ChromePowers(window);
-}
-
--- a/testing/mochitest/tests/SimpleTest/SimpleTest.js
+++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js
@@ -223,16 +223,18 @@ SimpleTest._timeoutFunctions = [];
 SimpleTest._inChaosMode = false;
 // When using failure pattern file to filter unexpected issues,
 // SimpleTest.expected would be an array of [pattern, expected count],
 // and SimpleTest.num_failed would be an array of actual counts which
 // has the same length as SimpleTest.expected.
 SimpleTest.expected = 'pass';
 SimpleTest.num_failed = 0;
 
+SpecialPowers.setAsDefaultAssertHandler();
+
 function usesFailurePatterns() {
   return Array.isArray(SimpleTest.expected);
 }
 
 /**
  * Checks whether there is any failure pattern matches the given error
  * message, and if found, bumps the counter of the failure pattern.
  * Returns whether a matched failure pattern is found.
--- a/testing/specialpowers/api.js
+++ b/testing/specialpowers/api.js
@@ -30,16 +30,17 @@ this.specialpowers = class extends Exten
 
     // Register special testing modules.
     Components.manager
       .QueryInterface(Ci.nsIComponentRegistrar)
       .autoRegister(FileUtils.getFile("ProfD", ["tests.manifest"]));
 
     ChromeUtils.registerWindowActor("SpecialPowers", {
       allFrames: true,
+      includeChrome: true,
       child: {
         moduleURI: "resource://specialpowers/SpecialPowersChild.jsm",
         events: {
           DOMWindowCreated: {},
         },
       },
       parent: {
         moduleURI: "resource://specialpowers/SpecialPowersParent.jsm",
deleted file mode 100644
--- a/testing/specialpowers/content/SpecialPowersAPI.jsm
+++ /dev/null
@@ -1,2128 +0,0 @@
-/* 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/. */
-/* This code is loaded in every child process that is started by mochitest in
- * order to be used as a replacement for UniversalXPConnect
- */
-
-"use strict";
-
-var EXPORTED_SYMBOLS = ["SpecialPowersAPI", "bindDOMWindowUtils"];
-
-var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-ChromeUtils.defineModuleGetter(
-  this,
-  "MockFilePicker",
-  "resource://specialpowers/MockFilePicker.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
-  "MockColorPicker",
-  "resource://specialpowers/MockColorPicker.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
-  "MockPermissionPrompt",
-  "resource://specialpowers/MockPermissionPrompt.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
-  "SpecialPowersSandbox",
-  "resource://specialpowers/SpecialPowersSandbox.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
-  "WrapPrivileged",
-  "resource://specialpowers/WrapPrivileged.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
-  "PrivateBrowsingUtils",
-  "resource://gre/modules/PrivateBrowsingUtils.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
-  "NetUtil",
-  "resource://gre/modules/NetUtil.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
-  "AppConstants",
-  "resource://gre/modules/AppConstants.jsm"
-);
-
-ChromeUtils.defineModuleGetter(
-  this,
-  "PerTestCoverageUtils",
-  "resource://testing-common/PerTestCoverageUtils.jsm"
-);
-
-// Allow stuff from this scope to be accessed from non-privileged scopes. This
-// would crash if used outside of automation.
-Cu.forcePermissiveCOWs();
-
-function bindDOMWindowUtils(aWindow) {
-  return aWindow && WrapPrivileged.wrap(aWindow.windowUtils);
-}
-
-// SPConsoleListener reflects nsIConsoleMessage objects into JS in a
-// tidy, XPCOM-hiding way.  Messages that are nsIScriptError objects
-// have their properties exposed in detail.  It also auto-unregisters
-// itself when it receives a "sentinel" message.
-function SPConsoleListener(callback) {
-  this.callback = callback;
-}
-
-SPConsoleListener.prototype = {
-  // Overload the observe method for both nsIConsoleListener and nsIObserver.
-  // The topic will be null for nsIConsoleListener.
-  observe(msg, topic) {
-    let m = {
-      message: msg.message,
-      errorMessage: null,
-      cssSelectors: null,
-      sourceName: null,
-      sourceLine: null,
-      lineNumber: null,
-      columnNumber: null,
-      category: null,
-      windowID: null,
-      isScriptError: false,
-      isConsoleEvent: false,
-      isWarning: false,
-      isException: false,
-      isStrict: false,
-    };
-    if (msg instanceof Ci.nsIScriptError) {
-      m.errorMessage = msg.errorMessage;
-      m.cssSelectors = msg.cssSelectors;
-      m.sourceName = msg.sourceName;
-      m.sourceLine = msg.sourceLine;
-      m.lineNumber = msg.lineNumber;
-      m.columnNumber = msg.columnNumber;
-      m.category = msg.category;
-      m.windowID = msg.outerWindowID;
-      m.innerWindowID = msg.innerWindowID;
-      m.isScriptError = true;
-      m.isWarning = (msg.flags & Ci.nsIScriptError.warningFlag) === 1;
-      m.isException = (msg.flags & Ci.nsIScriptError.exceptionFlag) === 1;
-      m.isStrict = (msg.flags & Ci.nsIScriptError.strictFlag) === 1;
-    } else if (topic === "console-api-log-event") {
-      // This is a dom/console event.
-      let unwrapped = msg.wrappedJSObject;
-      m.errorMessage = unwrapped.arguments[0];
-      m.sourceName = unwrapped.filename;
-      m.lineNumber = unwrapped.lineNumber;
-      m.columnNumber = unwrapped.columnNumber;
-      m.windowID = unwrapped.ID;
-      m.innerWindowID = unwrapped.innerID;
-      m.isConsoleEvent = true;
-      m.isWarning = unwrapped.level === "warning";
-    }
-
-    Object.freeze(m);
-
-    // Run in a separate runnable since console listeners aren't
-    // supposed to touch content and this one might.
-    Services.tm.dispatchToMainThread(() => {
-      this.callback.call(undefined, m);
-    });
-
-    if (!m.isScriptError && !m.isConsoleEvent && m.message === "SENTINEL") {
-      Services.obs.removeObserver(this, "console-api-log-event");
-      Services.console.unregisterListener(this);
-    }
-  },
-
-  QueryInterface: ChromeUtils.generateQI([
-    Ci.nsIConsoleListener,
-    Ci.nsIObserver,
-  ]),
-};
-
-class SpecialPowersAPI extends JSWindowActorChild {
-  constructor() {
-    super();
-
-    this._consoleListeners = [];
-    this._encounteredCrashDumpFiles = [];
-    this._unexpectedCrashDumpFiles = {};
-    this._crashDumpDir = null;
-    this._mfl = null;
-    this._applyingPermissions = false;
-    this._observingPermissions = false;
-    this._asyncObservers = new WeakMap();
-    this._xpcomabi = null;
-    this._os = null;
-    this._pu = null;
-
-    this._nextExtensionID = 0;
-    this._extensionListeners = null;
-  }
-
-  // Hack around devtools sometimes trying to JSON stringify us.
-  toJSON() {
-    return {};
-  }
-
-  receiveMessage(message) {
-    switch (message.name) {
-      case "Assert":
-        {
-          // An assertion has been done in a mochitest chrome script
-          let { name, passed, stack, diag } = message.data;
-
-          let SimpleTest =
-            this.contentWindow && this.contentWindow.wrappedJSObject.SimpleTest;
-
-          if (SimpleTest) {
-            SimpleTest.record(
-              passed,
-              name,
-              diag,
-              stack && stack.formattedStack
-            );
-          } else {
-            // Well, this is unexpected.
-            dump(name + "\n");
-          }
-        }
-        break;
-    }
-    return undefined;
-  }
-
-  /*
-   * Privileged object wrapping API
-   *
-   * Usage:
-   *   var wrapper = SpecialPowers.wrap(obj);
-   *   wrapper.privilegedMethod(); wrapper.privilegedProperty;
-   *   obj === SpecialPowers.unwrap(wrapper);
-   *
-   * These functions provide transparent access to privileged objects using
-   * various pieces of deep SpiderMagic. Conceptually, a wrapper is just an
-   * object containing a reference to the underlying object, where all method
-   * calls and property accesses are transparently performed with the System
-   * Principal. Moreover, objects obtained from the wrapper (including properties
-   * and method return values) are wrapped automatically. Thus, after a single
-   * call to SpecialPowers.wrap(), the wrapper layer is transitively maintained.
-   *
-   * Known Issues:
-   *
-   *  - The wrapping function does not preserve identity, so
-   *    SpecialPowers.wrap(foo) !== SpecialPowers.wrap(foo). See bug 718543.
-   *
-   *  - The wrapper cannot see expando properties on unprivileged DOM objects.
-   *    That is to say, the wrapper uses Xray delegation.
-   *
-   *  - The wrapper sometimes guesses certain ES5 attributes for returned
-   *    properties. This is explained in a comment in the wrapper code above,
-   *    and shouldn't be a problem.
-   */
-  wrap(obj) {
-    return WrapPrivileged.wrap(obj);
-  }
-  unwrap(obj) {
-    return WrapPrivileged.unwrap(obj);
-  }
-  isWrapper(val) {
-    return WrapPrivileged.isWrapper(val);
-  }
-
-  /*
-   * When content needs to pass a callback or a callback object to an API
-   * accessed over SpecialPowers, that API may sometimes receive arguments for
-   * whom it is forbidden to create a wrapper in content scopes. As such, we
-   * need a layer to wrap the values in SpecialPowers wrappers before they ever
-   * reach content.
-   */
-  wrapCallback(func) {
-    return WrapPrivileged.wrapCallback(func);
-  }
-  wrapCallbackObject(obj) {
-    return WrapPrivileged.wrapCallbackObject(obj);
-  }
-
-  /*
-   * Used for assigning a property to a SpecialPowers wrapper, without unwrapping
-   * the value that is assigned.
-   */
-  setWrapped(obj, prop, val) {
-    if (!WrapPrivileged.isWrapper(obj)) {
-      throw new Error(
-        "You only need to use this for SpecialPowers wrapped objects"
-      );
-    }
-
-    obj = WrapPrivileged.unwrap(obj);
-    return Reflect.set(obj, prop, val);
-  }
-
-  /*
-   * Create blank privileged objects to use as out-params for privileged functions.
-   */
-  createBlankObject() {
-    return {};
-  }
-
-  /*
-   * Because SpecialPowers wrappers don't preserve identity, comparing with ==
-   * can be hazardous. Sometimes we can just unwrap to compare, but sometimes
-   * wrapping the underlying object into a content scope is forbidden. This
-   * function strips any wrappers if they exist and compare the underlying
-   * values.
-   */
-  compare(a, b) {
-    return WrapPrivileged.unwrap(a) === WrapPrivileged.unwrap(b);
-  }
-
-  get MockFilePicker() {
-    return MockFilePicker;
-  }
-
-  get MockColorPicker() {
-    return MockColorPicker;
-  }
-
-  get MockPermissionPrompt() {
-    return MockPermissionPrompt;
-  }
-
-  /*
-   * Load a privileged script that runs same-process. This is different from
-   * |loadChromeScript|, which will run in the parent process in e10s mode.
-   */
-  loadPrivilegedScript(aFunction) {
-    var str = "(" + aFunction.toString() + ")();";
-    let gGlobalObject = Cu.getGlobalForObject(this);
-    let sb = Cu.Sandbox(gGlobalObject);
-    var window = this.contentWindow;
-    var mc = new window.MessageChannel();
-    sb.port = mc.port1;
-    try {
-      let blob = new Blob([str], { type: "application/javascript" });
-      let blobUrl = URL.createObjectURL(blob);
-      Services.scriptloader.loadSubScript(blobUrl, sb);
-    } catch (e) {
-      throw WrapPrivileged.wrap(e);
-    }
-
-    return mc.port2;
-  }
-
-  _readUrlAsString(aUrl) {
-    // Fetch script content as we can't use scriptloader's loadSubScript
-    // to evaluate http:// urls...
-    var scriptableStream = Cc[
-      "@mozilla.org/scriptableinputstream;1"
-    ].getService(Ci.nsIScriptableInputStream);
-
-    var channel = NetUtil.newChannel({
-      uri: aUrl,
-      loadUsingSystemPrincipal: true,
-    });
-    var input = channel.open();
-    scriptableStream.init(input);
-
-    var str;
-    var buffer = [];
-
-    while ((str = scriptableStream.read(4096))) {
-      buffer.push(str);
-    }
-
-    var output = buffer.join("");
-
-    scriptableStream.close();
-    input.close();
-
-    var status;
-    if (channel instanceof Ci.nsIHttpChannel) {
-      status = channel.responseStatus;
-    }
-
-    if (status == 404) {
-      throw new Error(
-        `Error while executing chrome script '${aUrl}':\n` +
-          "The script doesn't exist. Ensure you have registered it in " +
-          "'support-files' in your mochitest.ini."
-      );
-    }
-
-    return output;
-  }
-
-  loadChromeScript(urlOrFunction, sandboxOptions) {
-    // Create a unique id for this chrome script
-    let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
-      Ci.nsIUUIDGenerator
-    );
-    let id = uuidGenerator.generateUUID().toString();
-
-    // Tells chrome code to evaluate this chrome script
-    let scriptArgs = { id, sandboxOptions };
-    if (typeof urlOrFunction == "function") {
-      scriptArgs.function = {
-        body: "(" + urlOrFunction.toString() + ")();",
-        name: urlOrFunction.name,
-      };
-    } else {
-      // Note: We need to do this in the child since, even though
-      // `_readUrlAsString` pretends to be synchronous, its channel
-      // winds up spinning the event loop when loading HTTP URLs. That
-      // leads to unexpected out-of-order operations if the child sends
-      // a message immediately after loading the script.
-      scriptArgs.function = {
-        body: this._readUrlAsString(urlOrFunction),
-      };
-      scriptArgs.url = urlOrFunction;
-    }
-    this.sendAsyncMessage("SPLoadChromeScript", scriptArgs);
-
-    // Returns a MessageManager like API in order to be
-    // able to communicate with this chrome script
-    let listeners = [];
-    let chromeScript = {
-      addMessageListener: (name, listener) => {
-        listeners.push({ name, listener });
-      },
-
-      promiseOneMessage: name =>
-        new Promise(resolve => {
-          chromeScript.addMessageListener(name, function listener(message) {
-            chromeScript.removeMessageListener(name, listener);
-            resolve(message);
-          });
-        }),
-
-      removeMessageListener: (name, listener) => {
-        listeners = listeners.filter(
-          o => o.name != name || o.listener != listener
-        );
-      },
-
-      sendAsyncMessage: (name, message) => {
-        this.sendAsyncMessage("SPChromeScriptMessage", { id, name, message });
-      },
-
-      sendQuery: (name, message) => {
-        return this.sendQuery("SPChromeScriptMessage", { id, name, message });
-      },
-
-      destroy: () => {
-        listeners = [];
-        this._removeMessageListener("SPChromeScriptMessage", chromeScript);
-      },
-
-      receiveMessage: aMessage => {
-        let messageId = aMessage.json.id;
-        let name = aMessage.json.name;
-        let message = aMessage.json.message;
-        if (this.contentWindow) {
-          message = new StructuredCloneHolder(message).deserialize(
-            this.contentWindow
-          );
-        }
-        // Ignore message from other chrome script
-        if (messageId != id) {
-          return null;
-        }
-
-        let result;
-        if (aMessage.name == "SPChromeScriptMessage") {
-          for (let listener of listeners.filter(o => o.name == name)) {
-            result = listener.listener(message);
-          }
-        }
-        return result;
-      },
-    };
-    this._addMessageListener("SPChromeScriptMessage", chromeScript);
-
-    return this.wrap(chromeScript);
-  }
-
-  async importInMainProcess(importString) {
-    var message = await this.sendQuery("SPImportInMainProcess", importString);
-    if (message.hadError) {
-      throw new Error(
-        "SpecialPowers.importInMainProcess failed with error " +
-          message.errorMessage
-      );
-    }
-  }
-
-  get Services() {
-    return WrapPrivileged.wrap(Services);
-  }
-
-  /*
-   * A getter for the privileged Components object we have.
-   */
-  getFullComponents() {
-    return Components;
-  }
-
-  /*
-   * Convenient shortcuts to the standard Components abbreviations.
-   */
-  get Cc() {
-    return WrapPrivileged.wrap(this.getFullComponents().classes);
-  }
-  get Ci() {
-    return WrapPrivileged.wrap(this.getFullComponents().interfaces);
-  }
-  get Cu() {
-    return WrapPrivileged.wrap(this.getFullComponents().utils);
-  }
-  get Cr() {
-    return WrapPrivileged.wrap(this.getFullComponents().results);
-  }
-
-  getDOMWindowUtils(aWindow) {
-    if (aWindow == this.contentWindow && this.DOMWindowUtils != null) {
-      return this.DOMWindowUtils;
-    }
-
-    return bindDOMWindowUtils(aWindow);
-  }
-
-  async toggleMuteState(aMuted, aWindow) {
-    let actor = aWindow
-      ? aWindow.getWindowGlobalChild().getActor("SpecialPowers")
-      : this;
-    return actor.sendQuery("SPToggleMuteAudio", { mute: aMuted });
-  }
-
-  /*
-   * A method to get a DOMParser that can't parse XUL.
-   */
-  getNoXULDOMParser() {
-    // If we create it with a system subject principal (so it gets a
-    // nullprincipal), it won't be able to parse XUL by default.
-    return WrapPrivileged.wrap(new DOMParser());
-  }
-
-  get InspectorUtils() {
-    return WrapPrivileged.wrap(InspectorUtils);
-  }
-
-  get PromiseDebugging() {
-    return WrapPrivileged.wrap(PromiseDebugging);
-  }
-
-  async waitForCrashes(aExpectingProcessCrash) {
-    if (!aExpectingProcessCrash) {
-      return;
-    }
-
-    var crashIds = this._encounteredCrashDumpFiles
-      .filter(filename => {
-        return filename.length === 40 && filename.endsWith(".dmp");
-      })
-      .map(id => {
-        return id.slice(0, -4); // Strip the .dmp extension to get the ID
-      });
-
-    await this.sendQuery("SPProcessCrashManagerWait", {
-      crashIds,
-    });
-  }
-
-  async removeExpectedCrashDumpFiles(aExpectingProcessCrash) {
-    var success = true;
-    if (aExpectingProcessCrash) {
-      var message = {
-        op: "delete-crash-dump-files",
-        filenames: this._encounteredCrashDumpFiles,
-      };
-      if (!(await this.sendQuery("SPProcessCrashService", message))) {
-        success = false;
-      }
-    }
-    this._encounteredCrashDumpFiles.length = 0;
-    return success;
-  }
-
-  async findUnexpectedCrashDumpFiles() {
-    var self = this;
-    var message = {
-      op: "find-crash-dump-files",
-      crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles,
-    };
-    var crashDumpFiles = await this.sendQuery("SPProcessCrashService", message);
-    crashDumpFiles.forEach(function(aFilename) {
-      self._unexpectedCrashDumpFiles[aFilename] = true;
-    });
-    return crashDumpFiles;
-  }
-
-  removePendingCrashDumpFiles() {
-    var message = {
-      op: "delete-pending-crash-dump-files",
-    };
-    return this.sendQuery("SPProcessCrashService", message);
-  }
-
-  _setTimeout(callback) {
-    // for mochitest-browser
-    if (typeof this.chromeWindow != "undefined") {
-      this.chromeWindow.setTimeout(callback, 0);
-    }
-    // for mochitest-plain
-    else {
-      this.contentWindow.setTimeout(callback, 0);
-    }
-  }
-
-  promiseTimeout(delay) {
-    return new Promise(resolve => {
-      this._setTimeout(resolve, delay);
-    });
-  }
-
-  _delayCallbackTwice(callback) {
-    let delayedCallback = () => {
-      let delayAgain = aCallback => {
-        // Using this._setTimeout doesn't work here
-        // It causes failures in mochtests that use
-        // multiple pushPrefEnv calls
-        // For chrome/browser-chrome mochitests
-        this._setTimeout(aCallback);
-      };
-      delayAgain(delayAgain.bind(this, callback));
-    };
-    return delayedCallback;
-  }
-
-  /* apply permissions to the system and when the test case is finished (SimpleTest.finish())
-     we will revert the permission back to the original.
-
-     inPermissions is an array of objects where each object has a type, action, context, ex:
-     [{'type': 'SystemXHR', 'allow': 1, 'context': document},
-      {'type': 'SystemXHR', 'allow': Ci.nsIPermissionManager.PROMPT_ACTION, 'context': document}]
-
-     Allow can be a boolean value of true/false or ALLOW_ACTION/DENY_ACTION/PROMPT_ACTION/UNKNOWN_ACTION
-  */
-  async pushPermissions(inPermissions, callback) {
-    inPermissions = Cu.waiveXrays(inPermissions);
-    var pendingPermissions = [];
-    var cleanupPermissions = [];
-
-    for (var p in inPermissions) {
-      var permission = inPermissions[p];
-      var originalValue = Ci.nsIPermissionManager.UNKNOWN_ACTION;
-      var context = Cu.unwaiveXrays(permission.context); // Sometimes |context| is a DOM object on which we expect
-      // to be able to access .nodePrincipal, so we need to unwaive.
-      if (
-        await this.testPermission(
-          permission.type,
-          Ci.nsIPermissionManager.ALLOW_ACTION,
-          context
-        )
-      ) {
-        originalValue = Ci.nsIPermissionManager.ALLOW_ACTION;
-      } else if (
-        await this.testPermission(
-          permission.type,
-          Ci.nsIPermissionManager.DENY_ACTION,
-          context
-        )
-      ) {
-        originalValue = Ci.nsIPermissionManager.DENY_ACTION;
-      } else if (
-        await this.testPermission(
-          permission.type,
-          Ci.nsIPermissionManager.PROMPT_ACTION,
-          context
-        )
-      ) {
-        originalValue = Ci.nsIPermissionManager.PROMPT_ACTION;
-      } else if (
-        await this.testPermission(
-          permission.type,
-          Ci.nsICookiePermission.ACCESS_SESSION,
-          context
-        )
-      ) {
-        originalValue = Ci.nsICookiePermission.ACCESS_SESSION;
-      }
-
-      let principal = this._getPrincipalFromArg(context);
-      if (principal.isSystemPrincipal) {
-        continue;
-      }
-
-      let perm;
-      if (typeof permission.allow !== "boolean") {
-        perm = permission.allow;
-      } else {
-        perm = permission.allow
-          ? Ci.nsIPermissionManager.ALLOW_ACTION
-          : Ci.nsIPermissionManager.DENY_ACTION;
-      }
-
-      if (permission.remove) {
-        perm = Ci.nsIPermissionManager.UNKNOWN_ACTION;
-      }
-
-      if (originalValue == perm) {
-        continue;
-      }
-
-      var todo = {
-        op: "add",
-        type: permission.type,
-        permission: perm,
-        value: perm,
-        principal,
-        expireType:
-          typeof permission.expireType === "number" ? permission.expireType : 0, // default: EXPIRE_NEVER
-        expireTime:
-          typeof permission.expireTime === "number" ? permission.expireTime : 0,
-      };
-
-      var cleanupTodo = Object.assign({}, todo);
-
-      if (permission.remove) {
-        todo.op = "remove";
-      }
-
-      pendingPermissions.push(todo);
-
-      /* Push original permissions value or clear into cleanup array */
-      if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
-        cleanupTodo.op = "remove";
-      } else {
-        cleanupTodo.value = originalValue;
-        cleanupTodo.permission = originalValue;
-      }
-      cleanupPermissions.push(cleanupTodo);
-    }
-
-    if (pendingPermissions.length > 0) {
-      // The callback needs to be delayed twice. One delay is because the pref
-      // service doesn't guarantee the order it calls its observers in, so it
-      // may notify the observer holding the callback before the other
-      // observers have been notified and given a chance to make the changes
-      // that the callback checks for. The second delay is because pref
-      // observers often defer making their changes by posting an event to the
-      // event loop.
-      if (!this._observingPermissions) {
-        this._observingPermissions = true;
-        // If specialpowers is in main-process, then we can add a observer
-        // to get all 'perm-changed' signals. Otherwise, it can't receive
-        // all signals, so we register a observer in specialpowersobserver(in
-        // main-process) and get signals from it.
-        if (this.isMainProcess()) {
-          this.permissionObserverProxy._specialPowersAPI = this;
-          Services.obs.addObserver(
-            this.permissionObserverProxy,
-            "perm-changed"
-          );
-        } else {
-          this.registerObservers("perm-changed");
-          // bind() is used to set 'this' to SpecialPowersAPI itself.
-          this._addMessageListener(
-            "specialpowers-perm-changed",
-            this.permChangedProxy.bind(this)
-          );
-        }
-      }
-      this._permissionsUndoStack.push(cleanupPermissions);
-      this._pendingPermissions.push([
-        pendingPermissions,
-        this._delayCallbackTwice(callback),
-      ]);
-      this._applyPermissions();
-    } else {
-      this._setTimeout(callback);
-    }
-  }
-
-  /*
-   * This function should be used when specialpowers is in content process but
-   * it want to get the notification from chrome space.
-   *
-   * This function will call Services.obs.addObserver in SpecialPowersObserver
-   * (that is in chrome process) and forward the data received to SpecialPowers
-   * via messageManager.
-   * You can use this._addMessageListener("specialpowers-YOUR_TOPIC") to fire
-   * the callback.
-   *
-   * To get the expected data, you should modify
-   * SpecialPowersObserver.prototype._registerObservers.observe. Or the message
-   * you received from messageManager will only contain 'aData' from Service.obs.
-   *
-   * NOTICE: there is no implementation of _addMessageListener in
-   * ChromePowers.js
-   */
-  registerObservers(topic) {
-    var msg = {
-      op: "add",
-      observerTopic: topic,
-    };
-    return this.sendQuery("SPObserverService", msg);
-  }
-
-  permChangedProxy(aMessage) {
-    let permission = aMessage.json.permission;
-    let aData = aMessage.json.aData;
-    this._permissionObserver.observe(permission, aData);
-  }
-
-  popPermissions(callback) {
-    let promise = new Promise(resolve => {
-      if (this._permissionsUndoStack.length > 0) {
-        // See pushPermissions comment regarding delay.
-        let cb = this._delayCallbackTwice(resolve);
-        /* Each pop from the stack will yield an object {op/type/permission/value/url/appid/isInIsolatedMozBrowserElement} or null */
-        this._pendingPermissions.push([this._permissionsUndoStack.pop(), cb]);
-        this._applyPermissions();
-      } else {
-        if (this._observingPermissions) {
-          this._observingPermissions = false;
-          this._removeMessageListener(
-            "specialpowers-perm-changed",
-            this.permChangedProxy.bind(this)
-          );
-        }
-        this._setTimeout(resolve);
-      }
-    });
-    if (callback) {
-      promise.then(callback);
-    }
-    return promise;
-  }
-
-  flushPermissions(callback) {
-    while (this._permissionsUndoStack.length > 1) {
-      this.popPermissions(null);
-    }
-
-    return this.popPermissions(callback);
-  }
-
-  setTestPluginEnabledState(newEnabledState, pluginName) {
-    return this.sendQuery("SPSetTestPluginEnabledState", {
-      newEnabledState,
-      pluginName,
-    });
-  }
-
-  /*
-    Iterate through one atomic set of permissions actions and perform allow/deny as appropriate.
-    All actions performed must modify the relevant permission.
-  */
-  _applyPermissions() {
-    if (this._applyingPermissions || this._pendingPermissions.length <= 0) {
-      return;
-    }
-
-    /* Set lock and get prefs from the _pendingPrefs queue */
-    this._applyingPermissions = true;
-    var transaction = this._pendingPermissions.shift();
-    var pendingActions = transaction[0];
-    var callback = transaction[1];
-    var lastPermission = pendingActions[pendingActions.length - 1];
-
-    var self = this;
-    this._permissionObserver._self = self;
-    this._permissionObserver._lastPermission = lastPermission;
-    this._permissionObserver._callback = callback;
-    this._permissionObserver._nextCallback = function() {
-      self._applyingPermissions = false;
-      // Now apply any permissions that may have been queued while we were applying
-      self._applyPermissions();
-    };
-
-    for (var idx in pendingActions) {
-      var perm = pendingActions[idx];
-      this.sendAsyncMessage("SPPermissionManager", perm);
-    }
-  }
-
-  async pushPrefEnv(inPrefs, callback = null) {
-    await this.sendQuery("PushPrefEnv", inPrefs).then(callback);
-    await this.promiseTimeout(0);
-  }
-
-  async popPrefEnv(callback = null) {
-    await this.sendQuery("PopPrefEnv").then(callback);
-    await this.promiseTimeout(0);
-  }
-
-  async flushPrefEnv(callback = null) {
-    await this.sendQuery("FlushPrefEnv").then(callback);
-    await this.promiseTimeout(0);
-  }
-
-  _addObserverProxy(notification) {
-    if (notification in this._proxiedObservers) {
-      this._addMessageListener(
-        notification,
-        this._proxiedObservers[notification]
-      );
-    }
-  }
-  _removeObserverProxy(notification) {
-    if (notification in this._proxiedObservers) {
-      this._removeMessageListener(
-        notification,
-        this._proxiedObservers[notification]
-      );
-    }
-  }
-
-  addObserver(obs, notification, weak) {
-    // Make sure the parent side exists, or we won't get any notifications.
-    this.sendAsyncMessage("Wakeup");
-
-    this._addObserverProxy(notification);
-    obs = Cu.waiveXrays(obs);
-    if (
-      typeof obs == "object" &&
-      obs.observe.name != "SpecialPowersCallbackWrapper"
-    ) {
-      obs.observe = WrapPrivileged.wrapCallback(obs.observe);
-    }
-    Services.obs.addObserver(obs, notification, weak);
-  }
-  removeObserver(obs, notification) {
-    this._removeObserverProxy(notification);
-    Services.obs.removeObserver(Cu.waiveXrays(obs), notification);
-  }
-  notifyObservers(subject, topic, data) {
-    Services.obs.notifyObservers(subject, topic, data);
-  }
-
-  /**
-   * An async observer is useful if you're listening for a
-   * notification that normally is only used by C++ code or chrome
-   * code (so it runs in the SystemGroup), but we need to know about
-   * it for a test (which runs as web content). If we used
-   * addObserver, we would assert when trying to enter web content
-   * from a runnabled labeled by the SystemGroup. An async observer
-   * avoids this problem.
-   */
-  addAsyncObserver(obs, notification, weak) {
-    obs = Cu.waiveXrays(obs);
-    if (
-      typeof obs == "object" &&
-      obs.observe.name != "SpecialPowersCallbackWrapper"
-    ) {
-      obs.observe = WrapPrivileged.wrapCallback(obs.observe);
-    }
-    let asyncObs = (...args) => {
-      Services.tm.dispatchToMainThread(() => {
-        if (typeof obs == "function") {
-          obs(...args);
-        } else {
-          obs.observe.call(undefined, ...args);
-        }
-      });
-    };
-    this._asyncObservers.set(obs, asyncObs);
-    Services.obs.addObserver(asyncObs, notification, weak);
-  }
-  removeAsyncObserver(obs, notification) {
-    let asyncObs = this._asyncObservers.get(Cu.waiveXrays(obs));
-    Services.obs.removeObserver(asyncObs, notification);
-  }
-
-  can_QI(obj) {
-    return obj.QueryInterface !== undefined;
-  }
-  do_QueryInterface(obj, iface) {
-    return obj.QueryInterface(Ci[iface]);
-  }
-
-  call_Instanceof(obj1, obj2) {
-    obj1 = WrapPrivileged.unwrap(obj1);
-    obj2 = WrapPrivileged.unwrap(obj2);
-    return obj1 instanceof obj2;
-  }
-
-  // Returns a privileged getter from an object. GetOwnPropertyDescriptor does
-  // not work here because xray wrappers don't properly implement it.
-  //
-  // This terribleness is used by dom/base/test/test_object.html because
-  // <object> and <embed> tags will spawn plugins if their prototype is touched,
-  // so we need to get and cache the getter of |hasRunningPlugin| if we want to
-  // call it without paradoxically spawning the plugin.
-  do_lookupGetter(obj, name) {
-    return Object.prototype.__lookupGetter__.call(obj, name);
-  }
-
-  // Mimic the get*Pref API
-  getBoolPref(...args) {
-    return Services.prefs.getBoolPref(...args);
-  }
-  getIntPref(...args) {
-    return Services.prefs.getIntPref(...args);
-  }
-  getCharPref(...args) {
-    return Services.prefs.getCharPref(...args);
-  }
-  getComplexValue(prefName, iid) {
-    return Services.prefs.getComplexValue(prefName, iid);
-  }
-
-  getParentBoolPref(prefName, defaultValue) {
-    return this._getParentPref(prefName, "BOOL", { defaultValue });
-  }
-  getParentIntPref(prefName, defaultValue) {
-    return this._getParentPref(prefName, "INT", { defaultValue });
-  }
-  getParentCharPref(prefName, defaultValue) {
-    return this._getParentPref(prefName, "CHAR", { defaultValue });
-  }
-
-  // Mimic the set*Pref API
-  setBoolPref(prefName, value) {
-    return this._setPref(prefName, "BOOL", value);
-  }
-  setIntPref(prefName, value) {
-    return this._setPref(prefName, "INT", value);
-  }
-  setCharPref(prefName, value) {
-    return this._setPref(prefName, "CHAR", value);
-  }
-  setComplexValue(prefName, iid, value) {
-    return this._setPref(prefName, "COMPLEX", value, iid);
-  }
-
-  // Mimic the clearUserPref API
-  clearUserPref(prefName) {
-    let msg = {
-      op: "clear",
-      prefName,
-      prefType: "",
-    };
-    return this.sendQuery("SPPrefService", msg);
-  }
-
-  // Private pref functions to communicate to chrome
-  async _getParentPref(prefName, prefType, { defaultValue, iid }) {
-    let msg = {
-      op: "get",
-      prefName,
-      prefType,
-      iid, // Only used with complex prefs
-      defaultValue, // Optional default value
-    };
-    let val = await this.sendQuery("SPPrefService", msg);
-    if (val == null) {
-      throw new Error(`Error getting pref '${prefName}'`);
-    }
-    return val;
-  }
-  _getPref(prefName, prefType, { defaultValue }) {
-    switch (prefType) {
-      case "BOOL":
-        return Services.prefs.getBoolPref(prefName);
-      case "INT":
-        return Services.prefs.getIntPref(prefName);
-      case "CHAR":
-        return Services.prefs.getCharPref(prefName);
-    }
-    return undefined;
-  }
-  _setPref(prefName, prefType, prefValue, iid) {
-    let msg = {
-      op: "set",
-      prefName,
-      prefType,
-      iid, // Only used with complex prefs
-      prefValue,
-    };
-    return this.sendQuery("SPPrefService", msg);
-  }
-
-  _getMUDV(window) {
-    return window.docShell.contentViewer;
-  }
-  // XXX: these APIs really ought to be removed, they're not e10s-safe.
-  // (also they're pretty Firefox-specific)
-  _getTopChromeWindow(window) {
-    return window.docShell.rootTreeItem.domWindow;
-  }
-  _getAutoCompletePopup(window) {
-    return this._getTopChromeWindow(window).document.getElementById(
-      "PopupAutoComplete"
-    );
-  }
-  addAutoCompletePopupEventListener(window, eventname, listener) {
-    this._getAutoCompletePopup(window).addEventListener(eventname, listener);
-  }
-  removeAutoCompletePopupEventListener(window, eventname, listener) {
-    this._getAutoCompletePopup(window).removeEventListener(eventname, listener);
-  }
-  get formHistory() {
-    let tmp = {};
-    ChromeUtils.import("resource://gre/modules/FormHistory.jsm", tmp);
-    return WrapPrivileged.wrap(tmp.FormHistory);
-  }
-  getFormFillController(window) {
-    return Cc["@mozilla.org/satchel/form-fill-controller;1"].getService(
-      Ci.nsIFormFillController
-    );
-  }
-  attachFormFillControllerTo(window) {
-    this.getFormFillController().attachPopupElementToBrowser(
-      window.docShell,
-      this._getAutoCompletePopup(window)
-    );
-  }
-  detachFormFillControllerFrom(window) {
-    this.getFormFillController().detachFromBrowser(window.docShell);
-  }
-  isBackButtonEnabled(window) {
-    return !this._getTopChromeWindow(window)
-      .document.getElementById("Browser:Back")
-      .hasAttribute("disabled");
-  }
-  // XXX end of problematic APIs
-
-  addChromeEventListener(type, listener, capture, allowUntrusted) {
-    this.docShell.chromeEventHandler.addEventListener(
-      type,
-      listener,
-      capture,
-      allowUntrusted
-    );
-  }
-  removeChromeEventListener(type, listener, capture) {
-    this.docShell.chromeEventHandler.removeEventListener(
-      type,
-      listener,
-      capture
-    );
-  }
-
-  // Note: each call to registerConsoleListener MUST be paired with a
-  // call to postConsoleSentinel; when the callback receives the
-  // sentinel it will unregister itself (_after_ calling the
-  // callback).  SimpleTest.expectConsoleMessages does this for you.
-  // If you register more than one console listener, a call to
-  // postConsoleSentinel will zap all of them.
-  registerConsoleListener(callback) {
-    let listener = new SPConsoleListener(callback);
-    Services.console.registerListener(listener);
-
-    // listen for dom/console events as well
-    Services.obs.addObserver(listener, "console-api-log-event");
-  }
-  postConsoleSentinel() {
-    Services.console.logStringMessage("SENTINEL");
-  }
-  resetConsole() {
-    Services.console.reset();
-  }
-
-  getFullZoom(window) {
-    return this._getMUDV(window).fullZoom;
-  }
-  getDeviceFullZoom(window) {
-    return this._getMUDV(window).deviceFullZoom;
-  }
-  setFullZoom(window, zoom) {
-    this._getMUDV(window).fullZoom = zoom;
-  }
-  getTextZoom(window) {
-    return this._getMUDV(window).textZoom;
-  }
-  setTextZoom(window, zoom) {
-    this._getMUDV(window).textZoom = zoom;
-  }
-
-  getOverrideDPPX(window) {
-    return this._getMUDV(window).overrideDPPX;
-  }
-  setOverrideDPPX(window, dppx) {
-    this._getMUDV(window).overrideDPPX = dppx;
-  }
-
-  emulateMedium(window, mediaType) {
-    this._getMUDV(window).emulateMedium(mediaType);
-  }
-  stopEmulatingMedium(window) {
-    this._getMUDV(window).stopEmulatingMedium();
-  }
-
-  // Takes a snapshot of the given window and returns a <canvas>
-  // containing the image. When the window is same-process, the canvas
-  // is returned synchronously. When it is out-of-process (or when a
-  // BrowsingContext or FrameLoaderOwner is passed instead of a Window),
-  // a promise which resolves to such a canvas is returned instead.
-  snapshotWindowWithOptions(content, rect, bgcolor, options) {
-    function getImageData(rect, bgcolor, options) {
-      let el = content.document.createElementNS(
-        "http://www.w3.org/1999/xhtml",
-        "canvas"
-      );
-      if (rect === undefined) {
-        rect = {
-          top: content.scrollY,
-          left: content.scrollX,
-          width: content.innerWidth,
-          height: content.innerHeight,
-        };
-      }
-      if (bgcolor === undefined) {
-        bgcolor = "rgb(255,255,255)";
-      }
-      if (options === undefined) {
-        options = {};
-      }
-
-      el.width = rect.width;
-      el.height = rect.height;
-      let ctx = el.getContext("2d");
-
-      let flags = 0;
-      for (let option in options) {
-        flags |= options[option] && ctx[option];
-      }
-
-      ctx.drawWindow(
-        content,
-        rect.left,
-        rect.top,
-        rect.width,
-        rect.height,
-        bgcolor,
-        flags
-      );
-
-      return ctx.getImageData(0, 0, el.width, el.height);
-    }
-
-    let toCanvas = imageData => {
-      let el = this.document.createElementNS(
-        "http://www.w3.org/1999/xhtml",
-        "canvas"
-      );
-      el.width = imageData.width;
-      el.height = imageData.height;
-
-      if (ImageData.isInstance(imageData)) {
-        let ctx = el.getContext("2d");
-        ctx.putImageData(imageData, 0, 0);
-      }
-
-      return el;
-    };
-
-    if (Window.isInstance(content)) {
-      // Hack around tests that try to snapshot 0 width or height
-      // elements.
-      if (rect && !(rect.width && rect.height)) {
-        return toCanvas(rect);
-      }
-
-      // This is an in-process window. Snapshot it synchronously.
-      return toCanvas(getImageData(rect, bgcolor, options));
-    }
-
-    // This is a remote window or frame. Snapshot it asynchronously and
-    // return a promise for the result. Alas, consumers expect us to
-    // return a <canvas> element rather than an ImageData object, so we
-    // need to convert the result from the remote snapshot to a local
-    // canvas.
-    return this.spawn(content, [rect, bgcolor, options], getImageData).then(
-      toCanvas
-    );
-  }
-
-  snapshotWindow(win, withCaret, rect, bgcolor) {
-    return this.snapshotWindowWithOptions(win, rect, bgcolor, {
-      DRAWWINDOW_DRAW_CARET: withCaret,
-    });
-  }
-
-  snapshotRect(win, rect, bgcolor) {
-    return this.snapshotWindowWithOptions(win, rect, bgcolor);
-  }
-
-  gc() {
-    this.DOMWindowUtils.garbageCollect();
-  }
-
-  forceGC() {
-    Cu.forceGC();
-  }
-
-  forceShrinkingGC() {
-    Cu.forceShrinkingGC();
-  }
-
-  forceCC() {
-    Cu.forceCC();
-  }
-
-  finishCC() {
-    Cu.finishCC();
-  }
-
-  ccSlice(budget) {
-    Cu.ccSlice(budget);
-  }
-
-  // Due to various dependencies between JS objects and C++ objects, an ordinary
-  // forceGC doesn't necessarily clear all unused objects, thus the GC and CC
-  // needs to run several times and when no other JS is running.
-  // The current number of iterations has been determined according to massive
-  // cross platform testing.
-  exactGC(callback) {
-    let count = 0;
-
-    function genGCCallback(cb) {
-      return function() {
-        Cu.forceCC();
-        if (++count < 3) {
-          Cu.schedulePreciseGC(genGCCallback(cb));
-        } else if (cb) {
-          cb();
-        }
-      };
-    }
-
-    Cu.schedulePreciseGC(genGCCallback(callback));
-  }
-
-  nondeterministicGetWeakMapKeys(m) {
-    return ChromeUtils.nondeterministicGetWeakMapKeys(m);
-  }
-
-  getMemoryReports() {
-    try {
-      Cc["@mozilla.org/memory-reporter-manager;1"]
-        .getService(Ci.nsIMemoryReporterManager)
-        .getReports(() => {}, null, () => {}, null, false);
-    } catch (e) {}
-  }
-
-  setGCZeal(zeal) {
-    Cu.setGCZeal(zeal);
-  }
-
-  isMainProcess() {
-    try {
-      return (
-        Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
-      );
-    } catch (e) {}
-    return true;
-  }
-
-  get XPCOMABI() {
-    if (this._xpcomabi != null) {
-      return this._xpcomabi;
-    }
-
-    var xulRuntime = Services.appinfo.QueryInterface(Ci.nsIXULRuntime);
-
-    this._xpcomabi = xulRuntime.XPCOMABI;
-    return this._xpcomabi;
-  }
-
-  // The optional aWin parameter allows the caller to specify a given window in
-  // whose scope the runnable should be dispatched. If aFun throws, the
-  // exception will be reported to aWin.
-  executeSoon(aFun, aWin) {
-    // Create the runnable in the scope of aWin to avoid running into COWs.
-    var runnable = {};
-    if (aWin) {
-      runnable = Cu.createObjectIn(aWin);
-    }
-    runnable.run = aFun;
-    Cu.dispatch(runnable, aWin);
-  }
-
-  get OS() {
-    if (this._os != null) {
-      return this._os;
-    }
-
-    this._os = Services.appinfo.OS;
-    return this._os;
-  }
-
-  get useRemoteSubframes() {
-    return this.docShell.nsILoadContext.useRemoteSubframes;
-  }
-
-  addSystemEventListener(target, type, listener, useCapture) {
-    Services.els.addSystemEventListener(target, type, listener, useCapture);
-  }
-  removeSystemEventListener(target, type, listener, useCapture) {
-    Services.els.removeSystemEventListener(target, type, listener, useCapture);
-  }
-
-  // helper method to check if the event is consumed by either default group's
-  // event listener or system group's event listener.
-  defaultPreventedInAnyGroup(event) {
-    // FYI: Event.defaultPrevented returns false in content context if the
-    //      event is consumed only by system group's event listeners.
-    return event.defaultPrevented;
-  }
-
-  getDOMRequestService() {
-    var serv = Services.DOMRequest;
-    var res = {};
-    var props = [
-      "createRequest",
-      "createCursor",
-      "fireError",
-      "fireSuccess",
-      "fireDone",
-      "fireDetailedError",
-    ];
-    for (var i in props) {
-      let prop = props[i];
-      res[prop] = function() {
-        return serv[prop].apply(serv, arguments);
-      };
-    }
-    return res;
-  }
-
-  addCategoryEntry(category, entry, value, persists, replace) {
-    Services.catMan.addCategoryEntry(category, entry, value, persists, replace);
-  }
-
-  deleteCategoryEntry(category, entry, persists) {
-    Services.catMan.deleteCategoryEntry(category, entry, persists);
-  }
-  openDialog(win, args) {
-    return win.openDialog.apply(win, args);
-  }
-  // This is a blocking call which creates and spins a native event loop
-  spinEventLoop(win) {
-    // simply do a sync XHR back to our windows location.
-    var syncXHR = new win.XMLHttpRequest();
-    syncXHR.open("GET", win.location, false);
-    syncXHR.send();
-  }
-
-  // :jdm gets credit for this.  ex: getPrivilegedProps(window, 'location.href');
-  getPrivilegedProps(obj, props) {
-    var parts = props.split(".");
-    for (var i = 0; i < parts.length; i++) {
-      var p = parts[i];
-      if (obj[p] != undefined) {
-        obj = obj[p];
-      } else {
-        return null;
-      }
-    }
-    return obj;
-  }
-
-  _browsingContextForTarget(target) {
-    if (BrowsingContext.isInstance(target)) {
-      return target;
-    }
-    if (Element.isInstance(target)) {
-      return target.browsingContext;
-    }
-
-    return BrowsingContext.getFromWindow(target);
-  }
-
-  /**
-   * Runs a task in the context of the given frame, and returns a
-   * promise which resolves to the return value of that task.
-   *
-   * The given frame may be in-process or out-of-process. Either way,
-   * the task will run asynchronously, in a sandbox with access to the
-   * frame's content window via its `content` global. Any arguments
-   * passed will be copied via structured clone, as will its return
-   * value.
-   *
-   * The sandbox also has access to an Assert object, as provided by
-   * Assert.jsm. Any assertion methods called before the task resolves
-   * will be relayed back to the test environment of the caller.
-   *
-   * @param {BrowsingContext or FrameLoaderOwner or WindowProxy} target
-   *        The target in which to run the task. This may be any element
-   *        which implements the FrameLoaderOwner interface (including
-   *        HTML <iframe> elements and XUL <browser> elements) or a
-   *        WindowProxy (either in-process or remote).
-   * @param {Array<any>} args
-   *        An array of arguments to pass to the task. All arguments
-   *        must be structured clone compatible, and will be cloned
-   *        before being passed to the task.
-   * @param {function} task
-   *        The function to run in the context of the target. The
-   *        function will be stringified and re-evaluated in the context
-   *        of the target's content window. It may return any structured
-   *        clone compatible value, or a Promise which resolves to the
-   *        same, which will be returned to the caller.
-   *
-   * @returns {Promise<any>}
-   *        A promise which resolves to the return value of the task, or
-   *        which rejects if the task raises an exception. As this is
-   *        being written, the rejection value will always be undefined
-   *        in the cases where the task throws an error, though that may
-   *        change in the future.
-   */
-  spawn(target, args, task) {
-    let browsingContext = this._browsingContextForTarget(target);
-
-    return this.sendQuery("Spawn", {
-      browsingContext,
-      args,
-      task: String(task),
-      caller: SpecialPowersSandbox.getCallerInfo(Components.stack.caller),
-    });
-  }
-
-  snapshotContext(target, rect, background) {
-    let browsingContext = this._browsingContextForTarget(target);
-
-    return this.sendQuery("Snapshot", {
-      browsingContext,
-      rect,
-      background,
-    }).then(imageData => {
-      return this.contentWindow.createImageBitmap(imageData);
-    });
-  }
-
-  _spawnTask(task, args, caller, taskId) {
-    let sb = new SpecialPowersSandbox(null, data => {
-      this.sendAsyncMessage("ProxiedAssert", { taskId, data });
-    });
-
-    sb.sandbox.SpecialPowers = this;
-    Object.defineProperty(sb.sandbox, "content", {
-      get: () => {
-        return this.contentWindow;
-      },
-      enumerable: true,
-    });
-
-    return sb.execute(task, args, caller);
-  }
-
-  getFocusedElementForWindow(targetWindow, aDeep) {
-    var outParam = {};
-    Services.focus.getFocusedElementForWindow(targetWindow, aDeep, outParam);
-    return outParam.value;
-  }
-
-  get focusManager() {
-    return Services.focus;
-  }
-
-  activeWindow() {
-    return Services.focus.activeWindow;
-  }
-
-  focusedWindow() {
-    return Services.focus.focusedWindow;
-  }
-
-  focus(aWindow) {
-    // This is called inside TestRunner._makeIframe without aWindow, because of assertions in oop mochitests
-    // With aWindow, it is called in SimpleTest.waitForFocus to allow popup window opener focus switching
-    if (aWindow) {
-      aWindow.focus();
-    }
-
-    try {
-      let actor = aWindow
-        ? aWindow.getWindowGlobalChild().getActor("SpecialPowers")
-        : this;
-      actor.sendAsyncMessage("SpecialPowers.Focus", {});
-    } catch (e) {
-      Cu.reportError(e);
-    }
-  }
-
-  getClipboardData(flavor, whichClipboard) {
-    if (whichClipboard === undefined) {
-      whichClipboard = Services.clipboard.kGlobalClipboard;
-    }
-
-    var xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
-      Ci.nsITransferable
-    );
-    xferable.init(this.docShell);
-    xferable.addDataFlavor(flavor);
-    Services.clipboard.getData(xferable, whichClipboard);
-    var data = {};
-    try {
-      xferable.getTransferData(flavor, data);
-    } catch (e) {}
-    data = data.value || null;
-    if (data == null) {
-      return "";
-    }
-
-    return data.QueryInterface(Ci.nsISupportsString).data;
-  }
-
-  clipboardCopyString(str) {
-    Cc["@mozilla.org/widget/clipboardhelper;1"]
-      .getService(Ci.nsIClipboardHelper)
-      .copyString(str);
-  }
-
-  supportsSelectionClipboard() {
-    return Services.clipboard.supportsSelectionClipboard();
-  }
-
-  swapFactoryRegistration(cid, contractID, newFactory) {
-    newFactory = Cu.waiveXrays(newFactory);
-
-    var componentRegistrar = Components.manager.QueryInterface(
-      Ci.nsIComponentRegistrar
-    );
-
-    var currentCID = componentRegistrar.contractIDToCID(contractID);
-    var currentFactory = Components.manager.getClassObject(
-      Cc[contractID],
-      Ci.nsIFactory
-    );
-    if (cid) {
-      componentRegistrar.unregisterFactory(currentCID, currentFactory);
-    } else {
-      let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
-        Ci.nsIUUIDGenerator
-      );
-      cid = uuidGenerator.generateUUID();
-    }
-
-    // Restore the original factory.
-    componentRegistrar.registerFactory(cid, "", contractID, newFactory);
-    return { originalCID: currentCID };
-  }
-
-  _getElement(aWindow, id) {
-    return typeof id == "string" ? aWindow.document.getElementById(id) : id;
-  }
-
-  dispatchEvent(aWindow, target, event) {
-    var el = this._getElement(aWindow, target);
-    return el.dispatchEvent(event);
-  }
-
-  get isDebugBuild() {
-    delete SpecialPowersAPI.prototype.isDebugBuild;
-
-    var debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
-    return (SpecialPowersAPI.prototype.isDebugBuild = debug.isDebugBuild);
-  }
-  assertionCount() {
-    var debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
-    return debugsvc.assertionCount;
-  }
-
-  /**
-   * Get the message manager associated with an <iframe mozbrowser>.
-   */
-  getBrowserFrameMessageManager(aFrameElement) {
-    return this.wrap(aFrameElement.frameLoader.messageManager);
-  }
-
-  _getPrincipalFromArg(arg) {
-    let principal;
-    let secMan = Services.scriptSecurityManager;
-
-    if (typeof arg == "string") {
-      // It's an URL.
-      let uri = Services.io.newURI(arg);
-      principal = secMan.createContentPrincipal(uri, {});
-    } else if (arg.nodePrincipal) {
-      // It's a document.
-      // In some tests the arg is a wrapped DOM element, so we unwrap it first.
-      principal = WrapPrivileged.unwrap(arg).nodePrincipal;
-    } else {
-      let uri = Services.io.newURI(arg.url);
-      let attrs = arg.originAttributes || {};
-      principal = secMan.createContentPrincipal(uri, attrs);
-    }
-
-    return principal;
-  }
-
-  async addPermission(type, allow, arg, expireType, expireTime) {
-    let principal = this._getPrincipalFromArg(arg);
-    if (principal.isSystemPrincipal) {
-      return; // nothing to do
-    }
-
-    let permission;
-    if (typeof allow !== "boolean") {
-      permission = allow;
-    } else {
-      permission = allow
-        ? Ci.nsIPermissionManager.ALLOW_ACTION
-        : Ci.nsIPermissionManager.DENY_ACTION;
-    }
-
-    var msg = {
-      op: "add",
-      type,
-      permission,
-      principal,
-      expireType: typeof expireType === "number" ? expireType : 0,
-      expireTime: typeof expireTime === "number" ? expireTime : 0,
-    };
-
-    await this.sendQuery("SPPermissionManager", msg);
-  }
-
-  async removePermission(type, arg) {
-    let principal = this._getPrincipalFromArg(arg);
-    if (principal.isSystemPrincipal) {
-      return; // nothing to do
-    }
-
-    var msg = {
-      op: "remove",
-      type,
-      principal,
-    };
-
-    await this.sendQuery("SPPermissionManager", msg);
-  }
-
-  async hasPermission(type, arg) {
-    let principal = this._getPrincipalFromArg(arg);
-    if (principal.isSystemPrincipal) {
-      return true; // system principals have all permissions
-    }
-
-    var msg = {
-      op: "has",
-      type,
-      principal,
-    };
-
-    return this.sendQuery("SPPermissionManager", msg);
-  }
-
-  async testPermission(type, value, arg) {
-    let principal = this._getPrincipalFromArg(arg);
-    if (principal.isSystemPrincipal) {
-      return true; // system principals have all permissions
-    }
-
-    var msg = {
-      op: "test",
-      type,
-      value,
-      principal,
-    };
-    return this.sendQuery("SPPermissionManager", msg);
-  }
-
-  isContentWindowPrivate(win) {
-    return PrivateBrowsingUtils.isContentWindowPrivate(win);
-  }
-
-  async notifyObserversInParentProcess(subject, topic, data) {
-    if (subject) {
-      throw new Error("Can't send subject to another process!");
-    }
-    if (this.isMainProcess()) {
-      this.notifyObservers(subject, topic, data);
-      return;
-    }
-    var msg = {
-      op: "notify",
-      observerTopic: topic,
-      observerData: data,
-    };
-    await this.sendQuery("SPObserverService", msg);
-  }
-
-  removeAllServiceWorkerData() {
-    return this.sendQuery("SPRemoveAllServiceWorkers", {});
-  }
-
-  removeServiceWorkerDataForExampleDomain() {
-    return this.sendQuery("SPRemoveServiceWorkerDataForExampleDomain", {});
-  }
-
-  cleanUpSTSData(origin, flags) {
-    return this.sendQuery("SPCleanUpSTSData", { origin, flags: flags || 0 });
-  }
-
-  async requestDumpCoverageCounters(cb) {
-    // We want to avoid a roundtrip between child and parent.
-    if (!PerTestCoverageUtils.enabled) {
-      return;
-    }
-
-    await this.sendQuery("SPRequestDumpCoverageCounters", {});
-  }
-
-  async requestResetCoverageCounters(cb) {
-    // We want to avoid a roundtrip between child and parent.
-    if (!PerTestCoverageUtils.enabled) {
-      return;
-    }
-    await this.sendQuery("SPRequestResetCoverageCounters", {});
-  }
-
-  loadExtension(ext, handler) {
-    if (this._extensionListeners == null) {
-      this._extensionListeners = new Set();
-
-      this._addMessageListener("SPExtensionMessage", msg => {
-        for (let listener of this._extensionListeners) {
-          try {
-            listener(msg);
-          } catch (e) {
-            Cu.reportError(e);
-          }
-        }
-      });
-    }
-
-    // Note, this is not the addon is as used by the AddonManager etc,
-    // this is just an identifier used for specialpowers messaging
-    // between this content process and the chrome process.
-    let id = this._nextExtensionID++;
-
-    handler = Cu.waiveXrays(handler);
-    ext = Cu.waiveXrays(ext);
-
-    let sp = this;
-    let state = "uninitialized";
-    let extension = {
-      get state() {
-        return state;
-      },
-
-      startup() {
-        state = "pending";
-        return sp.sendQuery("SPStartupExtension", { id }).then(
-          () => {
-            state = "running";
-          },
-          () => {
-            state = "failed";
-            sp._extensionListeners.delete(listener);
-            return Promise.reject("startup failed");
-          }
-        );
-      },
-
-      unload() {
-        state = "unloading";
-        return sp.sendQuery("SPUnloadExtension", { id }).finally(() => {
-          sp._extensionListeners.delete(listener);
-          state = "unloaded";
-        });
-      },
-
-      sendMessage(...args) {
-        sp.sendAsyncMessage("SPExtensionMessage", { id, args });
-      },
-    };
-
-    this.sendAsyncMessage("SPLoadExtension", { ext, id });
-
-    let listener = msg => {
-      if (msg.data.id == id) {
-        if (msg.data.type == "extensionSetId") {
-          extension.id = msg.data.args[0];
-          extension.uuid = msg.data.args[1];
-        } else if (msg.data.type in handler) {
-          handler[msg.data.type](
-            ...Cu.cloneInto(msg.data.args, this.contentWindow)
-          );
-        } else {
-          dump(`Unexpected: ${msg.data.type}\n`);
-        }
-      }
-    };
-
-    this._extensionListeners.add(listener);
-    return extension;
-  }
-
-  invalidateExtensionStorageCache() {
-    this.notifyObserversInParentProcess(
-      null,
-      "extension-invalidate-storage-cache",
-      ""
-    );
-  }
-
-  allowMedia(window, enable) {
-    window.docShell.allowMedia = enable;
-  }
-
-  createChromeCache(name, url) {
-    let principal = this._getPrincipalFromArg(url);
-    return WrapPrivileged.wrap(
-      new this.contentWindow.CacheStorage(name, principal)
-    );
-  }
-
-  loadChannelAndReturnStatus(url, loadUsingSystemPrincipal) {
-    const BinaryInputStream = Components.Constructor(
-      "@mozilla.org/binaryinputstream;1",
-      "nsIBinaryInputStream",
-      "setInputStream"
-    );
-
-    return new Promise(function(resolve) {
-      let listener = {
-        httpStatus: 0,
-
-        onStartRequest(request) {
-          request.QueryInterface(Ci.nsIHttpChannel);
-          this.httpStatus = request.responseStatus;
-        },
-
-        onDataAvailable(request, stream, offset, count) {
-          new BinaryInputStream(stream).readByteArray(count);
-        },
-
-        onStopRequest(request, status) {
-          /* testing here that the redirect was not followed. If it was followed
-            we would see a http status of 200 and status of NS_OK */
-
-          let httpStatus = this.httpStatus;
-          resolve({ status, httpStatus });
-        },
-      };
-      let uri = NetUtil.newURI(url);
-      let channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal });
-
-      channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
-      channel.QueryInterface(Ci.nsIHttpChannelInternal);
-      channel.documentURI = uri;
-      channel.asyncOpen(listener);
-    });
-  }
-
-  get ParserUtils() {
-    if (this._pu != null) {
-      return this._pu;
-    }
-
-    let pu = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
-    // We need to create and return our own wrapper.
-    this._pu = {
-      sanitize(src, flags) {
-        return pu.sanitize(src, flags);
-      },
-      convertToPlainText(src, flags, wrapCol) {
-        return pu.convertToPlainText(src, flags, wrapCol);
-      },
-      parseFragment(fragment, flags, isXML, baseURL, element) {
-        let baseURI = baseURL ? NetUtil.newURI(baseURL) : null;
-        return pu.parseFragment(
-          WrapPrivileged.unwrap(fragment),
-          flags,
-          isXML,
-          baseURI,
-          WrapPrivileged.unwrap(element)
-        );
-      },
-    };
-    return this._pu;
-  }
-
-  createDOMWalker(node, showAnonymousContent) {
-    node = WrapPrivileged.unwrap(node);
-    let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance(
-      Ci.inIDeepTreeWalker
-    );
-    walker.showAnonymousContent = showAnonymousContent;
-    walker.init(node.ownerDocument, NodeFilter.SHOW_ALL);
-    walker.currentNode = node;
-    return {
-      get firstChild() {
-        return WrapPrivileged.wrap(walker.firstChild());
-      },
-      get lastChild() {
-        return WrapPrivileged.wrap(walker.lastChild());
-      },
-    };
-  }
-
-  observeMutationEvents(mo, node, nativeAnonymousChildList, subtree) {
-    WrapPrivileged.unwrap(mo).observe(WrapPrivileged.unwrap(node), {
-      nativeAnonymousChildList,
-      subtree,
-    });
-  }
-
-  doCommand(window, cmd) {
-    return window.docShell.doCommand(cmd);
-  }
-
-  isCommandEnabled(window, cmd) {
-    return window.docShell.isCommandEnabled(cmd);
-  }
-
-  setCommandNode(window, node) {
-    return window.docShell.contentViewer
-      .QueryInterface(Ci.nsIContentViewerEdit)
-      .setCommandNode(node);
-  }
-
-  /* Bug 1339006 Runnables of nsIURIClassifier.classify may be labeled by
-   * SystemGroup, but some test cases may run as web content. That would assert
-   * when trying to enter web content from a runnable labeled by the
-   * SystemGroup. To avoid that, we run classify from SpecialPowers which is
-   * chrome-privileged and allowed to run inside SystemGroup
-   */
-
-  doUrlClassify(principal, eventTarget, callback) {
-    let classifierService = Cc[
-      "@mozilla.org/url-classifier/dbservice;1"
-    ].getService(Ci.nsIURIClassifier);
-
-    let wrapCallback = (...args) => {
-      Services.tm.dispatchToMainThread(() => {
-        if (typeof callback == "function") {
-          callback(...args);
-        } else {
-          callback.onClassifyComplete.call(undefined, ...args);
-        }
-      });
-    };
-
-    return classifierService.classify(
-      WrapPrivileged.unwrap(principal),
-      eventTarget,
-      wrapCallback
-    );
-  }
-
-  // TODO: Bug 1353701 - Supports custom event target for labelling.
-  doUrlClassifyLocal(uri, tables, callback) {
-    let classifierService = Cc[
-      "@mozilla.org/url-classifier/dbservice;1"
-    ].getService(Ci.nsIURIClassifier);
-
-    let wrapCallback = results => {
-      Services.tm.dispatchToMainThread(() => {
-        if (typeof callback == "function") {
-          callback(WrapPrivileged.wrap(results));
-        } else {
-          callback.onClassifyComplete.call(
-            undefined,
-            WrapPrivileged.wrap(results)
-          );
-        }
-      });
-    };
-
-    let feature = classifierService.createFeatureWithTables(
-      "test",
-      tables.split(","),
-      []
-    );
-    return classifierService.asyncClassifyLocalWithFeatures(
-      WrapPrivileged.unwrap(uri),
-      [feature],
-      Ci.nsIUrlClassifierFeature.blacklist,
-      wrapCallback
-    );
-  }
-}
-
-SpecialPowersAPI.prototype._proxiedObservers = {
-  "specialpowers-http-notify-request": function(aMessage) {
-    let uri = aMessage.json.uri;
-    Services.obs.notifyObservers(
-      null,
-      "specialpowers-http-notify-request",
-      uri
-    );
-  },
-
-  "specialpowers-service-worker-shutdown": function(aMessage) {
-    Services.obs.notifyObservers(null, "specialpowers-service-worker-shutdown");
-  },
-};
-
-SpecialPowersAPI.prototype.permissionObserverProxy = {
-  // 'this' in permChangedObserverProxy is the permChangedObserverProxy
-  // object itself. The '_specialPowersAPI' will be set to the 'SpecialPowersAPI'
-  // object to call the member function in SpecialPowersAPI.
-  _specialPowersAPI: null,
-  observe(aSubject, aTopic, aData) {
-    if (aTopic == "perm-changed") {
-      var permission = aSubject.QueryInterface(Ci.nsIPermission);
-      this._specialPowersAPI._permissionObserver.observe(permission, aData);
-    }
-  },
-};
-
-SpecialPowersAPI.prototype._permissionObserver = {
-  _self: null,
-  _lastPermission: {},
-  _callBack: null,
-  _nextCallback: null,
-  _obsDataMap: {
-    deleted: "remove",
-    added: "add",
-  },
-  observe(permission, aData) {
-    if (this._self._applyingPermissions) {
-      if (permission.type == this._lastPermission.type) {
-        this._self._setTimeout(this._callback);
-        this._self._setTimeout(this._nextCallback);
-        this._callback = null;
-        this._nextCallback = null;
-      }
-    } else {
-      var found = false;
-      for (
-        var i = 0;
-        !found && i < this._self._permissionsUndoStack.length;
-        i++
-      ) {
-        var undos = this._self._permissionsUndoStack[i];
-        for (var j = 0; j < undos.length; j++) {
-          var undo = undos[j];
-          if (
-            undo.op == this._obsDataMap[aData] &&
-            undo.type == permission.type
-          ) {
-            // Remove this undo item if it has been done by others(not
-            // specialpowers itself.)
-            undos.splice(j, 1);
-            found = true;
-            break;
-          }
-        }
-        if (!undos.length) {
-          // Remove the empty row in permissionsUndoStack
-          this._self._permissionsUndoStack.splice(i, 1);
-        }
-      }
-    }
-  },
-};
-
-SpecialPowersAPI.prototype.EARLY_BETA_OR_EARLIER =
-  AppConstants.EARLY_BETA_OR_EARLIER;
-
-// Due to an unfortunate accident of history, when this API was
-// subclassed using `Thing.prototype = new SpecialPowersAPI()`, existing
-// code depends on all SpecialPowers instances using the same arrays for
-// these.
-Object.assign(SpecialPowersAPI.prototype, {
-  _permissionsUndoStack: [],
-  _pendingPermissions: [],
-});
-
-this.SpecialPowersAPI = SpecialPowersAPI;
-this.bindDOMWindowUtils = bindDOMWindowUtils;
deleted file mode 100644
--- a/testing/specialpowers/content/SpecialPowersAPIParent.jsm
+++ /dev/null
@@ -1,926 +0,0 @@
-/* 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";
-
-var EXPORTED_SYMBOLS = ["SpecialPowersAPIParent", "SpecialPowersError"];
-
-var { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
-var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyModuleGetters(this, {
-  ExtensionData: "resource://gre/modules/Extension.jsm",
-  ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm",
-  PerTestCoverageUtils: "resource://testing-common/PerTestCoverageUtils.jsm",
-  ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
-  SpecialPowersSandbox: "resource://specialpowers/SpecialPowersSandbox.jsm",
-  HiddenFrame: "resource://gre/modules/HiddenFrame.jsm",
-});
-
-class SpecialPowersError extends Error {
-  get name() {
-    return "SpecialPowersError";
-  }
-}
-
-function parseKeyValuePairs(text) {
-  var lines = text.split("\n");
-  var data = {};
-  for (let i = 0; i < lines.length; i++) {
-    if (lines[i] == "") {
-      continue;
-    }
-
-    // can't just .split() because the value might contain = characters
-    let eq = lines[i].indexOf("=");
-    if (eq != -1) {
-      let [key, value] = [
-        lines[i].substring(0, eq),
-        lines[i].substring(eq + 1),
-      ];
-      if (key && value) {
-        data[key] = value.replace(/\\n/g, "\n").replace(/\\\\/g, "\\");
-      }
-    }
-  }
-  return data;
-}
-
-function parseKeyValuePairsFromFile(file) {
-  var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
-    Ci.nsIFileInputStream
-  );
-  fstream.init(file, -1, 0, 0);
-  var is = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(
-    Ci.nsIConverterInputStream
-  );
-  is.init(
-    fstream,
-    "UTF-8",
-    1024,
-    Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER
-  );
-  var str = {};
-  var contents = "";
-  while (is.readString(4096, str) != 0) {
-    contents += str.value;
-  }
-  is.close();
-  fstream.close();
-  return parseKeyValuePairs(contents);
-}
-
-function getTestPlugin(pluginName) {
-  var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-  var tags = ph.getPluginTags();
-  var name = pluginName || "Test Plug-in";
-  for (var tag of tags) {
-    if (tag.name == name) {
-      return tag;
-    }
-  }
-
-  return null;
-}
-
-const PREF_TYPES = {
-  [Ci.nsIPrefBranch.PREF_INVALID]: "INVALID",
-  [Ci.nsIPrefBranch.PREF_INT]: "INT",
-  [Ci.nsIPrefBranch.PREF_BOOL]: "BOOL",
-  [Ci.nsIPrefBranch.PREF_STRING]: "CHAR",
-  number: "INT",
-  boolean: "BOOL",
-  string: "CHAR",
-};
-
-// We share a single preference environment stack between all
-// SpecialPowers instances, across all processes.
-let prefUndoStack = [];
-let inPrefEnvOp = false;
-
-function doPrefEnvOp(fn) {
-  if (inPrefEnvOp) {
-    throw new Error(
-      "Reentrant preference environment operations not supported"
-    );
-  }
-  inPrefEnvOp = true;
-  try {
-    return fn();
-  } finally {
-    inPrefEnvOp = false;
-  }
-}
-
-// Supplies the unique IDs for tasks created by SpecialPowers.spawn(),
-// used to bounce assertion messages back down to the correct child.
-let nextTaskID = 1;
-
-class SpecialPowersAPIParent extends JSWindowActorParent {
-  constructor() {
-    super();
-    this._crashDumpDir = null;
-    this._processCrashObserversRegistered = false;
-    this._chromeScriptListeners = [];
-    this._extensions = new Map();
-    this._taskActors = new Map();
-  }
-
-  _observe(aSubject, aTopic, aData) {
-    function addDumpIDToMessage(propertyName) {
-      try {
-        var id = aSubject.getPropertyAsAString(propertyName);
-      } catch (ex) {
-        id = null;
-      }
-      if (id) {
-        message.dumpIDs.push({ id, extension: "dmp" });
-        message.dumpIDs.push({ id, extension: "extra" });
-      }
-    }
-
-    switch (aTopic) {
-      case "plugin-crashed":
-      case "ipc:content-shutdown":
-        var message = { type: "crash-observed", dumpIDs: [] };
-        aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2);
-        if (aTopic == "plugin-crashed") {
-          addDumpIDToMessage("pluginDumpID");
-          addDumpIDToMessage("browserDumpID");
-
-          let pluginID = aSubject.getPropertyAsAString("pluginDumpID");
-          let extra = this._getExtraData(pluginID);
-          if (extra && "additional_minidumps" in extra) {
-            let dumpNames = extra.additional_minidumps.split(",");
-            for (let name of dumpNames) {
-              message.dumpIDs.push({
-                id: pluginID + "-" + name,
-                extension: "dmp",
-              });
-            }
-          }
-        } else {
-          // ipc:content-shutdown
-          if (!aSubject.hasKey("abnormal")) {
-            return; // This is a normal shutdown, ignore it
-          }
-
-          addDumpIDToMessage("dumpID");
-        }
-        this.sendAsyncMessage("SPProcessCrashService", message);
-        break;
-    }
-  }
-
-  _getCrashDumpDir() {
-    if (!this._crashDumpDir) {
-      this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
-      this._crashDumpDir.append("minidumps");
-    }
-    return this._crashDumpDir;
-  }
-
-  _getPendingCrashDumpDir() {
-    if (!this._pendingCrashDumpDir) {
-      this._pendingCrashDumpDir = Services.dirsvc.get("UAppData", Ci.nsIFile);
-      this._pendingCrashDumpDir.append("Crash Reports");
-      this._pendingCrashDumpDir.append("pending");
-    }
-    return this._pendingCrashDumpDir;
-  }
-
-  _getExtraData(dumpId) {
-    let extraFile = this._getCrashDumpDir().clone();
-    extraFile.append(dumpId + ".extra");
-    if (!extraFile.exists()) {
-      return null;
-    }
-    return parseKeyValuePairsFromFile(extraFile);
-  }
-
-  _deleteCrashDumpFiles(aFilenames) {
-    var crashDumpDir = this._getCrashDumpDir();
-    if (!crashDumpDir.exists()) {
-      return false;
-    }
-
-    var success = aFilenames.length != 0;
-    aFilenames.forEach(function(crashFilename) {
-      var file = crashDumpDir.clone();
-      file.append(crashFilename);
-      if (file.exists()) {
-        file.remove(false);
-      } else {
-        success = false;
-      }
-    });
-    return success;
-  }
-
-  _findCrashDumpFiles(aToIgnore) {
-    var crashDumpDir = this._getCrashDumpDir();
-    var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries;
-    if (!entries) {
-      return [];
-    }
-
-    var crashDumpFiles = [];
-    while (entries.hasMoreElements()) {
-      var file = entries.nextFile;
-      var path = String(file.path);
-      if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) {
-        crashDumpFiles.push(path);
-      }
-    }
-    return crashDumpFiles.concat();
-  }
-
-  _deletePendingCrashDumpFiles() {
-    var crashDumpDir = this._getPendingCrashDumpDir();
-    var removed = false;
-    if (crashDumpDir.exists()) {
-      let entries = crashDumpDir.directoryEntries;
-      while (entries.hasMoreElements()) {
-        let file = entries.nextFile;
-        if (file.isFile()) {
-          file.remove(false);
-          removed = true;
-        }
-      }
-    }
-    return removed;
-  }
-
-  _getURI(url) {
-    return Services.io.newURI(url);
-  }
-  _notifyCategoryAndObservers(subject, topic, data) {
-    const serviceMarker = "service,";
-
-    // First create observers from the category manager.
-
-    let observers = [];
-
-    for (let { value: contractID } of Services.catMan.enumerateCategory(
-      topic
-    )) {
-      let factoryFunction;
-      if (contractID.substring(0, serviceMarker.length) == serviceMarker) {
-        contractID = contractID.substring(serviceMarker.length);
-        factoryFunction = "getService";
-      } else {
-        factoryFunction = "createInstance";
-      }
-
-      try {
-        let handler = Cc[contractID][factoryFunction]();
-        if (handler) {
-          let observer = handler.QueryInterface(Ci.nsIObserver);
-          observers.push(observer);
-        }
-      } catch (e) {}
-    }
-
-    // Next enumerate the registered observers.
-    for (let observer of Services.obs.enumerateObservers(topic)) {
-      if (observer instanceof Ci.nsIObserver && !observers.includes(observer)) {
-        observers.push(observer);
-      }
-    }
-
-    observers.forEach(function(observer) {
-      try {
-        observer.observe(subject, topic, data);
-      } catch (e) {}
-    });
-  }
-
-  /*
-    Iterate through one atomic set of pref actions and perform sets/clears as appropriate.
-    All actions performed must modify the relevant pref.
-  */
-  _applyPrefs(actions) {
-    for (let pref of actions) {
-      if (pref.action == "set") {
-        this._setPref(pref.name, pref.type, pref.value, pref.iid);
-      } else if (pref.action == "clear") {
-        Services.prefs.clearUserPref(pref.name);
-      }
-    }
-  }
-
-  /**
-   * Take in a list of pref changes to make, pushes their current values
-   * onto the restore stack, and makes the changes.  When the test
-   * finishes, these changes are reverted.
-   *
-   * |inPrefs| must be an object with up to two properties: "set" and "clear".
-   * pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset
-   * the prefs indicated in |inPrefs.clear|.
-   *
-   * For example, you might pass |inPrefs| as:
-   *
-   *  inPrefs = {'set': [['foo.bar', 2], ['magic.pref', 'baz']],
-   *             'clear': [['clear.this'], ['also.this']] };
-   *
-   * Notice that |set| and |clear| are both an array of arrays.  In |set|, each
-   * of the inner arrays must have the form [pref_name, value] or [pref_name,
-   * value, iid].  (The latter form is used for prefs with "complex" values.)
-   *
-   * In |clear|, each inner array should have the form [pref_name].
-   *
-   * If you set the same pref more than once (or both set and clear a pref),
-   * the behavior of this method is undefined.
-   */
-  pushPrefEnv(inPrefs) {
-    return doPrefEnvOp(() => {
-      let pendingActions = [];
-      let cleanupActions = [];
-
-      for (let [action, prefs] of Object.entries(inPrefs)) {
-        for (let pref of prefs) {
-          let name = pref[0];
-          let value = null;
-          let iid = null;
-          let type = PREF_TYPES[Services.prefs.getPrefType(name)];
-          let originalValue = null;
-
-          if (pref.length == 3) {
-            value = pref[1];
-            iid = pref[2];
-          } else if (pref.length == 2) {
-            value = pref[1];
-          }
-
-          /* If pref is not found or invalid it doesn't exist. */
-          if (type !== "INVALID") {
-            if (
-              (Services.prefs.prefHasUserValue(name) && action == "clear") ||
-              action == "set"
-            ) {
-              originalValue = this._getPref(name, type);
-            }
-          } else if (action == "set") {
-            /* name doesn't exist, so 'clear' is pointless */
-            if (iid) {
-              type = "COMPLEX";
-            }
-          }
-
-          if (type === "INVALID") {
-            type = PREF_TYPES[typeof value];
-          }
-          if (type === "INVALID") {
-            throw new Error("Unexpected preference type");
-          }
-
-          pendingActions.push({ action, type, name, value, iid });
-
-          /* Push original preference value or clear into cleanup array */
-          var cleanupTodo = { type, name, value: originalValue, iid };
-          if (originalValue == null) {
-            cleanupTodo.action = "clear";
-          } else {
-            cleanupTodo.action = "set";
-          }
-          cleanupActions.push(cleanupTodo);
-        }
-      }
-
-      prefUndoStack.push(cleanupActions);
-      this._applyPrefs(pendingActions);
-    });
-  }
-
-  async popPrefEnv() {
-    return doPrefEnvOp(() => {
-      let env = prefUndoStack.pop();
-      if (env) {
-        this._applyPrefs(env);
-        return true;
-      }
-      return false;
-    });
-  }
-
-  flushPrefEnv() {
-    while (prefUndoStack.length) {
-      this.popPrefEnv();
-    }
-  }
-
-  _setPref(name, type, value, iid) {
-    switch (type) {
-      case "BOOL":
-        return Services.prefs.setBoolPref(name, value);
-      case "INT":
-        return Services.prefs.setIntPref(name, value);
-      case "CHAR":
-        return Services.prefs.setCharPref(name, value);
-      case "COMPLEX":
-        return Services.prefs.setComplexValue(name, iid, value);
-    }
-    throw new Error(`Unexpected preference type: ${type}`);
-  }
-
-  _getPref(name, type, defaultValue, iid) {
-    switch (type) {
-      case "BOOL":
-        if (defaultValue !== undefined) {
-          return Services.prefs.getBoolPref(name, defaultValue);
-        }
-        return Services.prefs.getBoolPref(name);
-      case "INT":
-        if (defaultValue !== undefined) {
-          return Services.prefs.getIntPref(name, defaultValue);
-        }
-        return Services.prefs.getIntPref(name);
-      case "CHAR":
-        if (defaultValue !== undefined) {
-          return Services.prefs.getCharPref(name, defaultValue);
-        }
-        return Services.prefs.getCharPref(name);
-      case "COMPLEX":
-        return Services.prefs.getComplexValue(name, iid);
-    }
-    throw new Error(`Unexpected preference type: ${type}`);
-  }
-
-  _toggleMuteAudio(aMuted) {
-    let browser = this.browsingContext.top.embedderElement;
-    if (aMuted) {
-      browser.mute();
-    } else {
-      browser.unmute();
-    }
-  }
-
-  /**
-   * messageManager callback function
-   * This will get requests from our API in the window and process them in chrome for it
-   **/
-  // eslint-disable-next-line complexity
-  receiveMessage(aMessage) {
-    // We explicitly return values in the below code so that this function
-    // doesn't trigger a flurry of warnings about "does not always return
-    // a value".
-    switch (aMessage.name) {
-      case "SPToggleMuteAudio":
-        return this._toggleMuteAudio(aMessage.data.mute);
-      case "PushPrefEnv":
-        return this.pushPrefEnv(aMessage.data);
-
-      case "PopPrefEnv":
-        return this.popPrefEnv();
-
-      case "FlushPrefEnv":
-        return this.flushPrefEnv();
-
-      case "SPPrefService": {
-        let prefs = Services.prefs;
-        let prefType = aMessage.json.prefType.toUpperCase();
-        let { prefName, prefValue, iid, defaultValue } = aMessage.json;
-
-        if (aMessage.json.op == "get") {
-          if (!prefName || !prefType) {
-            throw new SpecialPowersError(
-              "Invalid parameters for get in SPPrefService"
-            );
-          }
-
-          // return null if the pref doesn't exist
-          if (
-            defaultValue === undefined &&
-            prefs.getPrefType(prefName) == prefs.PREF_INVALID
-          ) {
-            return null;
-          }
-          return this._getPref(prefName, prefType, defaultValue, iid);
-        } else if (aMessage.json.op == "set") {
-          if (!prefName || !prefType || prefValue === undefined) {
-            throw new SpecialPowersError(
-              "Invalid parameters for set in SPPrefService"
-            );
-          }
-
-          return this._setPref(prefName, prefType, prefValue, iid);
-        } else if (aMessage.json.op == "clear") {
-          if (!prefName) {
-            throw new SpecialPowersError(
-              "Invalid parameters for clear in SPPrefService"
-            );
-          }
-
-          prefs.clearUserPref(prefName);
-        } else {
-          throw new SpecialPowersError("Invalid operation for SPPrefService");
-        }
-
-        return undefined; // See comment at the beginning of this function.
-      }
-
-      case "SPProcessCrashService": {
-        switch (aMessage.json.op) {
-          case "register-observer":
-            this._addProcessCrashObservers();
-            break;
-          case "unregister-observer":
-            this._removeProcessCrashObservers();
-            break;
-          case "delete-crash-dump-files":
-            return this._deleteCrashDumpFiles(aMessage.json.filenames);
-          case "find-crash-dump-files":
-            return this._findCrashDumpFiles(
-              aMessage.json.crashDumpFilesToIgnore
-            );
-          case "delete-pending-crash-dump-files":
-            return this._deletePendingCrashDumpFiles();
-          default:
-            throw new SpecialPowersError(
-              "Invalid operation for SPProcessCrashService"
-            );
-        }
-        return undefined; // See comment at the beginning of this function.
-      }
-
-      case "SPProcessCrashManagerWait": {
-        let promises = aMessage.json.crashIds.map(crashId => {
-          return Services.crashmanager.ensureCrashIsPresent(crashId);
-        });
-        return Promise.all(promises);
-      }
-
-      case "SPPermissionManager": {
-        let msg = aMessage.json;
-        let principal = msg.principal;
-
-        switch (msg.op) {
-          case "add":
-            Services.perms.addFromPrincipal(
-              principal,
-              msg.type,
-              msg.permission,
-              msg.expireType,
-              msg.expireTime
-            );
-            break;
-          case "remove":
-            Services.perms.removeFromPrincipal(principal, msg.type);
-            break;
-          case "has":
-            let hasPerm = Services.perms.testPermissionFromPrincipal(
-              principal,
-              msg.type
-            );
-            return hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
-          case "test":
-            let testPerm = Services.perms.testPermissionFromPrincipal(
-              principal,
-              msg.type
-            );
-            return testPerm == msg.value;
-          default:
-            throw new SpecialPowersError(
-              "Invalid operation for SPPermissionManager"
-            );
-        }
-        return undefined; // See comment at the beginning of this function.
-      }
-
-      case "SPSetTestPluginEnabledState": {
-        var plugin = getTestPlugin(aMessage.data.pluginName);
-        if (!plugin) {
-          return undefined;
-        }
-        var oldEnabledState = plugin.enabledState;
-        plugin.enabledState = aMessage.data.newEnabledState;
-        return oldEnabledState;
-      }
-
-      case "SPObserverService": {
-        let topic = aMessage.json.observerTopic;
-        switch (aMessage.json.op) {
-          case "notify":
-            let data = aMessage.json.observerData;
-            Services.obs.notifyObservers(null, topic, data);
-            break;
-          case "add":
-            this._registerObservers._add(topic);
-            break;
-          default:
-            throw new SpecialPowersError(
-              "Invalid operation for SPObserverervice"
-            );
-        }
-        return undefined; // See comment at the beginning of this function.
-      }
-
-      case "SPLoadChromeScript": {
-        let id = aMessage.json.id;
-        let scriptName;
-
-        let jsScript = aMessage.json.function.body;
-        if (aMessage.json.url) {
-          scriptName = aMessage.json.url;
-        } else if (aMessage.json.function) {
-          scriptName =
-            aMessage.json.function.name ||
-            "<loadChromeScript anonymous function>";
-        } else {
-          throw new SpecialPowersError("SPLoadChromeScript: Invalid script");
-        }
-
-        // Setup a chrome sandbox that has access to sendAsyncMessage
-        // and {add,remove}MessageListener in order to communicate with
-        // the mochitest.
-        let sb = new SpecialPowersSandbox(
-          scriptName,
-          data => {
-            this.sendAsyncMessage("Assert", data);
-          },
-          aMessage.data
-        );
-
-        Object.assign(sb.sandbox, {
-          sendAsyncMessage: (name, message) => {
-            this.sendAsyncMessage("SPChromeScriptMessage", {
-              id,
-              name,
-              message,
-            });
-          },
-          addMessageListener: (name, listener) => {
-            this._chromeScriptListeners.push({ id, name, listener });
-          },
-          removeMessageListener: (name, listener) => {
-            let index = this._chromeScriptListeners.findIndex(function(obj) {
-              return (
-                obj.id == id && obj.name == name && obj.listener == listener
-              );
-            });
-            if (index >= 0) {
-              this._chromeScriptListeners.splice(index, 1);
-            }
-          },
-          actorParent: this.manager,
-        });
-
-        // Evaluate the chrome script
-        try {
-          Cu.evalInSandbox(jsScript, sb.sandbox, "1.8", scriptName, 1);
-        } catch (e) {
-          throw new SpecialPowersError(
-            "Error while executing chrome script '" +
-              scriptName +
-              "':\n" +
-              e +
-              "\n" +
-              e.fileName +
-              ":" +
-              e.lineNumber
-          );
-        }
-        return undefined; // See comment at the beginning of this function.
-      }
-
-      case "SPChromeScriptMessage": {
-        let id = aMessage.json.id;
-        let name = aMessage.json.name;
-        let message = aMessage.json.message;
-        let result;
-        for (let listener of this._chromeScriptListeners) {
-          if (listener.name == name && listener.id == id) {
-            result = listener.listener(message);
-          }
-        }
-        return result;
-      }
-
-      case "SPImportInMainProcess": {
-        var message = { hadError: false, errorMessage: null };
-        try {
-          ChromeUtils.import(aMessage.data);
-        } catch (e) {
-          message.hadError = true;
-          message.errorMessage = e.toString();
-        }
-        return message;
-      }
-
-      case "SPCleanUpSTSData": {
-        let origin = aMessage.data.origin;
-        let flags = aMessage.data.flags;
-        let uri = Services.io.newURI(origin);
-        let sss = Cc["@mozilla.org/ssservice;1"].getService(
-          Ci.nsISiteSecurityService
-        );
-        sss.resetState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, flags);
-        return undefined;
-      }
-
-      case "SPRequestDumpCoverageCounters": {
-        return PerTestCoverageUtils.afterTest();
-      }
-
-      case "SPRequestResetCoverageCounters": {
-        return PerTestCoverageUtils.beforeTest();
-      }
-
-      case "SPCheckServiceWorkers": {
-        let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
-          Ci.nsIServiceWorkerManager
-        );
-        let regs = swm.getAllRegistrations();
-
-        // XXX This code is shared with specialpowers.js.
-        let workers = new Array(regs.length);
-        for (let i = 0; i < regs.length; ++i) {
-          let { scope, scriptSpec } = regs.queryElementAt(
-            i,
-            Ci.nsIServiceWorkerRegistrationInfo
-          );
-          workers[i] = { scope, scriptSpec };
-        }
-        return { workers };
-      }
-
-      case "SPLoadExtension": {
-        let id = aMessage.data.id;
-        let ext = aMessage.data.ext;
-        let extension = ExtensionTestCommon.generate(ext);
-
-        let resultListener = (...args) => {
-          this.sendAsyncMessage("SPExtensionMessage", {
-            id,
-            type: "testResult",
-            args,
-          });
-        };
-
-        let messageListener = (...args) => {
-          args.shift();
-          this.sendAsyncMessage("SPExtensionMessage", {
-            id,
-            type: "testMessage",
-            args,
-          });
-        };
-
-        // Register pass/fail handlers.
-        extension.on("test-result", resultListener);
-        extension.on("test-eq", resultListener);
-        extension.on("test-log", resultListener);
-        extension.on("test-done", resultListener);
-
-        extension.on("test-message", messageListener);
-
-        this._extensions.set(id, extension);
-        return undefined;
-      }
-
-      case "SPStartupExtension": {
-        let id = aMessage.data.id;
-        // This is either an Extension, or (if useAddonManager is set) a MockExtension.
-        let extension = this._extensions.get(id);
-        extension.on("startup", (eventName, ext) => {
-          if (!ext) {
-            // ext is only set by the "startup" event from Extension.jsm.
-            // Unfortunately ext-backgroundPage.js emits an event with the same
-            // name, but without the extension object as parameter.
-            return;
-          }
-          // ext is always the "real" Extension object, even when "extension"
-          // is a MockExtension.
-          this.sendAsyncMessage("SPExtensionMessage", {
-            id,
-            type: "extensionSetId",
-            args: [ext.id, ext.uuid],
-          });
-        });
-
-        // Make sure the extension passes the packaging checks when
-        // they're run on a bare archive rather than a running instance,
-        // as the add-on manager runs them.
-        let extensionData = new ExtensionData(extension.rootURI);
-        return extensionData
-          .loadManifest()
-          .then(
-            () => {
-              return extensionData.initAllLocales().then(() => {
-                if (extensionData.errors.length) {
-                  return Promise.reject("Extension contains packaging errors");
-                }
-                return undefined;
-              });
-            },
-            () => {
-              // loadManifest() will throw if we're loading an embedded
-              // extension, so don't worry about locale errors in that
-              // case.
-            }
-          )
-          .then(async () => {
-            // browser tests do not call startup in ExtensionXPCShellUtils or MockExtension,
-            // in that case we have an ID here and we need to set the override.
-            if (extension.id) {
-              await ExtensionTestCommon.setIncognitoOverride(extension);
-            }
-            return extension.startup().then(
-              () => {},
-              e => {
-                dump(`Extension startup failed: ${e}\n${e.stack}`);
-                throw e;
-              }
-            );
-          });
-      }
-
-      case "SPExtensionMessage": {
-        let id = aMessage.data.id;
-        let extension = this._extensions.get(id);
-        extension.testMessage(...aMessage.data.args);
-        return undefined;
-      }
-
-      case "SPUnloadExtension": {
-        let id = aMessage.data.id;
-        let extension = this._extensions.get(id);
-        this._extensions.delete(id);
-        return extension.shutdown().then(() => {
-          return extension._uninstallPromise;
-        });
-      }
-
-      case "Spawn": {
-        let { browsingContext, task, args, caller } = aMessage.data;
-
-        let spParent = browsingContext.currentWindowGlobal.getActor(
-          "SpecialPowers"
-        );
-
-        let taskId = nextTaskID++;
-        spParent._taskActors.set(taskId, this);
-
-        return spParent
-          .sendQuery("Spawn", { task, args, caller, taskId })
-          .finally(() => {
-            spParent._taskActors.delete(taskId);
-          });
-      }
-
-      case "Snapshot": {
-        let { browsingContext, rect, background } = aMessage.data;
-
-        return browsingContext.currentWindowGlobal
-          .drawSnapshot(rect, 1.0, background)
-          .then(async image => {
-            let hiddenFrame = new HiddenFrame();
-            let win = await hiddenFrame.get();
-
-            let canvas = win.document.createElement("canvas");
-            canvas.width = image.width;
-            canvas.height = image.height;
-
-            const ctx = canvas.getContext("2d");
-            ctx.drawImage(image, 0, 0);
-
-            let data = ctx.getImageData(0, 0, image.width, image.height);
-            hiddenFrame.destroy();
-            return data;
-          });
-      }
-
-      case "ProxiedAssert": {
-        let { taskId, data } = aMessage.data;
-        let actor = this._taskActors.get(taskId);
-
-        actor.sendAsyncMessage("Assert", data);
-        return undefined;
-      }
-
-      case "SPRemoveAllServiceWorkers": {
-        return ServiceWorkerCleanUp.removeAll();
-      }
-
-      case "SPRemoveServiceWorkerDataForExampleDomain": {
-        return ServiceWorkerCleanUp.removeFromHost("example.com");
-      }
-
-      case "Wakeup":
-        return undefined;
-
-      default:
-        throw new SpecialPowersError(
-          `Unrecognized Special Powers API: ${aMessage.name}`
-        );
-    }
-
-    // We throw an exception before reaching this explicit return because
-    // we should never be arriving here anyway.
-    throw new SpecialPowersError("Unreached code"); // eslint-disable-line no-unreachable
-    return undefined;
-  }
-}
--- a/testing/specialpowers/content/SpecialPowersChild.jsm
+++ b/testing/specialpowers/content/SpecialPowersChild.jsm
@@ -1,33 +1,156 @@
 /* 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/. */
 /* This code is loaded in every child process that is started by mochitest in
  * order to be used as a replacement for UniversalXPConnect
  */
 
+"use strict";
+
+var EXPORTED_SYMBOLS = ["SpecialPowersChild"];
+
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-var EXPORTED_SYMBOLS = [
-  "SpecialPowers",
-  "SpecialPowersChild",
-  "attachSpecialPowersToWindow",
-];
-
-const { bindDOMWindowUtils, SpecialPowersAPI } = ChromeUtils.import(
-  "resource://specialpowers/SpecialPowersAPI.jsm"
-);
 const { ExtensionUtils } = ChromeUtils.import(
   "resource://gre/modules/ExtensionUtils.jsm"
 );
 
+ChromeUtils.defineModuleGetter(
+  this,
+  "MockFilePicker",
+  "resource://specialpowers/MockFilePicker.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "MockColorPicker",
+  "resource://specialpowers/MockColorPicker.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "MockPermissionPrompt",
+  "resource://specialpowers/MockPermissionPrompt.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "SpecialPowersSandbox",
+  "resource://specialpowers/SpecialPowersSandbox.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "WrapPrivileged",
+  "resource://specialpowers/WrapPrivileged.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "PrivateBrowsingUtils",
+  "resource://gre/modules/PrivateBrowsingUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "NetUtil",
+  "resource://gre/modules/NetUtil.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "AppConstants",
+  "resource://gre/modules/AppConstants.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "PerTestCoverageUtils",
+  "resource://testing-common/PerTestCoverageUtils.jsm"
+);
+
+// Allow stuff from this scope to be accessed from non-privileged scopes. This
+// would crash if used outside of automation.
 Cu.forcePermissiveCOWs();
 
-class SpecialPowersChild extends SpecialPowersAPI {
+function bindDOMWindowUtils(aWindow) {
+  return aWindow && WrapPrivileged.wrap(aWindow.windowUtils);
+}
+
+// SPConsoleListener reflects nsIConsoleMessage objects into JS in a
+// tidy, XPCOM-hiding way.  Messages that are nsIScriptError objects
+// have their properties exposed in detail.  It also auto-unregisters
+// itself when it receives a "sentinel" message.
+function SPConsoleListener(callback) {
+  this.callback = callback;
+}
+
+SPConsoleListener.prototype = {
+  // Overload the observe method for both nsIConsoleListener and nsIObserver.
+  // The topic will be null for nsIConsoleListener.
+  observe(msg, topic) {
+    let m = {
+      message: msg.message,
+      errorMessage: null,
+      cssSelectors: null,
+      sourceName: null,
+      sourceLine: null,
+      lineNumber: null,
+      columnNumber: null,
+      category: null,
+      windowID: null,
+      isScriptError: false,
+      isConsoleEvent: false,
+      isWarning: false,
+      isException: false,
+      isStrict: false,
+    };
+    if (msg instanceof Ci.nsIScriptError) {
+      m.errorMessage = msg.errorMessage;
+      m.cssSelectors = msg.cssSelectors;
+      m.sourceName = msg.sourceName;
+      m.sourceLine = msg.sourceLine;
+      m.lineNumber = msg.lineNumber;
+      m.columnNumber = msg.columnNumber;
+      m.category = msg.category;
+      m.windowID = msg.outerWindowID;
+      m.innerWindowID = msg.innerWindowID;
+      m.isScriptError = true;
+      m.isWarning = (msg.flags & Ci.nsIScriptError.warningFlag) === 1;
+      m.isException = (msg.flags & Ci.nsIScriptError.exceptionFlag) === 1;
+      m.isStrict = (msg.flags & Ci.nsIScriptError.strictFlag) === 1;
+    } else if (topic === "console-api-log-event") {
+      // This is a dom/console event.
+      let unwrapped = msg.wrappedJSObject;
+      m.errorMessage = unwrapped.arguments[0];
+      m.sourceName = unwrapped.filename;
+      m.lineNumber = unwrapped.lineNumber;
+      m.columnNumber = unwrapped.columnNumber;
+      m.windowID = unwrapped.ID;
+      m.innerWindowID = unwrapped.innerID;
+      m.isConsoleEvent = true;
+      m.isWarning = unwrapped.level === "warning";
+    }
+
+    Object.freeze(m);
+
+    // Run in a separate runnable since console listeners aren't
+    // supposed to touch content and this one might.
+    Services.tm.dispatchToMainThread(() => {
+      this.callback.call(undefined, m);
+    });
+
+    if (!m.isScriptError && !m.isConsoleEvent && m.message === "SENTINEL") {
+      Services.obs.removeObserver(this, "console-api-log-event");
+      Services.console.unregisterListener(this);
+    }
+  },
+
+  QueryInterface: ChromeUtils.generateQI([
+    Ci.nsIConsoleListener,
+    Ci.nsIObserver,
+  ]),
+};
+
+class SpecialPowersChild extends JSWindowActorChild {
   constructor() {
     super();
 
     this._windowID = null;
     this.DOMWindowUtils = null;
 
     this._encounteredCrashDumpFiles = [];
     this._unexpectedCrashDumpFiles = {};
@@ -38,19 +161,39 @@ class SpecialPowersChild extends Special
       configurable: true,
       enumerable: true,
       value: this.getFullComponents(),
     });
     this._createFilesOnError = null;
     this._createFilesOnSuccess = null;
 
     this._messageListeners = new ExtensionUtils.DefaultMap(() => new Set());
+
+    this._consoleListeners = [];
+    this._encounteredCrashDumpFiles = [];
+    this._unexpectedCrashDumpFiles = {};
+    this._crashDumpDir = null;
+    this._mfl = null;
+    this._applyingPermissions = false;
+    this._observingPermissions = false;
+    this._asyncObservers = new WeakMap();
+    this._xpcomabi = null;
+    this._os = null;
+    this._pu = null;
+
+    this._nextExtensionID = 0;
+    this._extensionListeners = null;
   }
 
   handleEvent(aEvent) {
+    // We don't actually care much about the "DOMWindowCreated" event.
+    // We only listen to it to force creation of the actor.
+  }
+
+  actorCreated() {
     this.attachToWindow();
   }
 
   attachToWindow() {
     let window = this.contentWindow;
     if (!window.wrappedJSObject.SpecialPowers) {
       this._windowID = window.windowUtils.currentInnerWindowID;
       this.DOMWindowUtils = bindDOMWindowUtils(window);
@@ -62,96 +205,214 @@ class SpecialPowersChild extends Special
       }
     }
   }
 
   get window() {
     return this.contentWindow;
   }
 
+  // Hack around devtools sometimes trying to JSON stringify us.
+  toJSON() {
+    return {};
+  }
+
   toString() {
     return "[SpecialPowers]";
   }
   sanityCheck() {
     return "foo";
   }
 
   _addMessageListener(msgname, listener) {
     this._messageListeners.get(msgname).add(listener);
   }
 
   _removeMessageListener(msgname, listener) {
     this._messageListeners.get(msgname).delete(listener);
   }
 
+  receiveMessage(message) {
+    if (this._messageListeners.has(message.name)) {
+      for (let listener of this._messageListeners.get(message.name)) {
+        try {
+          if (typeof listener === "function") {
+            listener(message);
+          } else {
+            listener.receiveMessage(message);
+          }
+        } catch (e) {
+          Cu.reportError(e);
+        }
+      }
+    }
+
+    switch (message.name) {
+      case "SPProcessCrashService":
+        if (message.json.type == "crash-observed") {
+          for (let e of message.json.dumpIDs) {
+            this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
+          }
+        }
+        break;
+
+      case "SPServiceWorkerRegistered":
+        this._serviceWorkerRegistered = message.data.registered;
+        break;
+
+      case "SpecialPowers.FilesCreated":
+        var createdHandler = this._createFilesOnSuccess;
+        this._createFilesOnSuccess = null;
+        this._createFilesOnError = null;
+        if (createdHandler) {
+          createdHandler(Cu.cloneInto(message.data, this.contentWindow));
+        }
+        break;
+
+      case "SpecialPowers.FilesError":
+        var errorHandler = this._createFilesOnError;
+        this._createFilesOnSuccess = null;
+        this._createFilesOnError = null;
+        if (errorHandler) {
+          errorHandler(message.data);
+        }
+        break;
+
+      case "Spawn":
+        let { task, args, caller, taskId } = message.data;
+        return this._spawnTask(task, args, caller, taskId);
+
+      case "Assert":
+        {
+          // An assertion has been done in a mochitest chrome script
+          let { name, passed, stack, diag } = message.data;
+
+          let { SimpleTest } = this;
+          if (SimpleTest) {
+            SimpleTest.record(
+              passed,
+              name,
+              diag,
+              stack && stack.formattedStack
+            );
+          } else {
+            // Well, this is unexpected.
+            dump(name + "\n");
+          }
+        }
+        break;
+    }
+    return undefined;
+  }
+
   registerProcessCrashObservers() {
     this.sendAsyncMessage("SPProcessCrashService", { op: "register-observer" });
   }
 
   unregisterProcessCrashObservers() {
     this.sendAsyncMessage("SPProcessCrashService", {
       op: "unregister-observer",
     });
   }
 
-  receiveMessage(aMessage) {
-    if (this._messageListeners.has(aMessage.name)) {
-      for (let listener of this._messageListeners.get(aMessage.name)) {
-        try {
-          if (typeof listener === "function") {
-            listener(aMessage);
-          } else {
-            listener.receiveMessage(aMessage);
-          }
-        } catch (e) {
-          Cu.reportError(e);
-        }
-      }
+  /*
+   * Privileged object wrapping API
+   *
+   * Usage:
+   *   var wrapper = SpecialPowers.wrap(obj);
+   *   wrapper.privilegedMethod(); wrapper.privilegedProperty;
+   *   obj === SpecialPowers.unwrap(wrapper);
+   *
+   * These functions provide transparent access to privileged objects using
+   * various pieces of deep SpiderMagic. Conceptually, a wrapper is just an
+   * object containing a reference to the underlying object, where all method
+   * calls and property accesses are transparently performed with the System
+   * Principal. Moreover, objects obtained from the wrapper (including properties
+   * and method return values) are wrapped automatically. Thus, after a single
+   * call to SpecialPowers.wrap(), the wrapper layer is transitively maintained.
+   *
+   * Known Issues:
+   *
+   *  - The wrapping function does not preserve identity, so
+   *    SpecialPowers.wrap(foo) !== SpecialPowers.wrap(foo). See bug 718543.
+   *
+   *  - The wrapper cannot see expando properties on unprivileged DOM objects.
+   *    That is to say, the wrapper uses Xray delegation.
+   *
+   *  - The wrapper sometimes guesses certain ES5 attributes for returned
+   *    properties. This is explained in a comment in the wrapper code above,
+   *    and shouldn't be a problem.
+   */
+  wrap(obj) {
+    return WrapPrivileged.wrap(obj);
+  }
+  unwrap(obj) {
+    return WrapPrivileged.unwrap(obj);
+  }
+  isWrapper(val) {
+    return WrapPrivileged.isWrapper(val);
+  }
+
+  /*
+   * When content needs to pass a callback or a callback object to an API
+   * accessed over SpecialPowers, that API may sometimes receive arguments for
+   * whom it is forbidden to create a wrapper in content scopes. As such, we
+   * need a layer to wrap the values in SpecialPowers wrappers before they ever
+   * reach content.
+   */
+  wrapCallback(func) {
+    return WrapPrivileged.wrapCallback(func);
+  }
+  wrapCallbackObject(obj) {
+    return WrapPrivileged.wrapCallbackObject(obj);
+  }
+
+  /*
+   * Used for assigning a property to a SpecialPowers wrapper, without unwrapping
+   * the value that is assigned.
+   */
+  setWrapped(obj, prop, val) {
+    if (!WrapPrivileged.isWrapper(obj)) {
+      throw new Error(
+        "You only need to use this for SpecialPowers wrapped objects"
+      );
     }
 
-    switch (aMessage.name) {
-      case "SPProcessCrashService":
-        if (aMessage.json.type == "crash-observed") {
-          for (let e of aMessage.json.dumpIDs) {
-            this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
-          }
-        }
-        break;
+    obj = WrapPrivileged.unwrap(obj);
+    return Reflect.set(obj, prop, val);
+  }
 
-      case "SPServiceWorkerRegistered":
-        this._serviceWorkerRegistered = aMessage.data.registered;
-        break;
+  /*
+   * Create blank privileged objects to use as out-params for privileged functions.
+   */
+  createBlankObject() {
+    return {};
+  }
 
-      case "SpecialPowers.FilesCreated":
-        var createdHandler = this._createFilesOnSuccess;
-        this._createFilesOnSuccess = null;
-        this._createFilesOnError = null;
-        if (createdHandler) {
-          createdHandler(Cu.cloneInto(aMessage.data, this.contentWindow));
-        }
-        break;
+  /*
+   * Because SpecialPowers wrappers don't preserve identity, comparing with ==
+   * can be hazardous. Sometimes we can just unwrap to compare, but sometimes
+   * wrapping the underlying object into a content scope is forbidden. This
+   * function strips any wrappers if they exist and compare the underlying
+   * values.
+   */
+  compare(a, b) {
+    return WrapPrivileged.unwrap(a) === WrapPrivileged.unwrap(b);
+  }
 
-      case "SpecialPowers.FilesError":
-        var errorHandler = this._createFilesOnError;
-        this._createFilesOnSuccess = null;
-        this._createFilesOnError = null;
-        if (errorHandler) {
-          errorHandler(aMessage.data);
-        }
-        break;
+  get MockFilePicker() {
+    return MockFilePicker;
+  }
 
-      case "Spawn":
-        let { task, args, caller, taskId } = aMessage.data;
-        return this._spawnTask(task, args, caller, taskId);
+  get MockColorPicker() {
+    return MockColorPicker;
+  }
 
-      default:
-        return super.receiveMessage(aMessage);
-    }
-
-    return true;
+  get MockPermissionPrompt() {
+    return MockPermissionPrompt;
   }
 
   quit() {
     this.sendAsyncMessage("SpecialPowers.Quit", {});
   }
 
   // fileRequests is an array of file requests. Each file request is an object.
   // A request must have a field |name|, which gives the base of the name of the
@@ -206,9 +467,1853 @@ class SpecialPowersChild extends Special
       // call to the parent to make sure that it called unregister on all of its
       // service workers.
       let { workers } = await this.sendQuery("SPCheckServiceWorkers");
       return workers;
     }
 
     return [];
   }
+
+  /*
+   * Load a privileged script that runs same-process. This is different from
+   * |loadChromeScript|, which will run in the parent process in e10s mode.
+   */
+  loadPrivilegedScript(aFunction) {
+    var str = "(" + aFunction.toString() + ")();";
+    let gGlobalObject = Cu.getGlobalForObject(this);
+    let sb = Cu.Sandbox(gGlobalObject);
+    var window = this.contentWindow;
+    var mc = new window.MessageChannel();
+    sb.port = mc.port1;
+    try {
+      let blob = new Blob([str], { type: "application/javascript" });
+      let blobUrl = URL.createObjectURL(blob);
+      Services.scriptloader.loadSubScript(blobUrl, sb);
+    } catch (e) {
+      throw WrapPrivileged.wrap(e);
+    }
+
+    return mc.port2;
+  }
+
+  _readUrlAsString(aUrl) {
+    // Fetch script content as we can't use scriptloader's loadSubScript
+    // to evaluate http:// urls...
+    var scriptableStream = Cc[
+      "@mozilla.org/scriptableinputstream;1"
+    ].getService(Ci.nsIScriptableInputStream);
+
+    var channel = NetUtil.newChannel({
+      uri: aUrl,
+      loadUsingSystemPrincipal: true,
+    });
+    var input = channel.open();
+    scriptableStream.init(input);
+
+    var str;
+    var buffer = [];
+
+    while ((str = scriptableStream.read(4096))) {
+      buffer.push(str);
+    }
+
+    var output = buffer.join("");
+
+    scriptableStream.close();
+    input.close();
+
+    var status;
+    if (channel instanceof Ci.nsIHttpChannel) {
+      status = channel.responseStatus;
+    }
+
+    if (status == 404) {
+      throw new Error(
+        `Error while executing chrome script '${aUrl}':\n` +
+          "The script doesn't exist. Ensure you have registered it in " +
+          "'support-files' in your mochitest.ini."
+      );
+    }
+
+    return output;
+  }
+
+  loadChromeScript(urlOrFunction, sandboxOptions) {
+    // Create a unique id for this chrome script
+    let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
+      Ci.nsIUUIDGenerator
+    );
+    let id = uuidGenerator.generateUUID().toString();
+
+    // Tells chrome code to evaluate this chrome script
+    let scriptArgs = { id, sandboxOptions };
+    if (typeof urlOrFunction == "function") {
+      scriptArgs.function = {
+        body: "(" + urlOrFunction.toString() + ")();",
+        name: urlOrFunction.name,
+      };
+    } else {
+      // Note: We need to do this in the child since, even though
+      // `_readUrlAsString` pretends to be synchronous, its channel
+      // winds up spinning the event loop when loading HTTP URLs. That
+      // leads to unexpected out-of-order operations if the child sends
+      // a message immediately after loading the script.
+      scriptArgs.function = {
+        body: this._readUrlAsString(urlOrFunction),
+      };
+      scriptArgs.url = urlOrFunction;
+    }
+    this.sendAsyncMessage("SPLoadChromeScript", scriptArgs);
+
+    // Returns a MessageManager like API in order to be
+    // able to communicate with this chrome script
+    let listeners = [];
+    let chromeScript = {
+      addMessageListener: (name, listener) => {
+        listeners.push({ name, listener });
+      },
+
+      promiseOneMessage: name =>
+        new Promise(resolve => {
+          chromeScript.addMessageListener(name, function listener(message) {
+            chromeScript.removeMessageListener(name, listener);
+            resolve(message);
+          });
+        }),
+
+      removeMessageListener: (name, listener) => {
+        listeners = listeners.filter(
+          o => o.name != name || o.listener != listener
+        );
+      },
+
+      sendAsyncMessage: (name, message) => {
+        this.sendAsyncMessage("SPChromeScriptMessage", { id, name, message });
+      },
+
+      sendQuery: (name, message) => {
+        return this.sendQuery("SPChromeScriptMessage", { id, name, message });
+      },
+
+      destroy: () => {
+        listeners = [];
+        this._removeMessageListener("SPChromeScriptMessage", chromeScript);
+      },
+
+      receiveMessage: aMessage => {
+        let messageId = aMessage.json.id;
+        let name = aMessage.json.name;
+        let message = aMessage.json.message;
+        if (this.contentWindow) {
+          message = new StructuredCloneHolder(message).deserialize(
+            this.contentWindow
+          );
+        }
+        // Ignore message from other chrome script
+        if (messageId != id) {
+          return null;
+        }
+
+        let result;
+        if (aMessage.name == "SPChromeScriptMessage") {
+          for (let listener of listeners.filter(o => o.name == name)) {
+            result = listener.listener(message);
+          }
+        }
+        return result;
+      },
+    };
+    this._addMessageListener("SPChromeScriptMessage", chromeScript);
+
+    return this.wrap(chromeScript);
+  }
+
+  async importInMainProcess(importString) {
+    var message = await this.sendQuery("SPImportInMainProcess", importString);
+    if (message.hadError) {
+      throw new Error(
+        "SpecialPowers.importInMainProcess failed with error " +
+          message.errorMessage
+      );
+    }
+  }
+
+  get Services() {
+    return WrapPrivileged.wrap(Services);
+  }
+
+  /*
+   * A getter for the privileged Components object we have.
+   */
+  getFullComponents() {
+    return Components;
+  }
+
+  /*
+   * Convenient shortcuts to the standard Components abbreviations.
+   */
+  get Cc() {
+    return WrapPrivileged.wrap(this.getFullComponents().classes);
+  }
+  get Ci() {
+    return WrapPrivileged.wrap(this.getFullComponents().interfaces);
+  }
+  get Cu() {
+    return WrapPrivileged.wrap(this.getFullComponents().utils);
+  }
+  get Cr() {
+    return WrapPrivileged.wrap(this.getFullComponents().results);
+  }
+
+  getDOMWindowUtils(aWindow) {
+    if (aWindow == this.contentWindow && this.DOMWindowUtils != null) {
+      return this.DOMWindowUtils;
+    }
+
+    return bindDOMWindowUtils(aWindow);
+  }
+
+  async toggleMuteState(aMuted, aWindow) {
+    let actor = aWindow
+      ? aWindow.getWindowGlobalChild().getActor("SpecialPowers")
+      : this;
+    return actor.sendQuery("SPToggleMuteAudio", { mute: aMuted });
+  }
+
+  /*
+   * A method to get a DOMParser that can't parse XUL.
+   */
+  getNoXULDOMParser() {
+    // If we create it with a system subject principal (so it gets a
+    // nullprincipal), it won't be able to parse XUL by default.
+    return WrapPrivileged.wrap(new DOMParser());
+  }
+
+  get InspectorUtils() {
+    return WrapPrivileged.wrap(InspectorUtils);
+  }
+
+  get PromiseDebugging() {
+    return WrapPrivileged.wrap(PromiseDebugging);
+  }
+
+  async waitForCrashes(aExpectingProcessCrash) {
+    if (!aExpectingProcessCrash) {
+      return;
+    }
+
+    var crashIds = this._encounteredCrashDumpFiles
+      .filter(filename => {
+        return filename.length === 40 && filename.endsWith(".dmp");
+      })
+      .map(id => {
+        return id.slice(0, -4); // Strip the .dmp extension to get the ID
+      });
+
+    await this.sendQuery("SPProcessCrashManagerWait", {
+      crashIds,
+    });
+  }
+
+  async removeExpectedCrashDumpFiles(aExpectingProcessCrash) {
+    var success = true;
+    if (aExpectingProcessCrash) {
+      var message = {
+        op: "delete-crash-dump-files",
+        filenames: this._encounteredCrashDumpFiles,
+      };
+      if (!(await this.sendQuery("SPProcessCrashService", message))) {
+        success = false;
+      }
+    }
+    this._encounteredCrashDumpFiles.length = 0;
+    return success;
+  }
+
+  async findUnexpectedCrashDumpFiles() {
+    var self = this;
+    var message = {
+      op: "find-crash-dump-files",
+      crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles,
+    };
+    var crashDumpFiles = await this.sendQuery("SPProcessCrashService", message);
+    crashDumpFiles.forEach(function(aFilename) {
+      self._unexpectedCrashDumpFiles[aFilename] = true;
+    });
+    return crashDumpFiles;
+  }
+
+  removePendingCrashDumpFiles() {
+    var message = {
+      op: "delete-pending-crash-dump-files",
+    };
+    return this.sendQuery("SPProcessCrashService", message);
+  }
+
+  _setTimeout(callback) {
+    // for mochitest-browser
+    if (typeof this.chromeWindow != "undefined") {
+      this.chromeWindow.setTimeout(callback, 0);
+    }
+    // for mochitest-plain
+    else {
+      this.contentWindow.setTimeout(callback, 0);
+    }
+  }
+
+  promiseTimeout(delay) {
+    return new Promise(resolve => {
+      this._setTimeout(resolve, delay);
+    });
+  }
+
+  _delayCallbackTwice(callback) {
+    let delayedCallback = () => {
+      let delayAgain = aCallback => {
+        // Using this._setTimeout doesn't work here
+        // It causes failures in mochtests that use
+        // multiple pushPrefEnv calls
+        // For chrome/browser-chrome mochitests
+        this._setTimeout(aCallback);
+      };
+      delayAgain(delayAgain.bind(this, callback));
+    };
+    return delayedCallback;
+  }
+
+  /* apply permissions to the system and when the test case is finished (SimpleTest.finish())
+     we will revert the permission back to the original.
+
+     inPermissions is an array of objects where each object has a type, action, context, ex:
+     [{'type': 'SystemXHR', 'allow': 1, 'context': document},
+      {'type': 'SystemXHR', 'allow': Ci.nsIPermissionManager.PROMPT_ACTION, 'context': document}]
+
+     Allow can be a boolean value of true/false or ALLOW_ACTION/DENY_ACTION/PROMPT_ACTION/UNKNOWN_ACTION
+  */
+  async pushPermissions(inPermissions, callback) {
+    inPermissions = Cu.waiveXrays(inPermissions);
+    var pendingPermissions = [];
+    var cleanupPermissions = [];
+
+    for (var p in inPermissions) {
+      var permission = inPermissions[p];
+      var originalValue = Ci.nsIPermissionManager.UNKNOWN_ACTION;
+      var context = Cu.unwaiveXrays(permission.context); // Sometimes |context| is a DOM object on which we expect
+      // to be able to access .nodePrincipal, so we need to unwaive.
+      if (
+        await this.testPermission(
+          permission.type,
+          Ci.nsIPermissionManager.ALLOW_ACTION,
+          context
+        )
+      ) {
+        originalValue = Ci.nsIPermissionManager.ALLOW_ACTION;
+      } else if (
+        await this.testPermission(
+          permission.type,
+          Ci.nsIPermissionManager.DENY_ACTION,
+          context
+        )
+      ) {
+        originalValue = Ci.nsIPermissionManager.DENY_ACTION;
+      } else if (
+        await this.testPermission(
+          permission.type,
+          Ci.nsIPermissionManager.PROMPT_ACTION,
+          context
+        )
+      ) {
+        originalValue = Ci.nsIPermissionManager.PROMPT_ACTION;
+      } else if (
+        await this.testPermission(
+          permission.type,
+          Ci.nsICookiePermission.ACCESS_SESSION,
+          context
+        )
+      ) {
+        originalValue = Ci.nsICookiePermission.ACCESS_SESSION;
+      }
+
+      let principal = this._getPrincipalFromArg(context);
+      if (principal.isSystemPrincipal) {
+        continue;
+      }
+
+      let perm;
+      if (typeof permission.allow !== "boolean") {
+        perm = permission.allow;
+      } else {
+        perm = permission.allow
+          ? Ci.nsIPermissionManager.ALLOW_ACTION
+          : Ci.nsIPermissionManager.DENY_ACTION;
+      }
+
+      if (permission.remove) {
+        perm = Ci.nsIPermissionManager.UNKNOWN_ACTION;
+      }
+
+      if (originalValue == perm) {
+        continue;
+      }
+
+      var todo = {
+        op: "add",
+        type: permission.type,
+        permission: perm,
+        value: perm,
+        principal,
+        expireType:
+          typeof permission.expireType === "number" ? permission.expireType : 0, // default: EXPIRE_NEVER
+        expireTime:
+          typeof permission.expireTime === "number" ? permission.expireTime : 0,
+      };
+
+      var cleanupTodo = Object.assign({}, todo);
+
+      if (permission.remove) {
+        todo.op = "remove";
+      }
+
+      pendingPermissions.push(todo);
+
+      /* Push original permissions value or clear into cleanup array */
+      if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
+        cleanupTodo.op = "remove";
+      } else {
+        cleanupTodo.value = originalValue;
+        cleanupTodo.permission = originalValue;
+      }
+      cleanupPermissions.push(cleanupTodo);
+    }
+
+    if (pendingPermissions.length > 0) {
+      // The callback needs to be delayed twice. One delay is because the pref
+      // service doesn't guarantee the order it calls its observers in, so it
+      // may notify the observer holding the callback before the other
+      // observers have been notified and given a chance to make the changes
+      // that the callback checks for. The second delay is because pref
+      // observers often defer making their changes by posting an event to the
+      // event loop.
+      if (!this._observingPermissions) {
+        this._observingPermissions = true;
+        // If specialpowers is in main-process, then we can add a observer
+        // to get all 'perm-changed' signals. Otherwise, it can't receive
+        // all signals, so we register a observer in specialpowersobserver(in
+        // main-process) and get signals from it.
+        if (this.isMainProcess()) {
+          this.permissionObserverProxy._specialPowersAPI = this;
+          Services.obs.addObserver(
+            this.permissionObserverProxy,
+            "perm-changed"
+          );
+        } else {
+          this.registerObservers("perm-changed");
+          // bind() is used to set 'this' to SpecialPowersAPI itself.
+          this._addMessageListener(
+            "specialpowers-perm-changed",
+            this.permChangedProxy.bind(this)
+          );
+        }
+      }
+      this._permissionsUndoStack.push(cleanupPermissions);
+      this._pendingPermissions.push([
+        pendingPermissions,
+        this._delayCallbackTwice(callback),
+      ]);
+      this._applyPermissions();
+    } else {
+      this._setTimeout(callback);
+    }
+  }
+
+  /*
+   * This function should be used when specialpowers is in content process but
+   * it want to get the notification from chrome space.
+   *
+   * This function will call Services.obs.addObserver in SpecialPowersObserver
+   * (that is in chrome process) and forward the data received to SpecialPowers
+   * via messageManager.
+   * You can use this._addMessageListener("specialpowers-YOUR_TOPIC") to fire
+   * the callback.
+   *
+   * To get the expected data, you should modify
+   * SpecialPowersObserver.prototype._registerObservers.observe. Or the message
+   * you received from messageManager will only contain 'aData' from Service.obs.
+   */
+  registerObservers(topic) {
+    var msg = {
+      op: "add",
+      observerTopic: topic,
+    };
+    return this.sendQuery("SPObserverService", msg);
+  }
+
+  permChangedProxy(aMessage) {
+    let permission = aMessage.json.permission;
+    let aData = aMessage.json.aData;
+    this._permissionObserver.observe(permission, aData);
+  }
+
+  popPermissions(callback) {
+    let promise = new Promise(resolve => {
+      if (this._permissionsUndoStack.length > 0) {
+        // See pushPermissions comment regarding delay.
+        let cb = this._delayCallbackTwice(resolve);
+        /* Each pop from the stack will yield an object {op/type/permission/value/url/appid/isInIsolatedMozBrowserElement} or null */
+        this._pendingPermissions.push([this._permissionsUndoStack.pop(), cb]);
+        this._applyPermissions();
+      } else {
+        if (this._observingPermissions) {
+          this._observingPermissions = false;
+          this._removeMessageListener(
+            "specialpowers-perm-changed",
+            this.permChangedProxy.bind(this)
+          );
+        }
+        this._setTimeout(resolve);
+      }
+    });
+    if (callback) {
+      promise.then(callback);
+    }
+    return promise;
+  }
+
+  flushPermissions(callback) {
+    while (this._permissionsUndoStack.length > 1) {
+      this.popPermissions(null);
+    }
+
+    return this.popPermissions(callback);
+  }
+
+  setTestPluginEnabledState(newEnabledState, pluginName) {
+    return this.sendQuery("SPSetTestPluginEnabledState", {
+      newEnabledState,
+      pluginName,
+    });
+  }
+
+  /*
+    Iterate through one atomic set of permissions actions and perform allow/deny as appropriate.
+    All actions performed must modify the relevant permission.
+  */
+  _applyPermissions() {
+    if (this._applyingPermissions || this._pendingPermissions.length <= 0) {
+      return;
+    }
+
+    /* Set lock and get prefs from the _pendingPrefs queue */
+    this._applyingPermissions = true;
+    var transaction = this._pendingPermissions.shift();
+    var pendingActions = transaction[0];
+    var callback = transaction[1];
+    var lastPermission = pendingActions[pendingActions.length - 1];
+
+    var self = this;
+    this._permissionObserver._self = self;
+    this._permissionObserver._lastPermission = lastPermission;
+    this._permissionObserver._callback = callback;
+    this._permissionObserver._nextCallback = function() {
+      self._applyingPermissions = false;
+      // Now apply any permissions that may have been queued while we were applying
+      self._applyPermissions();
+    };
+
+    for (var idx in pendingActions) {
+      var perm = pendingActions[idx];
+      this.sendAsyncMessage("SPPermissionManager", perm);
+    }
+  }
+
+  async pushPrefEnv(inPrefs, callback = null) {
+    await this.sendQuery("PushPrefEnv", inPrefs).then(callback);
+    await this.promiseTimeout(0);
+  }
+
+  async popPrefEnv(callback = null) {
+    await this.sendQuery("PopPrefEnv").then(callback);
+    await this.promiseTimeout(0);
+  }
+
+  async flushPrefEnv(callback = null) {
+    await this.sendQuery("FlushPrefEnv").then(callback);
+    await this.promiseTimeout(0);
+  }
+
+  _addObserverProxy(notification) {
+    if (notification in this._proxiedObservers) {
+      this._addMessageListener(
+        notification,
+        this._proxiedObservers[notification]
+      );
+    }
+  }
+  _removeObserverProxy(notification) {
+    if (notification in this._proxiedObservers) {
+      this._removeMessageListener(
+        notification,
+        this._proxiedObservers[notification]
+      );
+    }
+  }
+
+  addObserver(obs, notification, weak) {
+    // Make sure the parent side exists, or we won't get any notifications.
+    this.sendAsyncMessage("Wakeup");
+
+    this._addObserverProxy(notification);
+    obs = Cu.waiveXrays(obs);
+    if (
+      typeof obs == "object" &&
+      obs.observe.name != "SpecialPowersCallbackWrapper"
+    ) {
+      obs.observe = WrapPrivileged.wrapCallback(obs.observe);
+    }
+    Services.obs.addObserver(obs, notification, weak);
+  }
+  removeObserver(obs, notification) {
+    this._removeObserverProxy(notification);
+    Services.obs.removeObserver(Cu.waiveXrays(obs), notification);
+  }
+  notifyObservers(subject, topic, data) {
+    Services.obs.notifyObservers(subject, topic, data);
+  }
+
+  /**
+   * An async observer is useful if you're listening for a
+   * notification that normally is only used by C++ code or chrome
+   * code (so it runs in the SystemGroup), but we need to know about
+   * it for a test (which runs as web content). If we used
+   * addObserver, we would assert when trying to enter web content
+   * from a runnabled labeled by the SystemGroup. An async observer
+   * avoids this problem.
+   */
+  addAsyncObserver(obs, notification, weak) {
+    obs = Cu.waiveXrays(obs);
+    if (
+      typeof obs == "object" &&
+      obs.observe.name != "SpecialPowersCallbackWrapper"
+    ) {
+      obs.observe = WrapPrivileged.wrapCallback(obs.observe);
+    }
+    let asyncObs = (...args) => {
+      Services.tm.dispatchToMainThread(() => {
+        if (typeof obs == "function") {
+          obs(...args);
+        } else {
+          obs.observe.call(undefined, ...args);
+        }
+      });
+    };
+    this._asyncObservers.set(obs, asyncObs);
+    Services.obs.addObserver(asyncObs, notification, weak);
+  }
+  removeAsyncObserver(obs, notification) {
+    let asyncObs = this._asyncObservers.get(Cu.waiveXrays(obs));
+    Services.obs.removeObserver(asyncObs, notification);
+  }
+
+  can_QI(obj) {
+    return obj.QueryInterface !== undefined;
+  }
+  do_QueryInterface(obj, iface) {
+    return obj.QueryInterface(Ci[iface]);
+  }
+
+  call_Instanceof(obj1, obj2) {
+    obj1 = WrapPrivileged.unwrap(obj1);
+    obj2 = WrapPrivileged.unwrap(obj2);
+    return obj1 instanceof obj2;
+  }
+
+  // Returns a privileged getter from an object. GetOwnPropertyDescriptor does
+  // not work here because xray wrappers don't properly implement it.
+  //
+  // This terribleness is used by dom/base/test/test_object.html because
+  // <object> and <embed> tags will spawn plugins if their prototype is touched,
+  // so we need to get and cache the getter of |hasRunningPlugin| if we want to
+  // call it without paradoxically spawning the plugin.
+  do_lookupGetter(obj, name) {
+    return Object.prototype.__lookupGetter__.call(obj, name);
+  }
+
+  // Mimic the get*Pref API
+  getBoolPref(...args) {
+    return Services.prefs.getBoolPref(...args);
+  }
+  getIntPref(...args) {
+    return Services.prefs.getIntPref(...args);
+  }
+  getCharPref(...args) {
+    return Services.prefs.getCharPref(...args);
+  }
+  getComplexValue(prefName, iid) {
+    return Services.prefs.getComplexValue(prefName, iid);
+  }
+
+  getParentBoolPref(prefName, defaultValue) {
+    return this._getParentPref(prefName, "BOOL", { defaultValue });
+  }
+  getParentIntPref(prefName, defaultValue) {
+    return this._getParentPref(prefName, "INT", { defaultValue });
+  }
+  getParentCharPref(prefName, defaultValue) {
+    return this._getParentPref(prefName, "CHAR", { defaultValue });
+  }
+
+  // Mimic the set*Pref API
+  setBoolPref(prefName, value) {
+    return this._setPref(prefName, "BOOL", value);
+  }
+  setIntPref(prefName, value) {
+    return this._setPref(prefName, "INT", value);
+  }
+  setCharPref(prefName, value) {
+    return this._setPref(prefName, "CHAR", value);
+  }
+  setComplexValue(prefName, iid, value) {
+    return this._setPref(prefName, "COMPLEX", value, iid);
+  }
+
+  // Mimic the clearUserPref API
+  clearUserPref(prefName) {
+    let msg = {
+      op: "clear",
+      prefName,
+      prefType: "",
+    };
+    return this.sendQuery("SPPrefService", msg);
+  }
+
+  // Private pref functions to communicate to chrome
+  async _getParentPref(prefName, prefType, { defaultValue, iid }) {
+    let msg = {
+      op: "get",
+      prefName,
+      prefType,
+      iid, // Only used with complex prefs
+      defaultValue, // Optional default value
+    };
+    let val = await this.sendQuery("SPPrefService", msg);
+    if (val == null) {
+      throw new Error(`Error getting pref '${prefName}'`);
+    }
+    return val;
+  }
+  _getPref(prefName, prefType, { defaultValue }) {
+    switch (prefType) {
+      case "BOOL":
+        return Services.prefs.getBoolPref(prefName);
+      case "INT":
+        return Services.prefs.getIntPref(prefName);
+      case "CHAR":
+        return Services.prefs.getCharPref(prefName);
+    }
+    return undefined;
+  }
+  _setPref(prefName, prefType, prefValue, iid) {
+    let msg = {
+      op: "set",
+      prefName,
+      prefType,
+      iid, // Only used with complex prefs
+      prefValue,
+    };
+    return this.sendQuery("SPPrefService", msg);
+  }
+
+  _getMUDV(window) {
+    return window.docShell.contentViewer;
+  }
+  // XXX: these APIs really ought to be removed, they're not e10s-safe.
+  // (also they're pretty Firefox-specific)
+  _getTopChromeWindow(window) {
+    return window.docShell.rootTreeItem.domWindow;
+  }
+  _getAutoCompletePopup(window) {
+    return this._getTopChromeWindow(window).document.getElementById(
+      "PopupAutoComplete"
+    );
+  }
+  addAutoCompletePopupEventListener(window, eventname, listener) {
+    this._getAutoCompletePopup(window).addEventListener(eventname, listener);
+  }
+  removeAutoCompletePopupEventListener(window, eventname, listener) {
+    this._getAutoCompletePopup(window).removeEventListener(eventname, listener);
+  }
+  get formHistory() {
+    let tmp = {};
+    ChromeUtils.import("resource://gre/modules/FormHistory.jsm", tmp);
+    return WrapPrivileged.wrap(tmp.FormHistory);
+  }
+  getFormFillController(window) {
+    return Cc["@mozilla.org/satchel/form-fill-controller;1"].getService(
+      Ci.nsIFormFillController
+    );
+  }
+  attachFormFillControllerTo(window) {
+    this.getFormFillController().attachPopupElementToBrowser(
+      window.docShell,
+      this._getAutoCompletePopup(window)
+    );
+  }
+  detachFormFillControllerFrom(window) {
+    this.getFormFillController().detachFromBrowser(window.docShell);
+  }
+  isBackButtonEnabled(window) {
+    return !this._getTopChromeWindow(window)
+      .document.getElementById("Browser:Back")
+      .hasAttribute("disabled");
+  }
+  // XXX end of problematic APIs
+
+  addChromeEventListener(type, listener, capture, allowUntrusted) {
+    this.docShell.chromeEventHandler.addEventListener(
+      type,
+      listener,
+      capture,
+      allowUntrusted
+    );
+  }
+  removeChromeEventListener(type, listener, capture) {
+    this.docShell.chromeEventHandler.removeEventListener(
+      type,
+      listener,
+      capture
+    );
+  }
+
+  // Note: each call to registerConsoleListener MUST be paired with a
+  // call to postConsoleSentinel; when the callback receives the
+  // sentinel it will unregister itself (_after_ calling the
+  // callback).  SimpleTest.expectConsoleMessages does this for you.
+  // If you register more than one console listener, a call to
+  // postConsoleSentinel will zap all of them.
+  registerConsoleListener(callback) {
+    let listener = new SPConsoleListener(callback);
+    Services.console.registerListener(listener);
+
+    // listen for dom/console events as well
+    Services.obs.addObserver(listener, "console-api-log-event");
+  }
+  postConsoleSentinel() {
+    Services.console.logStringMessage("SENTINEL");
+  }
+  resetConsole() {
+    Services.console.reset();
+  }
+
+  getFullZoom(window) {
+    return this._getMUDV(window).fullZoom;
+  }
+  getDeviceFullZoom(window) {
+    return this._getMUDV(window).deviceFullZoom;
+  }
+  setFullZoom(window, zoom) {
+    this._getMUDV(window).fullZoom = zoom;
+  }
+  getTextZoom(window) {
+    return this._getMUDV(window).textZoom;
+  }
+  setTextZoom(window, zoom) {
+    this._getMUDV(window).textZoom = zoom;
+  }
+
+  getOverrideDPPX(window) {
+    return this._getMUDV(window).overrideDPPX;
+  }
+  setOverrideDPPX(window, dppx) {
+    this._getMUDV(window).overrideDPPX = dppx;
+  }
+
+  emulateMedium(window, mediaType) {
+    this._getMUDV(window).emulateMedium(mediaType);
+  }
+  stopEmulatingMedium(window) {
+    this._getMUDV(window).stopEmulatingMedium();
+  }
+
+  // Takes a snapshot of the given window and returns a <canvas>
+  // containing the image. When the window is same-process, the canvas
+  // is returned synchronously. When it is out-of-process (or when a
+  // BrowsingContext or FrameLoaderOwner is passed instead of a Window),
+  // a promise which resolves to such a canvas is returned instead.
+  snapshotWindowWithOptions(content, rect, bgcolor, options) {
+    function getImageData(rect, bgcolor, options) {
+      let el = content.document.createElementNS(
+        "http://www.w3.org/1999/xhtml",
+        "canvas"
+      );
+      if (rect === undefined) {
+        rect = {
+          top: content.scrollY,
+          left: content.scrollX,
+          width: content.innerWidth,
+          height: content.innerHeight,
+        };
+      }
+      if (bgcolor === undefined) {
+        bgcolor = "rgb(255,255,255)";
+      }
+      if (options === undefined) {
+        options = {};
+      }
+
+      el.width = rect.width;
+      el.height = rect.height;
+      let ctx = el.getContext("2d");
+
+      let flags = 0;
+      for (let option in options) {
+        flags |= options[option] && ctx[option];
+      }
+
+      ctx.drawWindow(
+        content,
+        rect.left,
+        rect.top,
+        rect.width,
+        rect.height,
+        bgcolor,
+        flags
+      );
+
+      return ctx.getImageData(0, 0, el.width, el.height);
+    }
+
+    let toCanvas = imageData => {
+      let el = this.document.createElementNS(
+        "http://www.w3.org/1999/xhtml",
+        "canvas"
+      );
+      el.width = imageData.width;
+      el.height = imageData.height;
+
+      if (ImageData.isInstance(imageData)) {
+        let ctx = el.getContext("2d");
+        ctx.putImageData(imageData, 0, 0);
+      }
+
+      return el;
+    };
+
+    if (Window.isInstance(content)) {
+      // Hack around tests that try to snapshot 0 width or height
+      // elements.
+      if (rect && !(rect.width && rect.height)) {
+        return toCanvas(rect);
+      }
+
+      // This is an in-process window. Snapshot it synchronously.
+      return toCanvas(getImageData(rect, bgcolor, options));
+    }
+
+    // This is a remote window or frame. Snapshot it asynchronously and
+    // return a promise for the result. Alas, consumers expect us to
+    // return a <canvas> element rather than an ImageData object, so we
+    // need to convert the result from the remote snapshot to a local
+    // canvas.
+    return this.spawn(content, [rect, bgcolor, options], getImageData).then(
+      toCanvas
+    );
+  }
+
+  snapshotWindow(win, withCaret, rect, bgcolor) {
+    return this.snapshotWindowWithOptions(win, rect, bgcolor, {
+      DRAWWINDOW_DRAW_CARET: withCaret,
+    });
+  }
+
+  snapshotRect(win, rect, bgcolor) {
+    return this.snapshotWindowWithOptions(win, rect, bgcolor);
+  }
+
+  gc() {
+    this.DOMWindowUtils.garbageCollect();
+  }
+
+  forceGC() {
+    Cu.forceGC();
+  }
+
+  forceShrinkingGC() {
+    Cu.forceShrinkingGC();
+  }
+
+  forceCC() {
+    Cu.forceCC();
+  }
+
+  finishCC() {
+    Cu.finishCC();
+  }
+
+  ccSlice(budget) {
+    Cu.ccSlice(budget);
+  }
+
+  // Due to various dependencies between JS objects and C++ objects, an ordinary
+  // forceGC doesn't necessarily clear all unused objects, thus the GC and CC
+  // needs to run several times and when no other JS is running.
+  // The current number of iterations has been determined according to massive
+  // cross platform testing.
+  exactGC(callback) {
+    let count = 0;
+
+    function genGCCallback(cb) {
+      return function() {
+        Cu.forceCC();
+        if (++count < 3) {
+          Cu.schedulePreciseGC(genGCCallback(cb));
+        } else if (cb) {
+          cb();
+        }
+      };
+    }
+
+    Cu.schedulePreciseGC(genGCCallback(callback));
+  }
+
+  nondeterministicGetWeakMapKeys(m) {
+    return ChromeUtils.nondeterministicGetWeakMapKeys(m);
+  }
+
+  getMemoryReports() {
+    try {
+      Cc["@mozilla.org/memory-reporter-manager;1"]
+        .getService(Ci.nsIMemoryReporterManager)
+        .getReports(() => {}, null, () => {}, null, false);
+    } catch (e) {}
+  }
+
+  setGCZeal(zeal) {
+    Cu.setGCZeal(zeal);
+  }
+
+  isMainProcess() {
+    try {
+      return (
+        Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+      );
+    } catch (e) {}
+    return true;
+  }
+
+  get XPCOMABI() {
+    if (this._xpcomabi != null) {
+      return this._xpcomabi;
+    }
+
+    var xulRuntime = Services.appinfo.QueryInterface(Ci.nsIXULRuntime);
+
+    this._xpcomabi = xulRuntime.XPCOMABI;
+    return this._xpcomabi;
+  }
+
+  // The optional aWin parameter allows the caller to specify a given window in
+  // whose scope the runnable should be dispatched. If aFun throws, the
+  // exception will be reported to aWin.
+  executeSoon(aFun, aWin) {
+    // Create the runnable in the scope of aWin to avoid running into COWs.
+    var runnable = {};
+    if (aWin) {
+      runnable = Cu.createObjectIn(aWin);
+    }
+    runnable.run = aFun;
+    Cu.dispatch(runnable, aWin);
+  }
+
+  get OS() {
+    if (this._os != null) {
+      return this._os;
+    }
+
+    this._os = Services.appinfo.OS;
+    return this._os;
+  }
+
+  get useRemoteSubframes() {
+    return this.docShell.nsILoadContext.useRemoteSubframes;
+  }
+
+  addSystemEventListener(target, type, listener, useCapture) {
+    Services.els.addSystemEventListener(target, type, listener, useCapture);
+  }
+  removeSystemEventListener(target, type, listener, useCapture) {
+    Services.els.removeSystemEventListener(target, type, listener, useCapture);
+  }
+
+  // helper method to check if the event is consumed by either default group's
+  // event listener or system group's event listener.
+  defaultPreventedInAnyGroup(event) {
+    // FYI: Event.defaultPrevented returns false in content context if the
+    //      event is consumed only by system group's event listeners.
+    return event.defaultPrevented;
+  }
+
+  getDOMRequestService() {
+    var serv = Services.DOMRequest;
+    var res = {};
+    var props = [
+      "createRequest",
+      "createCursor",
+      "fireError",
+      "fireSuccess",
+      "fireDone",
+      "fireDetailedError",
+    ];
+    for (var i in props) {
+      let prop = props[i];
+      res[prop] = function() {
+        return serv[prop].apply(serv, arguments);
+      };
+    }
+    return res;
+  }
+
+  addCategoryEntry(category, entry, value, persists, replace) {
+    Services.catMan.addCategoryEntry(category, entry, value, persists, replace);
+  }
+
+  deleteCategoryEntry(category, entry, persists) {
+    Services.catMan.deleteCategoryEntry(category, entry, persists);
+  }
+  openDialog(win, args) {
+    return win.openDialog.apply(win, args);
+  }
+  // This is a blocking call which creates and spins a native event loop
+  spinEventLoop(win) {
+    // simply do a sync XHR back to our windows location.
+    var syncXHR = new win.XMLHttpRequest();
+    syncXHR.open("GET", win.location, false);
+    syncXHR.send();
+  }
+
+  // :jdm gets credit for this.  ex: getPrivilegedProps(window, 'location.href');
+  getPrivilegedProps(obj, props) {
+    var parts = props.split(".");
+    for (var i = 0; i < parts.length; i++) {
+      var p = parts[i];
+      if (obj[p] != undefined) {
+        obj = obj[p];
+      } else {
+        return null;
+      }
+    }
+    return obj;
+  }
+
+  _browsingContextForTarget(target) {
+    if (BrowsingContext.isInstance(target)) {
+      return target;
+    }
+    if (Element.isInstance(target)) {
+      return target.browsingContext;
+    }
+
+    return BrowsingContext.getFromWindow(target);
+  }
+
+  /**
+   * Runs a task in the context of the given frame, and returns a
+   * promise which resolves to the return value of that task.
+   *
+   * The given frame may be in-process or out-of-process. Either way,
+   * the task will run asynchronously, in a sandbox with access to the
+   * frame's content window via its `content` global. Any arguments
+   * passed will be copied via structured clone, as will its return
+   * value.
+   *
+   * The sandbox also has access to an Assert object, as provided by
+   * Assert.jsm. Any assertion methods called before the task resolves
+   * will be relayed back to the test environment of the caller.
+   *
+   * @param {BrowsingContext or FrameLoaderOwner or WindowProxy} target
+   *        The target in which to run the task. This may be any element
+   *        which implements the FrameLoaderOwner interface (including
+   *        HTML <iframe> elements and XUL <browser> elements) or a
+   *        WindowProxy (either in-process or remote).
+   * @param {Array<any>} args
+   *        An array of arguments to pass to the task. All arguments
+   *        must be structured clone compatible, and will be cloned
+   *        before being passed to the task.
+   * @param {function} task
+   *        The function to run in the context of the target. The
+   *        function will be stringified and re-evaluated in the context
+   *        of the target's content window. It may return any structured
+   *        clone compatible value, or a Promise which resolves to the
+   *        same, which will be returned to the caller.
+   *
+   * @returns {Promise<any>}
+   *        A promise which resolves to the return value of the task, or
+   *        which rejects if the task raises an exception. As this is
+   *        being written, the rejection value will always be undefined
+   *        in the cases where the task throws an error, though that may
+   *        change in the future.
+   */
+  spawn(target, args, task) {
+    let browsingContext = this._browsingContextForTarget(target);
+
+    return this.sendQuery("Spawn", {
+      browsingContext,
+      args,
+      task: String(task),
+      caller: SpecialPowersSandbox.getCallerInfo(Components.stack.caller),
+      hasHarness: typeof this.SimpleTest === "object",
+    });
+  }
+
+  snapshotContext(target, rect, background) {
+    let browsingContext = this._browsingContextForTarget(target);
+
+    return this.sendQuery("Snapshot", {
+      browsingContext,
+      rect,
+      background,
+    }).then(imageData => {
+      return this.contentWindow.createImageBitmap(imageData);
+    });
+  }
+
+  _spawnTask(task, args, caller, taskId) {
+    let sb = new SpecialPowersSandbox(null, data => {
+      this.sendAsyncMessage("ProxiedAssert", { taskId, data });
+    });
+
+    sb.sandbox.SpecialPowers = this;
+    Object.defineProperty(sb.sandbox, "content", {
+      get: () => {
+        return this.contentWindow;
+      },
+      enumerable: true,
+    });
+
+    return sb.execute(task, args, caller);
+  }
+
+  get SimpleTest() {
+    return this._SimpleTest || this.contentWindow.wrappedJSObject.SimpleTest;
+  }
+  set SimpleTest(val) {
+    this._SimpleTest = val;
+  }
+
+  /**
+   * Sets this actor as the default assertion result handler for tasks
+   * which originate in a window without a test harness.
+   */
+  setAsDefaultAssertHandler() {
+    this.sendAsyncMessage("SetAsDefaultAssertHandler");
+  }
+
+  getFocusedElementForWindow(targetWindow, aDeep) {
+    var outParam = {};
+    Services.focus.getFocusedElementForWindow(targetWindow, aDeep, outParam);
+    return outParam.value;
+  }
+
+  get focusManager() {
+    return Services.focus;
+  }
+
+  activeWindow() {
+    return Services.focus.activeWindow;
+  }
+
+  focusedWindow() {
+    return Services.focus.focusedWindow;
+  }
+
+  focus(aWindow) {
+    // This is called inside TestRunner._makeIframe without aWindow, because of assertions in oop mochitests
+    // With aWindow, it is called in SimpleTest.waitForFocus to allow popup window opener focus switching
+    if (aWindow) {
+      aWindow.focus();
+    }
+
+    try {
+      let actor = aWindow
+        ? aWindow.getWindowGlobalChild().getActor("SpecialPowers")
+        : this;
+      actor.sendAsyncMessage("SpecialPowers.Focus", {});
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  }
+
+  getClipboardData(flavor, whichClipboard) {
+    if (whichClipboard === undefined) {
+      whichClipboard = Services.clipboard.kGlobalClipboard;
+    }
+
+    var xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+      Ci.nsITransferable
+    );
+    xferable.init(this.docShell);
+    xferable.addDataFlavor(flavor);
+    Services.clipboard.getData(xferable, whichClipboard);
+    var data = {};
+    try {
+      xferable.getTransferData(flavor, data);
+    } catch (e) {}
+    data = data.value || null;
+    if (data == null) {
+      return "";
+    }
+
+    return data.QueryInterface(Ci.nsISupportsString).data;
+  }
+
+  clipboardCopyString(str) {
+    Cc["@mozilla.org/widget/clipboardhelper;1"]
+      .getService(Ci.nsIClipboardHelper)
+      .copyString(str);
+  }
+
+  supportsSelectionClipboard() {
+    return Services.clipboard.supportsSelectionClipboard();
+  }
+
+  swapFactoryRegistration(cid, contractID, newFactory) {
+    newFactory = Cu.waiveXrays(newFactory);
+
+    var componentRegistrar = Components.manager.QueryInterface(
+      Ci.nsIComponentRegistrar
+    );
+
+    var currentCID = componentRegistrar.contractIDToCID(contractID);
+    var currentFactory = Components.manager.getClassObject(
+      Cc[contractID],
+      Ci.nsIFactory
+    );
+    if (cid) {
+      componentRegistrar.unregisterFactory(currentCID, currentFactory);
+    } else {
+      let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
+        Ci.nsIUUIDGenerator
+      );
+      cid = uuidGenerator.generateUUID();
+    }
+
+    // Restore the original factory.
+    componentRegistrar.registerFactory(cid, "", contractID, newFactory);
+    return { originalCID: currentCID };
+  }
+
+  _getElement(aWindow, id) {
+    return typeof id == "string" ? aWindow.document.getElementById(id) : id;
+  }
+
+  dispatchEvent(aWindow, target, event) {
+    var el = this._getElement(aWindow, target);
+    return el.dispatchEvent(event);
+  }
+
+  get isDebugBuild() {
+    return Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2)
+      .isDebugBuild;
+  }
+  assertionCount() {
+    var debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
+    return debugsvc.assertionCount;
+  }
+
+  /**
+   * Get the message manager associated with an <iframe mozbrowser>.
+   */
+  getBrowserFrameMessageManager(aFrameElement) {
+    return this.wrap(aFrameElement.frameLoader.messageManager);
+  }
+
+  _getPrincipalFromArg(arg) {
+    let principal;
+    let secMan = Services.scriptSecurityManager;
+
+    if (typeof arg == "string") {
+      // It's an URL.
+      let uri = Services.io.newURI(arg);
+      principal = secMan.createContentPrincipal(uri, {});
+    } else if (arg.nodePrincipal) {
+      // It's a document.
+      // In some tests the arg is a wrapped DOM element, so we unwrap it first.
+      principal = WrapPrivileged.unwrap(arg).nodePrincipal;
+    } else {
+      let uri = Services.io.newURI(arg.url);
+      let attrs = arg.originAttributes || {};
+      principal = secMan.createContentPrincipal(uri, attrs);
+    }
+
+    return principal;
+  }
+
+  async addPermission(type, allow, arg, expireType, expireTime) {
+    let principal = this._getPrincipalFromArg(arg);
+    if (principal.isSystemPrincipal) {
+      return; // nothing to do
+    }
+
+    let permission;
+    if (typeof allow !== "boolean") {
+      permission = allow;
+    } else {
+      permission = allow
+        ? Ci.nsIPermissionManager.ALLOW_ACTION
+        : Ci.nsIPermissionManager.DENY_ACTION;
+    }
+
+    var msg = {
+      op: "add",
+      type,
+      permission,
+      principal,
+      expireType: typeof expireType === "number" ? expireType : 0,
+      expireTime: typeof expireTime === "number" ? expireTime : 0,
+    };
+
+    await this.sendQuery("SPPermissionManager", msg);
+  }
+
+  async removePermission(type, arg) {
+    let principal = this._getPrincipalFromArg(arg);
+    if (principal.isSystemPrincipal) {
+      return; // nothing to do
+    }
+
+    var msg = {
+      op: "remove",
+      type,
+      principal,
+    };
+
+    await this.sendQuery("SPPermissionManager", msg);
+  }
+
+  async hasPermission(type, arg) {
+    let principal = this._getPrincipalFromArg(arg);
+    if (principal.isSystemPrincipal) {
+      return true; // system principals have all permissions
+    }
+
+    var msg = {
+      op: "has",
+      type,
+      principal,
+    };
+
+    return this.sendQuery("SPPermissionManager", msg);
+  }
+
+  async testPermission(type, value, arg) {
+    let principal = this._getPrincipalFromArg(arg);
+    if (principal.isSystemPrincipal) {
+      return true; // system principals have all permissions
+    }
+
+    var msg = {
+      op: "test",
+      type,
+      value,
+      principal,
+    };
+    return this.sendQuery("SPPermissionManager", msg);
+  }
+
+  isContentWindowPrivate(win) {
+    return PrivateBrowsingUtils.isContentWindowPrivate(win);
+  }
+
+  async notifyObserversInParentProcess(subject, topic, data) {
+    if (subject) {
+      throw new Error("Can't send subject to another process!");
+    }
+    if (this.isMainProcess()) {
+      this.notifyObservers(subject, topic, data);
+      return;
+    }
+    var msg = {
+      op: "notify",
+      observerTopic: topic,
+      observerData: data,
+    };
+    await this.sendQuery("SPObserverService", msg);
+  }
+
+  removeAllServiceWorkerData() {
+    return this.sendQuery("SPRemoveAllServiceWorkers", {});
+  }
+
+  removeServiceWorkerDataForExampleDomain() {
+    return this.sendQuery("SPRemoveServiceWorkerDataForExampleDomain", {});
+  }
+
+  cleanUpSTSData(origin, flags) {
+    return this.sendQuery("SPCleanUpSTSData", { origin, flags: flags || 0 });
+  }
+
+  async requestDumpCoverageCounters(cb) {
+    // We want to avoid a roundtrip between child and parent.
+    if (!PerTestCoverageUtils.enabled) {
+      return;
+    }
+
+    await this.sendQuery("SPRequestDumpCoverageCounters", {});
+  }
+
+  async requestResetCoverageCounters(cb) {
+    // We want to avoid a roundtrip between child and parent.
+    if (!PerTestCoverageUtils.enabled) {
+      return;
+    }
+    await this.sendQuery("SPRequestResetCoverageCounters", {});
+  }
+
+  loadExtension(ext, handler) {
+    if (this._extensionListeners == null) {
+      this._extensionListeners = new Set();
+
+      this._addMessageListener("SPExtensionMessage", msg => {
+        for (let listener of this._extensionListeners) {
+          try {
+            listener(msg);
+          } catch (e) {
+            Cu.reportError(e);
+          }
+        }
+      });
+    }
+
+    // Note, this is not the addon is as used by the AddonManager etc,
+    // this is just an identifier used for specialpowers messaging
+    // between this content process and the chrome process.
+    let id = this._nextExtensionID++;
+
+    handler = Cu.waiveXrays(handler);
+    ext = Cu.waiveXrays(ext);
+
+    let sp = this;
+    let state = "uninitialized";
+    let extension = {
+      get state() {
+        return state;
+      },
+
+      startup() {
+        state = "pending";
+        return sp.sendQuery("SPStartupExtension", { id }).then(
+          () => {
+            state = "running";
+          },
+          () => {
+            state = "failed";
+            sp._extensionListeners.delete(listener);
+            return Promise.reject("startup failed");
+          }
+        );
+      },
+
+      unload() {
+        state = "unloading";
+        return sp.sendQuery("SPUnloadExtension", { id }).finally(() => {
+          sp._extensionListeners.delete(listener);
+          state = "unloaded";
+        });
+      },
+
+      sendMessage(...args) {
+        sp.sendAsyncMessage("SPExtensionMessage", { id, args });
+      },
+    };
+
+    this.sendAsyncMessage("SPLoadExtension", { ext, id });
+
+    let listener = msg => {
+      if (msg.data.id == id) {
+        if (msg.data.type == "extensionSetId") {
+          extension.id = msg.data.args[0];
+          extension.uuid = msg.data.args[1];
+        } else if (msg.data.type in handler) {
+          handler[msg.data.type](
+            ...Cu.cloneInto(msg.data.args, this.contentWindow)
+          );
+        } else {
+          dump(`Unexpected: ${msg.data.type}\n`);
+        }
+      }
+    };
+
+    this._extensionListeners.add(listener);
+    return extension;
+  }
+
+  invalidateExtensionStorageCache() {
+    this.notifyObserversInParentProcess(
+      null,
+      "extension-invalidate-storage-cache",
+      ""
+    );
+  }
+
+  allowMedia(window, enable) {
+    window.docShell.allowMedia = enable;
+  }
+
+  createChromeCache(name, url) {
+    let principal = this._getPrincipalFromArg(url);
+    return WrapPrivileged.wrap(
+      new this.contentWindow.CacheStorage(name, principal)
+    );
+  }
+
+  loadChannelAndReturnStatus(url, loadUsingSystemPrincipal) {
+    const BinaryInputStream = Components.Constructor(
+      "@mozilla.org/binaryinputstream;1",
+      "nsIBinaryInputStream",
+      "setInputStream"
+    );
+
+    return new Promise(function(resolve) {
+      let listener = {
+        httpStatus: 0,
+
+        onStartRequest(request) {
+          request.QueryInterface(Ci.nsIHttpChannel);
+          this.httpStatus = request.responseStatus;
+        },
+
+        onDataAvailable(request, stream, offset, count) {
+          new BinaryInputStream(stream).readByteArray(count);
+        },
+
+        onStopRequest(request, status) {
+          /* testing here that the redirect was not followed. If it was followed
+            we would see a http status of 200 and status of NS_OK */
+
+          let httpStatus = this.httpStatus;
+          resolve({ status, httpStatus });
+        },
+      };
+      let uri = NetUtil.newURI(url);
+      let channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal });
+
+      channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
+      channel.QueryInterface(Ci.nsIHttpChannelInternal);
+      channel.documentURI = uri;
+      channel.asyncOpen(listener);
+    });
+  }
+
+  get ParserUtils() {
+    if (this._pu != null) {
+      return this._pu;
+    }
+
+    let pu = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
+    // We need to create and return our own wrapper.
+    this._pu = {
+      sanitize(src, flags) {
+        return pu.sanitize(src, flags);
+      },
+      convertToPlainText(src, flags, wrapCol) {
+        return pu.convertToPlainText(src, flags, wrapCol);
+      },
+      parseFragment(fragment, flags, isXML, baseURL, element) {
+        let baseURI = baseURL ? NetUtil.newURI(baseURL) : null;
+        return pu.parseFragment(
+          WrapPrivileged.unwrap(fragment),
+          flags,
+          isXML,
+          baseURI,
+          WrapPrivileged.unwrap(element)
+        );
+      },
+    };
+    return this._pu;
+  }
+
+  createDOMWalker(node, showAnonymousContent) {
+    node = WrapPrivileged.unwrap(node);
+    let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance(
+      Ci.inIDeepTreeWalker
+    );
+    walker.showAnonymousContent = showAnonymousContent;
+    walker.init(node.ownerDocument, NodeFilter.SHOW_ALL);
+    walker.currentNode = node;
+    return {
+      get firstChild() {
+        return WrapPrivileged.wrap(walker.firstChild());
+      },
+      get lastChild() {
+        return WrapPrivileged.wrap(walker.lastChild());
+      },
+    };
+  }
+
+  observeMutationEvents(mo, node, nativeAnonymousChildList, subtree) {
+    WrapPrivileged.unwrap(mo).observe(WrapPrivileged.unwrap(node), {
+      nativeAnonymousChildList,
+      subtree,
+    });
+  }
+
+  doCommand(window, cmd) {
+    return window.docShell.doCommand(cmd);
+  }
+
+  isCommandEnabled(window, cmd) {
+    return window.docShell.isCommandEnabled(cmd);
+  }
+
+  setCommandNode(window, node) {
+    return window.docShell.contentViewer
+      .QueryInterface(Ci.nsIContentViewerEdit)
+      .setCommandNode(node);
+  }
+
+  /* Bug 1339006 Runnables of nsIURIClassifier.classify may be labeled by
+   * SystemGroup, but some test cases may run as web content. That would assert
+   * when trying to enter web content from a runnable labeled by the
+   * SystemGroup. To avoid that, we run classify from SpecialPowers which is
+   * chrome-privileged and allowed to run inside SystemGroup
+   */
+
+  doUrlClassify(principal, eventTarget, callback) {
+    let classifierService = Cc[
+      "@mozilla.org/url-classifier/dbservice;1"
+    ].getService(Ci.nsIURIClassifier);
+
+    let wrapCallback = (...args) => {
+      Services.tm.dispatchToMainThread(() => {
+        if (typeof callback == "function") {
+          callback(...args);
+        } else {
+          callback.onClassifyComplete.call(undefined, ...args);
+        }
+      });
+    };
+
+    return classifierService.classify(
+      WrapPrivileged.unwrap(principal),
+      eventTarget,
+      wrapCallback
+    );
+  }
+
+  // TODO: Bug 1353701 - Supports custom event target for labelling.
+  doUrlClassifyLocal(uri, tables, callback) {
+    let classifierService = Cc[
+      "@mozilla.org/url-classifier/dbservice;1"
+    ].getService(Ci.nsIURIClassifier);
+
+    let wrapCallback = results => {
+      Services.tm.dispatchToMainThread(() => {
+        if (typeof callback == "function") {
+          callback(WrapPrivileged.wrap(results));
+        } else {
+          callback.onClassifyComplete.call(
+            undefined,
+            WrapPrivileged.wrap(results)
+          );
+        }
+      });
+    };
+
+    let feature = classifierService.createFeatureWithTables(
+      "test",
+      tables.split(","),
+      []
+    );
+    return classifierService.asyncClassifyLocalWithFeatures(
+      WrapPrivileged.unwrap(uri),
+      [feature],
+      Ci.nsIUrlClassifierFeature.blacklist,
+      wrapCallback
+    );
+  }
 }
+
+SpecialPowersChild.prototype._proxiedObservers = {
+  "specialpowers-http-notify-request": function(aMessage) {
+    let uri = aMessage.json.uri;
+    Services.obs.notifyObservers(
+      null,
+      "specialpowers-http-notify-request",
+      uri
+    );
+  },
+
+  "specialpowers-service-worker-shutdown": function(aMessage) {
+    Services.obs.notifyObservers(null, "specialpowers-service-worker-shutdown");
+  },
+};
+
+SpecialPowersChild.prototype.permissionObserverProxy = {
+  // 'this' in permChangedObserverProxy is the permChangedObserverProxy
+  // object itself. The '_specialPowersAPI' will be set to the 'SpecialPowersChild'
+  // object to call the member function in SpecialPowersChild.
+  _specialPowersAPI: null,
+  observe(aSubject, aTopic, aData) {
+    if (aTopic == "perm-changed") {
+      var permission = aSubject.QueryInterface(Ci.nsIPermission);
+      this._specialPowersAPI._permissionObserver.observe(permission, aData);
+    }
+  },
+};
+
+SpecialPowersChild.prototype._permissionObserver = {
+  _self: null,
+  _lastPermission: {},
+  _callBack: null,
+  _nextCallback: null,
+  _obsDataMap: {
+    deleted: "remove",
+    added: "add",
+  },
+  observe(permission, aData) {
+    if (this._self._applyingPermissions) {
+      if (permission.type == this._lastPermission.type) {
+        this._self._setTimeout(this._callback);
+        this._self._setTimeout(this._nextCallback);
+        this._callback = null;
+        this._nextCallback = null;
+      }
+    } else {
+      var found = false;
+      for (
+        var i = 0;
+        !found && i < this._self._permissionsUndoStack.length;
+        i++
+      ) {
+        var undos = this._self._permissionsUndoStack[i];
+        for (var j = 0; j < undos.length; j++) {
+          var undo = undos[j];
+          if (
+            undo.op == this._obsDataMap[aData] &&
+            undo.type == permission.type
+          ) {
+            // Remove this undo item if it has been done by others(not
+            // specialpowers itself.)
+            undos.splice(j, 1);
+            found = true;
+            break;
+          }
+        }
+        if (!undos.length) {
+          // Remove the empty row in permissionsUndoStack
+          this._self._permissionsUndoStack.splice(i, 1);
+        }
+      }
+    }
+  },
+};
+
+SpecialPowersChild.prototype.EARLY_BETA_OR_EARLIER =
+  AppConstants.EARLY_BETA_OR_EARLIER;
+
+// Due to an unfortunate accident of history, when this API was
+// subclassed using `Thing.prototype = new SpecialPowersChild()`, existing
+// code depends on all SpecialPowers instances using the same arrays for
+// these.
+Object.assign(SpecialPowersChild.prototype, {
+  _permissionsUndoStack: [],
+  _pendingPermissions: [],
+});
--- a/testing/specialpowers/content/SpecialPowersParent.jsm
+++ b/testing/specialpowers/content/SpecialPowersParent.jsm
@@ -1,30 +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/. */
+
 "use strict";
 
-// Based on:
-// https://bugzilla.mozilla.org/show_bug.cgi?id=549539
-// https://bug549539.bugzilla.mozilla.org/attachment.cgi?id=429661
-// https://developer.mozilla.org/en/XPCOM/XPCOM_changes_in_Gecko_1.9.3
-// https://developer.mozilla.org/en/how_to_build_an_xpcom_component_in_javascript
-
 var EXPORTED_SYMBOLS = ["SpecialPowersParent"];
 
+var { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-const { SpecialPowersAPIParent } = ChromeUtils.import(
-  "resource://specialpowers/SpecialPowersAPIParent.jsm"
-);
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ExtensionData: "resource://gre/modules/Extension.jsm",
+  ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm",
+  PerTestCoverageUtils: "resource://testing-common/PerTestCoverageUtils.jsm",
+  ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
+  SpecialPowersSandbox: "resource://specialpowers/SpecialPowersSandbox.jsm",
+  HiddenFrame: "resource://gre/modules/HiddenFrame.jsm",
+});
+
+class SpecialPowersError extends Error {
+  get name() {
+    return "SpecialPowersError";
+  }
+}
+
+function parseKeyValuePairs(text) {
+  var lines = text.split("\n");
+  var data = {};
+  for (let i = 0; i < lines.length; i++) {
+    if (lines[i] == "") {
+      continue;
+    }
+
+    // can't just .split() because the value might contain = characters
+    let eq = lines[i].indexOf("=");
+    if (eq != -1) {
+      let [key, value] = [
+        lines[i].substring(0, eq),
+        lines[i].substring(eq + 1),
+      ];
+      if (key && value) {
+        data[key] = value.replace(/\\n/g, "\n").replace(/\\\\/g, "\\");
+      }
+    }
+  }
+  return data;
+}
 
-class SpecialPowersParent extends SpecialPowersAPIParent {
+function parseKeyValuePairsFromFile(file) {
+  var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+    Ci.nsIFileInputStream
+  );
+  fstream.init(file, -1, 0, 0);
+  var is = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(
+    Ci.nsIConverterInputStream
+  );
+  is.init(
+    fstream,
+    "UTF-8",
+    1024,
+    Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER
+  );
+  var str = {};
+  var contents = "";
+  while (is.readString(4096, str) != 0) {
+    contents += str.value;
+  }
+  is.close();
+  fstream.close();
+  return parseKeyValuePairs(contents);
+}
+
+function getTestPlugin(pluginName) {
+  var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+  var tags = ph.getPluginTags();
+  var name = pluginName || "Test Plug-in";
+  for (var tag of tags) {
+    if (tag.name == name) {
+      return tag;
+    }
+  }
+
+  return null;
+}
+
+const PREF_TYPES = {
+  [Ci.nsIPrefBranch.PREF_INVALID]: "INVALID",
+  [Ci.nsIPrefBranch.PREF_INT]: "INT",
+  [Ci.nsIPrefBranch.PREF_BOOL]: "BOOL",
+  [Ci.nsIPrefBranch.PREF_STRING]: "CHAR",
+  number: "INT",
+  boolean: "BOOL",
+  string: "CHAR",
+};
+
+// We share a single preference environment stack between all
+// SpecialPowers instances, across all processes.
+let prefUndoStack = [];
+let inPrefEnvOp = false;
+
+function doPrefEnvOp(fn) {
+  if (inPrefEnvOp) {
+    throw new Error(
+      "Reentrant preference environment operations not supported"
+    );
+  }
+  inPrefEnvOp = true;
+  try {
+    return fn();
+  } finally {
+    inPrefEnvOp = false;
+  }
+}
+
+// Supplies the unique IDs for tasks created by SpecialPowers.spawn(),
+// used to bounce assertion messages back down to the correct child.
+let nextTaskID = 1;
+
+// The default actor to send assertions to if a task originated in a
+// window without a test harness.
+let defaultAssertHandler;
+
+class SpecialPowersParent extends JSWindowActorParent {
   constructor() {
     super();
+
     this._messageManager = Services.mm;
     this._serviceWorkerListener = null;
 
     this._observer = this.observe.bind(this);
 
     this.didDestroy = this.uninit.bind(this);
 
     this._registerObservers = {
@@ -56,27 +163,22 @@ class SpecialPowersParent extends Specia
           // fall through
           default:
             this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
         }
       },
     };
 
     this.init();
-  }
 
-  observe(aSubject, aTopic, aData) {
-    if (aTopic == "http-on-modify-request") {
-      if (aSubject instanceof Ci.nsIChannel) {
-        let uri = aSubject.URI.spec;
-        this.sendAsyncMessage("specialpowers-http-notify-request", { uri });
-      }
-    } else {
-      this._observe(aSubject, aTopic, aData);
-    }
+    this._crashDumpDir = null;
+    this._processCrashObserversRegistered = false;
+    this._chromeScriptListeners = [];
+    this._extensions = new Map();
+    this._taskActors = new Map();
   }
 
   init() {
     Services.obs.addObserver(this._observer, "http-on-modify-request");
 
     // We would like to check that tests don't leave service workers around
     // after they finish, but that information lives in the parent process.
     // Ideally, we'd be able to tell the child processes whenever service
@@ -97,29 +199,165 @@ class SpecialPowersParent extends Specia
       onUnregister() {
         // no-op
       },
     };
     swm.addListener(this._serviceWorkerListener);
   }
 
   uninit() {
+    if (defaultAssertHandler === this) {
+      defaultAssertHandler = null;
+    }
+
     var obs = Services.obs;
     obs.removeObserver(this._observer, "http-on-modify-request");
     this._registerObservers._topics.splice(0).forEach(element => {
       obs.removeObserver(this._registerObservers, element);
     });
     this._removeProcessCrashObservers();
 
     let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
       Ci.nsIServiceWorkerManager
     );
     swm.removeListener(this._serviceWorkerListener);
   }
 
+  observe(aSubject, aTopic, aData) {
+    function addDumpIDToMessage(propertyName) {
+      try {
+        var id = aSubject.getPropertyAsAString(propertyName);
+      } catch (ex) {
+        id = null;
+      }
+      if (id) {
+        message.dumpIDs.push({ id, extension: "dmp" });
+        message.dumpIDs.push({ id, extension: "extra" });
+      }
+    }
+
+    switch (aTopic) {
+      case "http-on-modify-request":
+        if (aSubject instanceof Ci.nsIChannel) {
+          let uri = aSubject.URI.spec;
+          this.sendAsyncMessage("specialpowers-http-notify-request", { uri });
+        }
+        break;
+
+      case "plugin-crashed":
+      case "ipc:content-shutdown":
+        var message = { type: "crash-observed", dumpIDs: [] };
+        aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2);
+        if (aTopic == "plugin-crashed") {
+          addDumpIDToMessage("pluginDumpID");
+          addDumpIDToMessage("browserDumpID");
+
+          let pluginID = aSubject.getPropertyAsAString("pluginDumpID");
+          let extra = this._getExtraData(pluginID);
+          if (extra && "additional_minidumps" in extra) {
+            let dumpNames = extra.additional_minidumps.split(",");
+            for (let name of dumpNames) {
+              message.dumpIDs.push({
+                id: pluginID + "-" + name,
+                extension: "dmp",
+              });
+            }
+          }
+        } else {
+          // ipc:content-shutdown
+          if (!aSubject.hasKey("abnormal")) {
+            return; // This is a normal shutdown, ignore it
+          }
+
+          addDumpIDToMessage("dumpID");
+        }
+        this.sendAsyncMessage("SPProcessCrashService", message);
+        break;
+    }
+  }
+
+  _getCrashDumpDir() {
+    if (!this._crashDumpDir) {
+      this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+      this._crashDumpDir.append("minidumps");
+    }
+    return this._crashDumpDir;
+  }
+
+  _getPendingCrashDumpDir() {
+    if (!this._pendingCrashDumpDir) {
+      this._pendingCrashDumpDir = Services.dirsvc.get("UAppData", Ci.nsIFile);
+      this._pendingCrashDumpDir.append("Crash Reports");
+      this._pendingCrashDumpDir.append("pending");
+    }
+    return this._pendingCrashDumpDir;
+  }
+
+  _getExtraData(dumpId) {
+    let extraFile = this._getCrashDumpDir().clone();
+    extraFile.append(dumpId + ".extra");
+    if (!extraFile.exists()) {
+      return null;
+    }
+    return parseKeyValuePairsFromFile(extraFile);
+  }
+
+  _deleteCrashDumpFiles(aFilenames) {
+    var crashDumpDir = this._getCrashDumpDir();
+    if (!crashDumpDir.exists()) {
+      return false;
+    }
+
+    var success = aFilenames.length != 0;
+    aFilenames.forEach(function(crashFilename) {
+      var file = crashDumpDir.clone();
+      file.append(crashFilename);
+      if (file.exists()) {
+        file.remove(false);
+      } else {
+        success = false;
+      }
+    });
+    return success;
+  }
+
+  _findCrashDumpFiles(aToIgnore) {
+    var crashDumpDir = this._getCrashDumpDir();
+    var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries;
+    if (!entries) {
+      return [];
+    }
+
+    var crashDumpFiles = [];
+    while (entries.hasMoreElements()) {
+      var file = entries.nextFile;
+      var path = String(file.path);
+      if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) {
+        crashDumpFiles.push(path);
+      }
+    }
+    return crashDumpFiles.concat();
+  }
+
+  _deletePendingCrashDumpFiles() {
+    var crashDumpDir = this._getPendingCrashDumpDir();
+    var removed = false;
+    if (crashDumpDir.exists()) {
+      let entries = crashDumpDir.directoryEntries;
+      while (entries.hasMoreElements()) {
+        let file = entries.nextFile;
+        if (file.isFile()) {
+          file.remove(false);
+          removed = true;
+        }
+      }
+    }
+    return removed;
+  }
+
   _addProcessCrashObservers() {
     if (this._processCrashObserversRegistered) {
       return;
     }
 
     Services.obs.addObserver(this._observer, "plugin-crashed");
     Services.obs.addObserver(this._observer, "ipc:content-shutdown");
     this._processCrashObserversRegistered = true;
@@ -130,30 +368,248 @@ class SpecialPowersParent extends Specia
       return;
     }
 
     Services.obs.removeObserver(this._observer, "plugin-crashed");
     Services.obs.removeObserver(this._observer, "ipc:content-shutdown");
     this._processCrashObserversRegistered = false;
   }
 
+  onRegister() {
+    this.sendAsyncMessage("SPServiceWorkerRegistered", { registered: true });
+  }
+
+  _getURI(url) {
+    return Services.io.newURI(url);
+  }
+  _notifyCategoryAndObservers(subject, topic, data) {
+    const serviceMarker = "service,";
+
+    // First create observers from the category manager.
+
+    let observers = [];
+
+    for (let { value: contractID } of Services.catMan.enumerateCategory(
+      topic
+    )) {
+      let factoryFunction;
+      if (contractID.substring(0, serviceMarker.length) == serviceMarker) {
+        contractID = contractID.substring(serviceMarker.length);
+        factoryFunction = "getService";
+      } else {
+        factoryFunction = "createInstance";
+      }
+
+      try {
+        let handler = Cc[contractID][factoryFunction]();
+        if (handler) {
+          let observer = handler.QueryInterface(Ci.nsIObserver);
+          observers.push(observer);
+        }
+      } catch (e) {}
+    }
+
+    // Next enumerate the registered observers.
+    for (let observer of Services.obs.enumerateObservers(topic)) {
+      if (observer instanceof Ci.nsIObserver && !observers.includes(observer)) {
+        observers.push(observer);
+      }
+    }
+
+    observers.forEach(function(observer) {
+      try {
+        observer.observe(subject, topic, data);
+      } catch (e) {}
+    });
+  }
+
+  /*
+    Iterate through one atomic set of pref actions and perform sets/clears as appropriate.
+    All actions performed must modify the relevant pref.
+  */
+  _applyPrefs(actions) {
+    for (let pref of actions) {
+      if (pref.action == "set") {
+        this._setPref(pref.name, pref.type, pref.value, pref.iid);
+      } else if (pref.action == "clear") {
+        Services.prefs.clearUserPref(pref.name);
+      }
+    }
+  }
+
+  /**
+   * Take in a list of pref changes to make, pushes their current values
+   * onto the restore stack, and makes the changes.  When the test
+   * finishes, these changes are reverted.
+   *
+   * |inPrefs| must be an object with up to two properties: "set" and "clear".
+   * pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset
+   * the prefs indicated in |inPrefs.clear|.
+   *
+   * For example, you might pass |inPrefs| as:
+   *
+   *  inPrefs = {'set': [['foo.bar', 2], ['magic.pref', 'baz']],
+   *             'clear': [['clear.this'], ['also.this']] };
+   *
+   * Notice that |set| and |clear| are both an array of arrays.  In |set|, each
+   * of the inner arrays must have the form [pref_name, value] or [pref_name,
+   * value, iid].  (The latter form is used for prefs with "complex" values.)
+   *
+   * In |clear|, each inner array should have the form [pref_name].
+   *
+   * If you set the same pref more than once (or both set and clear a pref),
+   * the behavior of this method is undefined.
+   */
+  pushPrefEnv(inPrefs) {
+    return doPrefEnvOp(() => {
+      let pendingActions = [];
+      let cleanupActions = [];
+
+      for (let [action, prefs] of Object.entries(inPrefs)) {
+        for (let pref of prefs) {
+          let name = pref[0];
+          let value = null;
+          let iid = null;
+          let type = PREF_TYPES[Services.prefs.getPrefType(name)];
+          let originalValue = null;
+
+          if (pref.length == 3) {
+            value = pref[1];
+            iid = pref[2];
+          } else if (pref.length == 2) {
+            value = pref[1];
+          }
+
+          /* If pref is not found or invalid it doesn't exist. */
+          if (type !== "INVALID") {
+            if (
+              (Services.prefs.prefHasUserValue(name) && action == "clear") ||
+              action == "set"
+            ) {
+              originalValue = this._getPref(name, type);
+            }
+          } else if (action == "set") {
+            /* name doesn't exist, so 'clear' is pointless */
+            if (iid) {
+              type = "COMPLEX";
+            }
+          }
+
+          if (type === "INVALID") {
+            type = PREF_TYPES[typeof value];
+          }
+          if (type === "INVALID") {
+            throw new Error("Unexpected preference type");
+          }
+
+          pendingActions.push({ action, type, name, value, iid });
+
+          /* Push original preference value or clear into cleanup array */
+          var cleanupTodo = { type, name, value: originalValue, iid };
+          if (originalValue == null) {
+            cleanupTodo.action = "clear";
+          } else {
+            cleanupTodo.action = "set";
+          }
+          cleanupActions.push(cleanupTodo);
+        }
+      }
+
+      prefUndoStack.push(cleanupActions);
+      this._applyPrefs(pendingActions);
+    });
+  }
+
+  async popPrefEnv() {
+    return doPrefEnvOp(() => {
+      let env = prefUndoStack.pop();
+      if (env) {
+        this._applyPrefs(env);
+        return true;
+      }
+      return false;
+    });
+  }
+
+  flushPrefEnv() {
+    while (prefUndoStack.length) {
+      this.popPrefEnv();
+    }
+  }
+
+  _setPref(name, type, value, iid) {
+    switch (type) {
+      case "BOOL":
+        return Services.prefs.setBoolPref(name, value);
+      case "INT":
+        return Services.prefs.setIntPref(name, value);
+      case "CHAR":
+        return Services.prefs.setCharPref(name, value);
+      case "COMPLEX":
+        return Services.prefs.setComplexValue(name, iid, value);
+    }
+    throw new Error(`Unexpected preference type: ${type}`);
+  }
+
+  _getPref(name, type, defaultValue, iid) {
+    switch (type) {
+      case "BOOL":
+        if (defaultValue !== undefined) {
+          return Services.prefs.getBoolPref(name, defaultValue);
+        }
+        return Services.prefs.getBoolPref(name);
+      case "INT":
+        if (defaultValue !== undefined) {
+          return Services.prefs.getIntPref(name, defaultValue);
+        }
+        return Services.prefs.getIntPref(name);
+      case "CHAR":
+        if (defaultValue !== undefined) {
+          return Services.prefs.getCharPref(name, defaultValue);
+        }
+        return Services.prefs.getCharPref(name);
+      case "COMPLEX":
+        return Services.prefs.getComplexValue(name, iid);
+    }
+    throw new Error(`Unexpected preference type: ${type}`);
+  }
+
+  _toggleMuteAudio(aMuted) {
+    let browser = this.browsingContext.top.embedderElement;
+    if (aMuted) {
+      browser.mute();
+    } else {
+      browser.unmute();
+    }
+  }
+
   /**
    * messageManager callback function
    * This will get requests from our API in the window and process them in chrome for it
    **/
+  // eslint-disable-next-line complexity
   receiveMessage(aMessage) {
+    // We explicitly return values in the below code so that this function
+    // doesn't trigger a flurry of warnings about "does not always return
+    // a value".
     switch (aMessage.name) {
+      case "SPToggleMuteAudio":
+        return this._toggleMuteAudio(aMessage.data.mute);
+
       case "Ping":
         return undefined;
+
       case "SpecialPowers.Quit":
         Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
-        break;
+        return undefined;
+
       case "SpecialPowers.Focus":
         this.manager.rootFrameLoader.ownerElement.focus();
-        break;
+        return undefined;
+
       case "SpecialPowers.CreateFiles":
         return (async () => {
           let filePaths = [];
           if (!this._createdFiles) {
             this._createdFiles = [];
           }
           let createdFiles = this._createdFiles;
 
@@ -200,23 +656,469 @@ class SpecialPowersParent extends Specia
         if (this._createdFiles) {
           this._createdFiles.forEach(function(testFile) {
             try {
               testFile.remove(false);
             } catch (e) {}
           });
           this._createdFiles = null;
         }
-        break;
+        return undefined;
 
       case "Wakeup":
-        break;
+        return undefined;
+
+      case "PushPrefEnv":
+        return this.pushPrefEnv(aMessage.data);
+
+      case "PopPrefEnv":
+        return this.popPrefEnv();
+
+      case "FlushPrefEnv":
+        return this.flushPrefEnv();
+
+      case "SPPrefService": {
+        let prefs = Services.prefs;
+        let prefType = aMessage.json.prefType.toUpperCase();
+        let { prefName, prefValue, iid, defaultValue } = aMessage.json;
+
+        if (aMessage.json.op == "get") {
+          if (!prefName || !prefType) {
+            throw new SpecialPowersError(
+              "Invalid parameters for get in SPPrefService"
+            );
+          }
+
+          // return null if the pref doesn't exist
+          if (
+            defaultValue === undefined &&
+            prefs.getPrefType(prefName) == prefs.PREF_INVALID
+          ) {
+            return null;
+          }
+          return this._getPref(prefName, prefType, defaultValue, iid);
+        } else if (aMessage.json.op == "set") {
+          if (!prefName || !prefType || prefValue === undefined) {
+            throw new SpecialPowersError(
+              "Invalid parameters for set in SPPrefService"
+            );
+          }
+
+          return this._setPref(prefName, prefType, prefValue, iid);
+        } else if (aMessage.json.op == "clear") {
+          if (!prefName) {
+            throw new SpecialPowersError(
+              "Invalid parameters for clear in SPPrefService"
+            );
+          }
+
+          prefs.clearUserPref(prefName);
+        } else {
+          throw new SpecialPowersError("Invalid operation for SPPrefService");
+        }
+
+        return undefined; // See comment at the beginning of this function.
+      }
+
+      case "SPProcessCrashService": {
+        switch (aMessage.json.op) {
+          case "register-observer":
+            this._addProcessCrashObservers();
+            break;
+          case "unregister-observer":
+            this._removeProcessCrashObservers();
+            break;
+          case "delete-crash-dump-files":
+            return this._deleteCrashDumpFiles(aMessage.json.filenames);
+          case "find-crash-dump-files":
+            return this._findCrashDumpFiles(
+              aMessage.json.crashDumpFilesToIgnore
+            );
+          case "delete-pending-crash-dump-files":
+            return this._deletePendingCrashDumpFiles();
+          default:
+            throw new SpecialPowersError(
+              "Invalid operation for SPProcessCrashService"
+            );
+        }
+        return undefined; // See comment at the beginning of this function.
+      }
+
+      case "SPProcessCrashManagerWait": {
+        let promises = aMessage.json.crashIds.map(crashId => {
+          return Services.crashmanager.ensureCrashIsPresent(crashId);
+        });
+        return Promise.all(promises);
+      }
+
+      case "SPPermissionManager": {
+        let msg = aMessage.json;
+        let principal = msg.principal;
+
+        switch (msg.op) {
+          case "add":
+            Services.perms.addFromPrincipal(
+              principal,
+              msg.type,
+              msg.permission,
+              msg.expireType,
+              msg.expireTime
+            );
+            break;
+          case "remove":
+            Services.perms.removeFromPrincipal(principal, msg.type);
+            break;
+          case "has":
+            let hasPerm = Services.perms.testPermissionFromPrincipal(
+              principal,
+              msg.type
+            );
+            return hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
+          case "test":
+            let testPerm = Services.perms.testPermissionFromPrincipal(
+              principal,
+              msg.type
+            );
+            return testPerm == msg.value;
+          default:
+            throw new SpecialPowersError(
+              "Invalid operation for SPPermissionManager"
+            );
+        }
+        return undefined; // See comment at the beginning of this function.
+      }
+
+      case "SPSetTestPluginEnabledState": {
+        var plugin = getTestPlugin(aMessage.data.pluginName);
+        if (!plugin) {
+          return undefined;
+        }
+        var oldEnabledState = plugin.enabledState;
+        plugin.enabledState = aMessage.data.newEnabledState;
+        return oldEnabledState;
+      }
+
+      case "SPObserverService": {
+        let topic = aMessage.json.observerTopic;
+        switch (aMessage.json.op) {
+          case "notify":
+            let data = aMessage.json.observerData;
+            Services.obs.notifyObservers(null, topic, data);
+            break;
+          case "add":
+            this._registerObservers._add(topic);
+            break;
+          default:
+            throw new SpecialPowersError(
+              "Invalid operation for SPObserverervice"
+            );
+        }
+        return undefined; // See comment at the beginning of this function.
+      }
+
+      case "SPLoadChromeScript": {
+        let id = aMessage.json.id;
+        let scriptName;
+
+        let jsScript = aMessage.json.function.body;
+        if (aMessage.json.url) {
+          scriptName = aMessage.json.url;
+        } else if (aMessage.json.function) {
+          scriptName =
+            aMessage.json.function.name ||
+            "<loadChromeScript anonymous function>";
+        } else {
+          throw new SpecialPowersError("SPLoadChromeScript: Invalid script");
+        }
+
+        // Setup a chrome sandbox that has access to sendAsyncMessage
+        // and {add,remove}MessageListener in order to communicate with
+        // the mochitest.
+        let sb = new SpecialPowersSandbox(
+          scriptName,
+          data => {
+            this.sendAsyncMessage("Assert", data);
+          },
+          aMessage.data
+        );
+
+        Object.assign(sb.sandbox, {
+          sendAsyncMessage: (name, message) => {
+            this.sendAsyncMessage("SPChromeScriptMessage", {
+              id,
+              name,
+              message,
+            });
+          },
+          addMessageListener: (name, listener) => {
+            this._chromeScriptListeners.push({ id, name, listener });
+          },
+          removeMessageListener: (name, listener) => {
+            let index = this._chromeScriptListeners.findIndex(function(obj) {
+              return (
+                obj.id == id && obj.name == name && obj.listener == listener
+              );
+            });
+            if (index >= 0) {
+              this._chromeScriptListeners.splice(index, 1);
+            }
+          },
+          actorParent: this.manager,
+        });
+
+        // Evaluate the chrome script
+        try {
+          Cu.evalInSandbox(jsScript, sb.sandbox, "1.8", scriptName, 1);
+        } catch (e) {
+          throw new SpecialPowersError(
+            "Error while executing chrome script '" +
+              scriptName +
+              "':\n" +
+              e +
+              "\n" +
+              e.fileName +
+              ":" +
+              e.lineNumber
+          );
+        }
+        return undefined; // See comment at the beginning of this function.
+      }
+
+      case "SPChromeScriptMessage": {
+        let id = aMessage.json.id;
+        let name = aMessage.json.name;
+        let message = aMessage.json.message;
+        let result;
+        for (let listener of this._chromeScriptListeners) {
+          if (listener.name == name && listener.id == id) {
+            result = listener.listener(message);
+          }
+        }
+        return result;
+      }
+
+      case "SPImportInMainProcess": {
+        var message = { hadError: false, errorMessage: null };
+        try {
+          ChromeUtils.import(aMessage.data);
+        } catch (e) {
+          message.hadError = true;
+          message.errorMessage = e.toString();
+        }
+        return message;
+      }
+
+      case "SPCleanUpSTSData": {
+        let origin = aMessage.data.origin;
+        let flags = aMessage.data.flags;
+        let uri = Services.io.newURI(origin);
+        let sss = Cc["@mozilla.org/ssservice;1"].getService(
+          Ci.nsISiteSecurityService
+        );
+        sss.resetState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, flags);
+        return undefined;
+      }
+
+      case "SPRequestDumpCoverageCounters": {
+        return PerTestCoverageUtils.afterTest();
+      }
+
+      case "SPRequestResetCoverageCounters": {
+        return PerTestCoverageUtils.beforeTest();
+      }
+
+      case "SPCheckServiceWorkers": {
+        let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+          Ci.nsIServiceWorkerManager
+        );
+        let regs = swm.getAllRegistrations();
+
+        // XXX This code is shared with specialpowers.js.
+        let workers = new Array(regs.length);
+        for (let i = 0; i < regs.length; ++i) {
+          let { scope, scriptSpec } = regs.queryElementAt(
+            i,
+            Ci.nsIServiceWorkerRegistrationInfo
+          );
+          workers[i] = { scope, scriptSpec };
+        }
+        return { workers };
+      }
+
+      case "SPLoadExtension": {
+        let id = aMessage.data.id;
+        let ext = aMessage.data.ext;
+        let extension = ExtensionTestCommon.generate(ext);
+
+        let resultListener = (...args) => {
+          this.sendAsyncMessage("SPExtensionMessage", {
+            id,
+            type: "testResult",
+            args,
+          });
+        };
+
+        let messageListener = (...args) => {
+          args.shift();
+          this.sendAsyncMessage("SPExtensionMessage", {
+            id,
+            type: "testMessage",
+            args,
+          });
+        };
+
+        // Register pass/fail handlers.
+        extension.on("test-result", resultListener);
+        extension.on("test-eq", resultListener);
+        extension.on("test-log", resultListener);
+        extension.on("test-done", resultListener);
+
+        extension.on("test-message", messageListener);
+
+        this._extensions.set(id, extension);
+        return undefined;
+      }
+
+      case "SPStartupExtension": {
+        let id = aMessage.data.id;
+        // This is either an Extension, or (if useAddonManager is set) a MockExtension.
+        let extension = this._extensions.get(id);
+        extension.on("startup", (eventName, ext) => {
+          if (!ext) {
+            // ext is only set by the "startup" event from Extension.jsm.
+            // Unfortunately ext-backgroundPage.js emits an event with the same
+            // name, but without the extension object as parameter.
+            return;
+          }
+          // ext is always the "real" Extension object, even when "extension"
+          // is a MockExtension.
+          this.sendAsyncMessage("SPExtensionMessage", {
+            id,
+            type: "extensionSetId",
+            args: [ext.id, ext.uuid],
+          });
+        });
+
+        // Make sure the extension passes the packaging checks when
+        // they're run on a bare archive rather than a running instance,
+        // as the add-on manager runs them.
+        let extensionData = new ExtensionData(extension.rootURI);
+        return extensionData
+          .loadManifest()
+          .then(
+            () => {
+              return extensionData.initAllLocales().then(() => {
+                if (extensionData.errors.length) {
+                  return Promise.reject("Extension contains packaging errors");
+                }
+                return undefined;
+              });
+            },
+            () => {
+              // loadManifest() will throw if we're loading an embedded
+              // extension, so don't worry about locale errors in that
+              // case.
+            }
+          )
+          .then(async () => {
+            // browser tests do not call startup in ExtensionXPCShellUtils or MockExtension,
+            // in that case we have an ID here and we need to set the override.
+            if (extension.id) {
+              await ExtensionTestCommon.setIncognitoOverride(extension);
+            }
+            return extension.startup().then(
+              () => {},
+              e => {
+                dump(`Extension startup failed: ${e}\n${e.stack}`);
+                throw e;
+              }
+            );
+          });
+      }
+
+      case "SPExtensionMessage": {
+        let id = aMessage.data.id;
+        let extension = this._extensions.get(id);
+        extension.testMessage(...aMessage.data.args);
+        return undefined;
+      }
+
+      case "SPUnloadExtension": {
+        let id = aMessage.data.id;
+        let extension = this._extensions.get(id);
+        this._extensions.delete(id);
+        return extension.shutdown().then(() => {
+          return extension._uninstallPromise;
+        });
+      }
+
+      case "SetAsDefaultAssertHandler": {
+        defaultAssertHandler = this;
+        return undefined;
+      }
+
+      case "Spawn": {
+        let { browsingContext, task, args, caller, hasHarness } = aMessage.data;
+
+        let spParent = browsingContext.currentWindowGlobal.getActor(
+          "SpecialPowers"
+        );
+
+        let taskId = nextTaskID++;
+        if (hasHarness) {
+          spParent._taskActors.set(taskId, this);
+        }
+
+        return spParent
+          .sendQuery("Spawn", { task, args, caller, taskId })
+          .finally(() => {
+            spParent._taskActors.delete(taskId);
+          });
+      }
+
+      case "Snapshot": {
+        let { browsingContext, rect, background } = aMessage.data;
+
+        return browsingContext.currentWindowGlobal
+          .drawSnapshot(rect, 1.0, background)
+          .then(async image => {
+            let hiddenFrame = new HiddenFrame();
+            let win = await hiddenFrame.get();
+
+            let canvas = win.document.createElement("canvas");
+            canvas.width = image.width;
+            canvas.height = image.height;
+
+            const ctx = canvas.getContext("2d");
+            ctx.drawImage(image, 0, 0);
+
+            let data = ctx.getImageData(0, 0, image.width, image.height);
+            hiddenFrame.destroy();
+            return data;
+          });
+      }
+
+      case "ProxiedAssert": {
+        let { taskId, data } = aMessage.data;
+
+        let actor = this._taskActors.get(taskId) || defaultAssertHandler;
+        actor.sendAsyncMessage("Assert", data);
+
+        return undefined;
+      }
+
+      case "SPRemoveAllServiceWorkers": {
+        return ServiceWorkerCleanUp.removeAll();
+      }
+
+      case "SPRemoveServiceWorkerDataForExampleDomain": {
+        return ServiceWorkerCleanUp.removeFromHost("example.com");
+      }
 
       default:
-        return super.receiveMessage(aMessage);
+        throw new SpecialPowersError(
+          `Unrecognized Special Powers API: ${aMessage.name}`
+        );
     }
-    return undefined;
-  }
-
-  onRegister() {
-    this.sendAsyncMessage("SPServiceWorkerRegistered", { registered: true });
+    // This should be unreachable. If it ever becomes reachable, ESLint
+    // will produce an error about inconsistent return values.
   }
 }
--- a/testing/specialpowers/moz.build
+++ b/testing/specialpowers/moz.build
@@ -14,18 +14,16 @@ FINAL_TARGET_FILES += [
     'schema.json',
 ]
 
 FINAL_TARGET_FILES.content += [
     '../modules/Assert.jsm',
     'content/MockColorPicker.jsm',
     'content/MockFilePicker.jsm',
     'content/MockPermissionPrompt.jsm',
-    'content/SpecialPowersAPI.jsm',
-    'content/SpecialPowersAPIParent.jsm',
     'content/SpecialPowersChild.jsm',
     'content/SpecialPowersParent.jsm',
     'content/SpecialPowersSandbox.jsm',
     'content/WrapPrivileged.jsm',
 ]
 
 with Files("**"):
     BUG_COMPONENT = ("Testing", "Mochitest")