Bug 1344590: Part 1 - Handle extension reloads, upgrades, and manager restarts in xpcshell helpers. r=rhelmer draft
authorKris Maglione <maglione.k@gmail.com>
Tue, 07 Mar 2017 23:41:57 -0800
changeset 495355 4bf4646138684c8411bebddcd2d09b4d05bc8d7b
parent 494526 0aac487103cebe31b430b44d21b89720fc3989ef
child 495356 f7a9e603ba59ef64aa7417beaea77049c7a1a0ec
push id48299
push usermaglione.k@gmail.com
push dateWed, 08 Mar 2017 17:22:14 +0000
reviewersrhelmer
bugs1344590
milestone54.0a1
Bug 1344590: Part 1 - Handle extension reloads, upgrades, and manager restarts in xpcshell helpers. r=rhelmer MozReview-Commit-ID: FJLeYCz26PX
toolkit/components/extensions/ExtensionXPCShellUtils.jsm
toolkit/components/extensions/test/xpcshell/test_ext_extensionPreferencesManager.js
toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js
toolkit/components/extensions/test/xpcshell/test_ext_privacy_update.js
toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js
toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -7,16 +7,20 @@
 this.EXPORTED_SYMBOLS = ["ExtensionTestUtils"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
 Components.utils.import("resource://gre/modules/Task.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonTestUtils",
+                                  "resource://testing-common/AddonTestUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
@@ -147,109 +151,165 @@ class ContentPage {
     this.browser = null;
 
     this.windowlessBrowser.close();
     this.windowlessBrowser = null;
   }
 }
 
 class ExtensionWrapper {
-  constructor(extension, testScope) {
-    this.extension = extension;
+  constructor(testScope, extension = null) {
     this.testScope = testScope;
 
+    this.extension = null;
+
+    this.handleResult = this.handleResult.bind(this);
+    this.handleMessage = this.handleMessage.bind(this);
+
     this.state = "uninitialized";
 
     this.testResolve = null;
     this.testDone = new Promise(resolve => { this.testResolve = resolve; });
 
     this.messageHandler = new Map();
     this.messageAwaiter = new Map();
 
     this.messageQueue = new Set();
 
-    this.attachListeners();
 
     this.testScope.do_register_cleanup(() => {
-      if (this.messageQueue.size) {
-        let names = Array.from(this.messageQueue, ([msg]) => msg);
-        this.testScope.equal(JSON.stringify(names), "[]", "message queue is empty");
-      }
-      if (this.messageAwaiter.size) {
-        let names = Array.from(this.messageAwaiter.keys());
-        this.testScope.equal(JSON.stringify(names), "[]", "no tasks awaiting on messages");
-      }
-    });
+      this.clearMessageQueues();
 
-    this.testScope.do_register_cleanup(() => {
       if (this.state == "pending" || this.state == "running") {
         this.testScope.equal(this.state, "unloaded", "Extension left running at test shutdown");
         return this.unload();
-      } else if (extension.state == "unloading") {
+      } else if (this.state == "unloading") {
         this.testScope.equal(this.state, "unloaded", "Extension not fully unloaded at test shutdown");
       }
+      this.destroy();
     });
 
-    this.testScope.do_print(`Extension loaded`);
+    if (extension) {
+      this.id = extension.id;
+      this.attachExtension(extension);
+    }
+  }
+
+  destroy() {
+    // This method should be implemented in subclasses which need to
+    // perform cleanup when destroyed.
+  }
+
+  attachExtension(extension) {
+    if (extension === this.extension) {
+      return;
+    }
+
+    if (this.extension) {
+      this.extension.off("test-eq", this.handleResult);
+      this.extension.off("test-log", this.handleResult);
+      this.extension.off("test-result", this.handleResult);
+      this.extension.off("test-done", this.handleResult);
+      this.extension.off("test-message", this.handleMessage);
+      this.clearMessageQueues();
+    }
+    this.extension = extension;
+
+    extension.on("test-eq", this.handleResult);
+    extension.on("test-log", this.handleResult);
+    extension.on("test-result", this.handleResult);
+    extension.on("test-done", this.handleResult);
+    extension.on("test-message", this.handleMessage);
+
+    this.testScope.do_print(`Extension attached`);
   }
 
-  attachListeners() {
-    /* eslint-disable mozilla/balanced-listeners */
-    this.extension.on("test-eq", (kind, pass, msg, expected, actual) => {
-      this.testScope.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`);
-    });
-    this.extension.on("test-log", (kind, pass, msg) => {
-      this.testScope.do_print(msg);
-    });
-    this.extension.on("test-result", (kind, pass, msg) => {
-      this.testScope.ok(pass, msg);
-    });
-    this.extension.on("test-done", (kind, pass, msg, expected, actual) => {
-      this.testScope.ok(pass, msg);
-      this.testResolve(msg);
-    });
+  clearMessageQueues() {
+    if (this.messageQueue.size) {
+      let names = Array.from(this.messageQueue, ([msg]) => msg);
+      this.testScope.equal(JSON.stringify(names), "[]", "message queue is empty");
+      this.messageQueue.clear();
+    }
+    if (this.messageAwaiter.size) {
+      let names = Array.from(this.messageAwaiter.keys());
+      this.testScope.equal(JSON.stringify(names), "[]", "no tasks awaiting on messages");
+      for (let promise of this.messageAwaiter.values()) {
+        promise.reject();
+      }
+      this.messageAwaiter.clear();
+    }
+  }
+
+  handleResult(kind, pass, msg, expected, actual) {
+    switch (kind) {
+      case "test-eq":
+        this.testScope.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`);
+        break;
 
-    this.extension.on("test-message", (kind, msg, ...args) => {
-      let handler = this.messageHandler.get(msg);
-      if (handler) {
-        handler(...args);
-      } else {
-        this.messageQueue.add([msg, ...args]);
-        this.checkMessages();
-      }
-    });
-    /* eslint-enable mozilla/balanced-listeners */
+      case "test-log":
+        this.testScope.do_print(msg);
+        break;
+
+      case "test-result":
+        this.testScope.ok(pass, msg);
+        break;
+
+      case "test-done":
+        this.testScope.ok(pass, msg);
+        this.testResolve(msg);
+        break;
+    }
+  }
+
+  handleMessage(kind, msg, ...args) {
+    let handler = this.messageHandler.get(msg);
+    if (handler) {
+      handler(...args);
+    } else {
+      this.messageQueue.add([msg, ...args]);
+      this.checkMessages();
+    }
+  }
+
+  awaitStartup() {
+    return this.startupPromise;
   }
 
   startup() {
     if (this.state != "uninitialized") {
       throw new Error("Extension already started");
     }
     this.state = "pending";
 
-    return this.extension.startup().then(
+    this.startupPromise = this.extension.startup().then(
       result => {
         this.state = "running";
 
         return result;
       },
       error => {
         this.state = "failed";
 
         return Promise.reject(error);
       });
+
+    return this.startupPromise;
   }
 
   async unload() {
     if (this.state != "running") {
       throw new Error("Extension not running");
     }
     this.state = "unloading";
 
-    await this.extension.shutdown();
+    if (this.addon) {
+      this.addon.uninstall();
+    } else {
+      await this.extension.shutdown();
+    }
 
     this.state = "unloaded";
   }
 
   /*
    * This method marks the extension unloading without actually calling
    * shutdown, since shutting down a MockExtension causes it to be uninstalled.
    *
@@ -295,30 +355,211 @@ class ExtensionWrapper {
 
   checkDuplicateListeners(msg) {
     if (this.messageHandler.has(msg) || this.messageAwaiter.has(msg)) {
       throw new Error("only one message handler allowed");
     }
   }
 
   awaitMessage(msg) {
-    return new Promise(resolve => {
+    return new Promise((resolve, reject) => {
       this.checkDuplicateListeners(msg);
 
-      this.messageAwaiter.set(msg, {resolve});
+      this.messageAwaiter.set(msg, {resolve, reject});
       this.checkMessages();
     });
   }
 
   onMessage(msg, callback) {
     this.checkDuplicateListeners(msg);
     this.messageHandler.set(msg, callback);
   }
 }
 
+class AOMExtensionWrapper extends ExtensionWrapper {
+  constructor(testScope, xpiFile, installType) {
+    super(testScope);
+
+    this.onEvent = this.onEvent.bind(this);
+
+    this.file = xpiFile;
+    this.installType = installType;
+
+    this.cleanupFiles = [xpiFile];
+
+    Management.on("ready", this.onEvent);
+    Management.on("shutdown", this.onEvent);
+    Management.on("startup", this.onEvent);
+
+    AddonTestUtils.on("addon-manager-shutdown", this.onEvent);
+    AddonTestUtils.on("addon-manager-started", this.onEvent);
+
+    AddonManager.addAddonListener(this);
+  }
+
+  destroy() {
+    this.id = null;
+    this.addon = null;
+
+    Management.off("ready", this.onEvent);
+    Management.off("shutdown", this.onEvent);
+    Management.off("startup", this.onEvent);
+
+    AddonTestUtils.off("addon-manager-shutdown", this.onEvent);
+    AddonTestUtils.off("addon-manager-started", this.onEvent);
+
+    AddonManager.removeAddonListener(this);
+
+    for (let file of this.cleanupFiles.splice(0)) {
+      try {
+        Services.obs.notifyObservers(file, "flush-cache-entry", null);
+        file.remove(false);
+      } catch (e) {
+        Cu.reportError(e);
+      }
+    }
+  }
+
+  setRestarting() {
+    if (this.state !== "restarting") {
+      this.startupPromise = new Promise(resolve => {
+        this.resolveStartup = resolve;
+      });
+    }
+    this.state = "restarting";
+  }
+
+  onEnabling(addon) {
+    if (addon.id === this.id) {
+      this.setRestarting();
+    }
+  }
+
+  onInstalling(addon) {
+    if (addon.id === this.id) {
+      this.setRestarting();
+    }
+  }
+
+  onInstalled(addon) {
+    if (addon.id === this.id) {
+      this.addon = addon;
+    }
+  }
+
+  onUninstalled(addon) {
+    if (addon.id === this.id) {
+      this.destroy();
+    }
+  }
+
+  onEvent(kind, ...args) {
+    switch (kind) {
+      case "addon-manager-started":
+        AddonManager.getAddonByID(this.id).then(addon => {
+          this.addon = addon;
+        });
+        // FALLTHROUGH
+      case "addon-manager-shutdown":
+        this.addon = null;
+
+        this.setRestarting();
+        break;
+
+      case "startup": {
+        let [extension] = args;
+        if (extension.id === this.id) {
+          this.attachExtension(extension);
+          this.state = "pending";
+        }
+        break;
+      }
+
+      case "shutdown": {
+        let [extension] = args;
+        if (extension.id === this.id && this.state !== "restarting") {
+          this.state = "unloaded";
+        }
+        break;
+      }
+
+      case "ready": {
+        let [extension] = args;
+        if (extension.id === this.id) {
+          this.state = "running";
+          this.resolveStartup(extension);
+        }
+        break;
+      }
+    }
+  }
+
+  _install(xpiFile) {
+    if (this.installType === "temporary") {
+      return AddonManager.installTemporaryAddon(xpiFile).then(addon => {
+        this.id = addon.id;
+        this.addon = addon;
+
+        return this.startupPromise;
+      }).catch(e => {
+        this.state = "unloaded";
+        return Promise.reject(e);
+      });
+    } else if (this.installType === "permanent") {
+      return AddonManager.getInstallForFile(xpiFile).then(install => {
+        let listener = {
+          onInstallFailed: () => {
+            this.state = "unloaded";
+            this.resolveStartup(Promise.reject(new Error("Install failed")));
+          },
+          onInstallEnded: (install, newAddon) => {
+            this.id = newAddon.id;
+            this.addon = newAddon;
+          },
+        };
+
+        install.addListener(listener);
+        install.install();
+
+        return this.startupPromise;
+      });
+    }
+  }
+
+  get version() {
+    return this.addon && this.addon.version;
+  }
+
+  startup() {
+    if (this.state != "uninitialized") {
+      throw new Error("Extension already started");
+    }
+
+    this.state = "pending";
+    this.startupPromise = new Promise(resolve => {
+      this.resolveStartup = resolve;
+    });
+
+    return this._install(this.file);
+  }
+
+  upgrade(data) {
+    this.startupPromise = new Promise(resolve => {
+      this.resolveStartup = resolve;
+    });
+    this.state = "restarting";
+
+    let xpiFile = Extension.generateXPI(data);
+
+    this.cleanupFiles.push(xpiFile);
+
+    return this._install(xpiFile);
+  }
+}
+
 var ExtensionTestUtils = {
   BASE_MANIFEST,
 
   normalizeManifest: Task.async(function* (manifest, baseManifest = BASE_MANIFEST) {
     yield Management.lazyInit();
 
     let errors = [];
     let context = {
@@ -403,19 +644,25 @@ var ExtensionTestUtils = {
     this.mockAppInfo();
 
     let manager = Cc["@mozilla.org/addons/integration;1"].getService(Ci.nsIObserver)
                                                          .QueryInterface(Ci.nsITimerCallback);
     manager.observe(null, "addons-startup", null);
   },
 
   loadExtension(data) {
+    if (data.useAddonManager) {
+      let xpiFile = Extension.generateXPI(data);
+
+      return new AOMExtensionWrapper(this.currentScope, xpiFile, data.useAddonManager);
+    }
+
     let extension = Extension.generate(data);
 
-    return new ExtensionWrapper(extension, this.currentScope);
+    return new ExtensionWrapper(this.currentScope, extension);
   },
 
   get remoteContentScripts() {
     return REMOTE_CONTENT_SCRIPTS;
   },
 
   set remoteContentScripts(val) {
     REMOTE_CONTENT_SCRIPTS = !!val;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_extensionPreferencesManager.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extensionPreferencesManager.js
@@ -73,37 +73,37 @@ for (let setting in SETTINGS) {
 
 function checkPrefs(settingObj, value, msg) {
   for (let pref of settingObj.prefNames) {
     equal(Preferences.get(pref), settingObj.valueFn(pref, value), msg);
   }
 }
 
 add_task(async function test_preference_manager() {
+  await promiseStartupManager();
+
   // Create an array of test framework extension wrappers to install.
   let testExtensions = [
     ExtensionTestUtils.loadExtension({
       useAddonManager: "temporary",
       manifest: {},
     }),
     ExtensionTestUtils.loadExtension({
       useAddonManager: "temporary",
       manifest: {},
     }),
   ];
 
-  await promiseStartupManager();
-
   for (let extension of testExtensions) {
     await extension.startup();
   }
 
   // Create an array actual Extension objects which correspond to the
   // test framework extension wrappers.
-  let extensions = testExtensions.map(extension => extension.extension._extension);
+  let extensions = testExtensions.map(extension => extension.extension);
 
   for (let setting in SETTINGS) {
     let settingObj = SETTINGS[setting];
     let newValue1 = "newValue1";
     let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
       extensions[1], setting);
     equal(levelOfControl, "controllable_by_this_extension",
       "getLevelOfControl returns correct levelOfControl with no settings set.");
--- a/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
@@ -38,41 +38,41 @@ const TEST_TYPE = "myType";
 let callbackCount = 0;
 
 function initialValue(key) {
   callbackCount++;
   return `key:${key}`;
 }
 
 add_task(async function test_settings_store() {
+  await promiseStartupManager();
+
   // Create an array of test framework extension wrappers to install.
   let testExtensions = [
     ExtensionTestUtils.loadExtension({
       useAddonManager: "temporary",
       manifest: {},
     }),
     ExtensionTestUtils.loadExtension({
       useAddonManager: "temporary",
       manifest: {},
     }),
     ExtensionTestUtils.loadExtension({
       useAddonManager: "temporary",
       manifest: {},
     }),
   ];
 
-  await promiseStartupManager();
-
   for (let extension of testExtensions) {
     await extension.startup();
   }
 
   // Create an array actual Extension objects which correspond to the
   // test framework extension wrappers.
-  let extensions = testExtensions.map(extension => extension.extension._extension);
+  let extensions = testExtensions.map(extension => extension.extension);
 
   let expectedCallbackCount = 0;
 
   // Add a setting for the second oldest extension, where it is the only setting for a key.
   for (let key of KEY_LIST) {
     let extensionIndex = 1;
     let itemToAdd = ITEMS[key][extensionIndex];
     let levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[extensionIndex], TEST_TYPE, key);
--- a/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js
@@ -62,17 +62,16 @@ add_task(function* test_management_unins
     useAddonManager: "temporary",
   });
 
   yield extension.startup();
   let addon = yield AddonManager.getAddonByID(id);
   notEqual(addon, null, "Add-on is installed");
   extension.sendMessage("uninstall");
   yield waitForUninstalled();
-  yield extension.markUnloaded();
   Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
 });
 
 add_task(function* test_management_uninstall_prompt_uninstall() {
   promptService._response = 0;
 
   function background() {
     browser.test.onMessage.addListener(msg => {
@@ -86,17 +85,16 @@ add_task(function* test_management_unins
     useAddonManager: "temporary",
   });
 
   yield extension.startup();
   let addon = yield AddonManager.getAddonByID(id);
   notEqual(addon, null, "Add-on is installed");
   extension.sendMessage("uninstall");
   yield waitForUninstalled();
-  yield extension.markUnloaded();
 
   // Test localization strings
   equal(promptService._confirmExArgs[1], `Uninstall ${manifest.name}`);
   equal(promptService._confirmExArgs[2],
         `The extension “${manifest.name}” is requesting to be uninstalled. What would you like to do?`);
   equal(promptService._confirmExArgs[4], "Uninstall");
   equal(promptService._confirmExArgs[5], "Keep Installed");
   Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
@@ -118,17 +116,20 @@ add_task(function* test_management_unins
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest,
     background,
     useAddonManager: "temporary",
   });
 
   yield extension.startup();
+
   let addon = yield AddonManager.getAddonByID(id);
   notEqual(addon, null, "Add-on is installed");
+
   extension.sendMessage("uninstall");
   yield extension.awaitMessage("uninstall-rejected");
+
   addon = yield AddonManager.getAddonByID(id);
   notEqual(addon, null, "Add-on remains installed");
+
   yield extension.unload();
-  Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
 });
--- a/toolkit/components/extensions/test/xpcshell/test_ext_privacy_update.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_privacy_update.js
@@ -10,43 +10,29 @@ XPCOMUtils.defineLazyGetter(this, "Manag
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 
 const {
   createAppInfo,
   createTempWebExtensionFile,
-  promiseAddonEvent,
   promiseCompleteAllInstalls,
   promiseFindAddonUpdates,
   promiseShutdownManager,
   promiseStartupManager,
 } = AddonTestUtils;
 
 AddonTestUtils.init(this);
 
 // Allow for unsigned addons.
 AddonTestUtils.overrideCertDB();
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
 
-function awaitEvent(eventName) {
-  return new Promise(resolve => {
-    let listener = (_eventName, ...args) => {
-      if (_eventName === eventName) {
-        Management.off(eventName, listener);
-        resolve(...args);
-      }
-    };
-
-    Management.on(eventName, listener);
-  });
-}
-
 add_task(async function test_privacy_update() {
   // Create a object to hold the values to which we will initialize the prefs.
   const PREFS = {
     "network.predictor.enabled": true,
     "network.prefetch-next": true,
     "network.http.speculative-parallel-limit": 10,
     "network.dns.disablePrefetch": false,
   };
@@ -139,42 +125,34 @@ add_task(async function test_privacy_upd
 
   await extension.startup();
 
   // Change the value to false.
   extension.sendMessage("set", {value: false});
   let data = await extension.awaitMessage("privacyData");
   ok(!data.value, "get returns expected value after setting.");
 
-  let addon = await AddonManager.getAddonByID(EXTENSION_ID);
-  equal(addon.version, "1.0", "The installed addon has the expected version.");
+  equal(extension.version, "1.0", "The installed addon has the expected version.");
 
-  let update = await promiseFindAddonUpdates(addon);
+  let update = await promiseFindAddonUpdates(extension.addon);
   let install = update.updateAvailable;
 
-  let promiseInstalled = promiseAddonEvent("onInstalled");
   await promiseCompleteAllInstalls([install]);
 
-  let startupPromise = awaitEvent("ready");
+  await extension.awaitStartup();
 
-  let [updated_addon] = await promiseInstalled;
-  equal(updated_addon.version, "2.0", "The updated addon has the expected version.");
-
-  extension.extension = await startupPromise;
-  extension.attachListeners();
+  equal(extension.version, "2.0", "The updated addon has the expected version.");
 
   extension.sendMessage("get");
   data = await extension.awaitMessage("privacyData");
   ok(!data.value, "get returns expected value after updating.");
 
   // Verify the prefs are still set to match the "false" setting.
   for (let pref in PREFS) {
     let msg = `${pref} set correctly.`;
     let expectedValue = pref === "network.http.speculative-parallel-limit" ? 0 : !PREFS[pref];
     equal(Preferences.get(pref), expectedValue, msg);
   }
 
   await extension.unload();
 
-  await updated_addon.uninstall();
-
   await promiseShutdownManager();
 });
--- a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js
@@ -3,16 +3,17 @@
 "use strict";
 
 XPCOMUtils.defineLazyGetter(this, "Management", () => {
   const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
   return Management;
 });
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
 
 const {
   createAppInfo,
   createTempWebExtensionFile,
   promiseAddonEvent,
   promiseCompleteAllInstalls,
   promiseFindAddonUpdates,
   promiseRestartManager,
@@ -22,29 +23,16 @@ const {
 
 AddonTestUtils.init(this);
 
 // Allow for unsigned addons.
 AddonTestUtils.overrideCertDB();
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
 
-function awaitEvent(eventName) {
-  return new Promise(resolve => {
-    let listener = (_eventName, ...args) => {
-      if (_eventName === eventName) {
-        Management.off(eventName, listener);
-        resolve(...args);
-      }
-    };
-
-    Management.on(eventName, listener);
-  });
-}
-
 function background() {
   let onInstalledDetails = null;
   let onStartupFired = false;
 
   browser.runtime.onInstalled.addListener(details => {
     onInstalledDetails = details;
   });
 
@@ -79,16 +67,20 @@ function* expectEvents(extension, {onSta
   }
 
   extension.sendMessage("did-on-startup-fire");
   let fired = yield extension.awaitMessage("on-startup-fired");
   equal(fired, onStartupFired, `Expected runtime.onStartup to ${onStartupFired ? "" : "not "} fire`);
 }
 
 add_task(function* test_should_fire_on_addon_update() {
+  Preferences.set("extensions.logging.enabled", false);
+
+  yield promiseStartupManager();
+
   const EXTENSION_ID = "test_runtime_on_installed_addon_update@tests.mozilla.org";
 
   const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
 
   // The test extension uses an insecure update url.
   Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
 
   const testServer = createHttpServer();
@@ -132,18 +124,16 @@ add_task(function* test_should_fire_on_a
         },
       },
     },
     background,
   });
 
   testServer.registerFile("/addons/test_runtime_on_installed-2.0.xpi", webExtensionFile);
 
-  yield promiseStartupManager();
-
   yield extension.startup();
 
   yield expectEvents(extension, {
     onStartupFired: false,
     onInstalledFired: true,
     onInstalledReason: "install",
   });
 
@@ -153,33 +143,29 @@ add_task(function* test_should_fire_on_a
   let update = yield promiseFindAddonUpdates(addon);
   let install = update.updateAvailable;
 
   let promiseInstalled = promiseAddonEvent("onInstalled");
   yield promiseCompleteAllInstalls([install]);
 
   yield extension.awaitMessage("reloading");
 
-  let startupPromise = awaitEvent("ready");
-
   let [updated_addon] = yield promiseInstalled;
   equal(updated_addon.version, "2.0", "The updated addon has the correct version");
 
-  extension.extension = yield startupPromise;
-  extension.attachListeners();
+  yield extension.awaitStartup();
 
   yield expectEvents(extension, {
     onStartupFired: false,
     onInstalledFired: true,
     onInstalledReason: "update",
   });
 
   yield extension.unload();
 
-  yield updated_addon.uninstall();
   yield promiseShutdownManager();
 });
 
 add_task(function* test_should_fire_on_browser_update() {
   const EXTENSION_ID = "test_runtime_on_installed_browser_update@tests.mozilla.org";
 
   yield promiseStartupManager();
 
@@ -199,54 +185,47 @@ add_task(function* test_should_fire_on_b
   yield extension.startup();
 
   yield expectEvents(extension, {
     onStartupFired: false,
     onInstalledFired: true,
     onInstalledReason: "install",
   });
 
-  let startupPromise = awaitEvent("ready");
   yield promiseRestartManager("1");
-  extension.extension = yield startupPromise;
-  extension.attachListeners();
+
+  yield extension.awaitStartup();
 
   yield expectEvents(extension, {
     onStartupFired: true,
     onInstalledFired: false,
   });
 
   // Update the browser.
-  startupPromise = awaitEvent("ready");
   yield promiseRestartManager("2");
-  extension.extension = yield startupPromise;
-  extension.attachListeners();
+  yield extension.awaitStartup();
 
   yield expectEvents(extension, {
     onStartupFired: true,
     onInstalledFired: true,
     onInstalledReason: "browser_update",
   });
 
   // Restart the browser.
-  startupPromise = awaitEvent("ready");
   yield promiseRestartManager("2");
-  extension.extension = yield startupPromise;
-  extension.attachListeners();
+  yield extension.awaitStartup();
 
   yield expectEvents(extension, {
     onStartupFired: true,
     onInstalledFired: false,
   });
 
   // Update the browser again.
-  startupPromise = awaitEvent("ready");
   yield promiseRestartManager("3");
-  extension.extension = yield startupPromise;
-  extension.attachListeners();
+  yield extension.awaitStartup();
 
   yield expectEvents(extension, {
     onStartupFired: true,
     onInstalledFired: true,
     onInstalledReason: "browser_update",
   });
 
   yield extension.unload();
@@ -275,20 +254,19 @@ add_task(function* test_should_not_fire_
   yield extension.startup();
 
   yield expectEvents(extension, {
     onStartupFired: false,
     onInstalledFired: true,
     onInstalledReason: "install",
   });
 
-  let startupPromise = awaitEvent("ready");
   extension.sendMessage("reload-extension");
-  extension.extension = yield startupPromise;
-  extension.attachListeners();
+  extension.setRestarting();
+  yield extension.awaitStartup();
 
   yield expectEvents(extension, {
     onStartupFired: false,
     onInstalledFired: false,
   });
 
   yield extension.unload();
   yield promiseShutdownManager();
@@ -318,20 +296,18 @@ add_task(function* test_should_not_fire_
     onStartupFired: false,
     onInstalledFired: true,
     onInstalledReason: "install",
   });
 
   let addon = yield AddonManager.getAddonByID(EXTENSION_ID);
   addon.userDisabled = true;
 
-  let startupPromise = awaitEvent("ready");
   addon.userDisabled = false;
-  extension.extension = yield startupPromise;
-  extension.attachListeners();
+  yield extension.awaitStartup();
 
   yield expectEvents(extension, {
     onStartupFired: false,
     onInstalledFired: false,
   });
 
   yield extension.markUnloaded();
   yield promiseShutdownManager();
--- a/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js
@@ -25,29 +25,16 @@ gPort = testserver.identity.primaryPort;
 mapFile("/data/test_delay_updates_complete.json", testserver);
 mapFile("/data/test_delay_updates_ignore.json", testserver);
 mapFile("/data/test_delay_updates_defer.json", testserver);
 mapFile("/data/test_no_update.json", testserver);
 testserver.registerDirectory("/addons/", do_get_file("addons"));
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
 
-const { Management } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
-
-function promiseWebExtensionStartup() {
-  return new Promise(resolve => {
-    let listener = (event, extension) => {
-      Management.off("startup", listener);
-      resolve(extension);
-    };
-
-    Management.on("startup", listener);
-  });
-}
-
 // add-on registers upgrade listener, and ignores update.
 add_task(function* delay_updates_ignore() {
   startupManager();
 
   let extension = ExtensionTestUtils.loadExtension({
     useAddonManager: "permanent",
     manifest: {
       "version": "1.0",
@@ -100,31 +87,30 @@ add_task(function* delay_updates_ignore(
   do_check_true(addon_postponed.isCompatible);
   do_check_false(addon_postponed.appDisabled);
   do_check_true(addon_postponed.isActive);
   do_check_eq(addon_postponed.type, "extension");
 
   yield extension.awaitFinish("delay");
 
   // restarting allows upgrade to proceed
-  yield extension.markUnloaded();
   yield promiseRestartManager();
 
   let addon_upgraded = yield promiseAddonByID(IGNORE_ID);
-  yield promiseWebExtensionStartup();
+  yield extension.awaitStartup();
 
   do_check_neq(addon_upgraded, null);
   do_check_eq(addon_upgraded.version, "2.0");
   do_check_eq(addon_upgraded.name, "Delay Upgrade");
   do_check_true(addon_upgraded.isCompatible);
   do_check_false(addon_upgraded.appDisabled);
   do_check_true(addon_upgraded.isActive);
   do_check_eq(addon_upgraded.type, "extension");
 
-  yield addon_upgraded.uninstall();
+  yield extension.unload();
   yield promiseShutdownManager();
 });
 
 // add-on registers upgrade listener, and allows update.
 add_task(function* delay_updates_complete() {
   startupManager();
 
   let extension = ExtensionTestUtils.loadExtension({
@@ -163,32 +149,31 @@ add_task(function* delay_updates_complet
 
   let promiseInstalled = promiseAddonEvent("onInstalled");
   yield promiseCompleteAllInstalls([install]);
 
   yield extension.awaitFinish("reload");
 
   // addon upgrade has been allowed
   let [addon_allowed] = yield promiseInstalled;
-  yield promiseWebExtensionStartup();
+  yield extension.awaitStartup();
 
   do_check_neq(addon_allowed, null);
   do_check_eq(addon_allowed.version, "2.0");
   do_check_eq(addon_allowed.name, "Delay Upgrade");
   do_check_true(addon_allowed.isCompatible);
   do_check_false(addon_allowed.appDisabled);
   do_check_true(addon_allowed.isActive);
   do_check_eq(addon_allowed.type, "extension");
 
   if (stageDir.exists()) {
     do_throw("Staging directory should not exist for formerly-postponed extension");
   }
 
-  yield extension.markUnloaded();
-  yield addon_allowed.uninstall();
+  yield extension.unload();
   yield promiseShutdownManager();
 });
 
 // add-on registers upgrade listener, initially defers update then allows upgrade
 add_task(function* delay_updates_defer() {
   startupManager();
 
   let extension = ExtensionTestUtils.loadExtension({
@@ -250,42 +235,41 @@ add_task(function* delay_updates_defer()
 
   // add-on will not allow upgrade until message is received
   yield extension.awaitMessage("truly ready");
   extension.sendMessage("allow");
   yield extension.awaitFinish("allowed");
 
   // addon upgrade has been allowed
   let [addon_allowed] = yield promiseInstalled;
-  yield promiseWebExtensionStartup();
+  yield extension.awaitStartup();
 
   do_check_neq(addon_allowed, null);
   do_check_eq(addon_allowed.version, "2.0");
   do_check_eq(addon_allowed.name, "Delay Upgrade");
   do_check_true(addon_allowed.isCompatible);
   do_check_false(addon_allowed.appDisabled);
   do_check_true(addon_allowed.isActive);
   do_check_eq(addon_allowed.type, "extension");
 
-  yield extension.markUnloaded();
   yield promiseRestartManager();
 
   // restart changes nothing
   addon_allowed = yield promiseAddonByID(DEFER_ID);
-  yield promiseWebExtensionStartup();
+  yield extension.awaitStartup();
 
   do_check_neq(addon_allowed, null);
   do_check_eq(addon_allowed.version, "2.0");
   do_check_eq(addon_allowed.name, "Delay Upgrade");
   do_check_true(addon_allowed.isCompatible);
   do_check_false(addon_allowed.appDisabled);
   do_check_true(addon_allowed.isActive);
   do_check_eq(addon_allowed.type, "extension");
 
-  yield addon_allowed.uninstall();
+  yield extension.unload();
   yield promiseShutdownManager();
 });
 
 // browser.runtime.reload() without a pending upgrade should just reload.
 add_task(function* runtime_reload() {
   startupManager();
 
   let extension = ExtensionTestUtils.loadExtension({
@@ -321,23 +305,22 @@ add_task(function* runtime_reload() {
   do_check_false(addon.appDisabled);
   do_check_true(addon.isActive);
   do_check_eq(addon.type, "extension");
 
   yield promiseFindAddonUpdates(addon);
 
   extension.sendMessage("reload");
   // Wait for extension to restart, to make sure reload works.
-  yield promiseWebExtensionStartup();
+  yield extension.awaitStartup();
 
   addon = yield promiseAddonByID(NOUPDATE_ID);
   do_check_neq(addon, null);
   do_check_eq(addon.version, "1.0");
   do_check_eq(addon.name, "Generated extension");
   do_check_true(addon.isCompatible);
   do_check_false(addon.appDisabled);
   do_check_true(addon.isActive);
   do_check_eq(addon.type, "extension");
 
-  yield extension.markUnloaded();
-  yield addon.uninstall();
+  yield extension.unload();
   yield promiseShutdownManager();
 });