Bug 1246030 - Allow reloading an add-on in about:debugging. r=ochameau
authorKumar McMillan <kumar.mcmillan@gmail.com>
Thu, 31 Mar 2016 12:01:02 -0500
changeset 317232 c5b30ad336bd5004f87738fd0c4f48a60f22bbeb
parent 317225 898a9b29cadbf098059bc7574fe0206989a03766
child 317233 ff8aa19ba327597352157dbe53c7af971d38052a
push id9480
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 17:12:58 +0000
treeherdermozilla-aurora@0d6a91c76a9e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1246030
milestone48.0a1
Bug 1246030 - Allow reloading an add-on in about:debugging. r=ochameau MozReview-Commit-ID: Lh2iwPgmlhU
devtools/client/aboutdebugging/components/addon-target.js
devtools/client/aboutdebugging/components/addons-tab.js
devtools/client/aboutdebugging/test/browser.ini
devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
devtools/client/aboutdebugging/test/browser_addons_install.js
devtools/client/aboutdebugging/test/browser_addons_reload.js
devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
devtools/client/aboutdebugging/test/head.js
devtools/client/locales/en-US/aboutdebugging.properties
devtools/server/actors/addon.js
--- a/devtools/client/aboutdebugging/components/addon-target.js
+++ b/devtools/client/aboutdebugging/components/addon-target.js
@@ -20,28 +20,45 @@ const Strings = Services.strings.createB
 module.exports = createClass({
   displayName: "AddonTarget",
 
   debug() {
     let { target } = this.props;
     BrowserToolboxProcess.init({ addonID: target.addonID });
   },
 
+  reload() {
+    let { client, target } = this.props;
+    // This function sometimes returns a partial promise that only
+    // implements then().
+    client.request({
+      to: target.addonActor,
+      type: "reload"
+    }).then(() => {}, error => {
+      throw new Error(
+        "Error reloading addon " + target.addonID + ": " + error);
+    });
+  },
+
   render() {
     let { target, debugDisabled } = this.props;
 
     return dom.div({ className: "target-container" },
       dom.img({
         className: "target-icon",
         role: "presentation",
         src: target.icon
       }),
       dom.div({ className: "target" },
         dom.div({ className: "target-name" }, target.name)
       ),
       dom.button({
         className: "debug-button",
         onClick: this.debug,
         disabled: debugDisabled,
-      }, Strings.GetStringFromName("debug"))
+      }, Strings.GetStringFromName("debug")),
+      dom.button({
+        className: "reload-button",
+        onClick: this.reload
+      }, Strings.GetStringFromName("reload"))
     );
   }
 });
--- a/devtools/client/aboutdebugging/components/addons-tab.js
+++ b/devtools/client/aboutdebugging/components/addons-tab.js
@@ -55,27 +55,31 @@ module.exports = createClass({
     let debugDisabled =
       !Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
       !Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
 
     this.setState({ debugDisabled });
   },
 
   updateAddonsList() {
-    AddonManager.getAllAddons(addons => {
-      let extensions = addons.filter(addon => addon.isDebuggable).map(addon => {
-        return {
-          name: addon.name,
-          icon: addon.iconURL || ExtensionIcon,
-          addonID: addon.id
-        };
+    this.props.client.listAddons()
+      .then(({addons}) => {
+        let extensions = addons.filter(addon => addon.debuggable).map(addon => {
+          return {
+            name: addon.name,
+            icon: addon.iconURL || ExtensionIcon,
+            addonID: addon.id,
+            addonActor: addon.actor
+          };
+        });
+
+        this.setState({ extensions });
+      }, error => {
+        throw new Error("Client error while listing addons: " + error);
       });
-
-      this.setState({ extensions });
-    });
   },
 
   /**
    * Mandatory callback as AddonManager listener.
    */
   onInstalled() {
     this.updateAddonsList();
   },
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -9,15 +9,16 @@ support-files =
   service-workers/empty-sw.html
   service-workers/empty-sw.js
   service-workers/push-sw.html
   service-workers/push-sw.js
 
 [browser_addons_debug_bootstrapped.js]
 [browser_addons_debugging_initial_state.js]
 [browser_addons_install.js]
+[browser_addons_reload.js]
 [browser_addons_toggle_debug.js]
 [browser_service_workers.js]
 [browser_service_workers_push.js]
 [browser_service_workers_start.js]
 [browser_service_workers_timeout.js]
 skip-if = true # Bug 1232931
 [browser_service_workers_unregister.js]
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
@@ -17,16 +17,17 @@ add_task(function* () {
       ["devtools.debugger.prompt-connection", false],
       // Enable Browser toolbox test script execution via env variable
       ["devtools.browser-toolbox.allow-unsafe-script", true],
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
   yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
                      "test-devtools");
 
   // Retrieve the DEBUG button for the addon
   let names = [...document.querySelectorAll("#addons .target-name")];
   let name = names.filter(element => element.textContent === ADDON_NAME)[0];
   ok(name, "Found the addon in the list");
   let targetElement = name.parentNode.parentNode;
--- a/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
@@ -42,16 +42,17 @@ function* testCheckboxState(testData) {
     let options = {"set": [
       ["devtools.chrome.enabled", testData.chromeEnabled],
       ["devtools.debugger.remote-enabled", testData.debuggerRemoteEnable],
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
 
   info("Install a test addon.");
   yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
                      "test-devtools");
 
   info("Test checkbox checked state.");
   let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
   is(addonDebugCheckbox.checked, testData.expected,
--- a/devtools/client/aboutdebugging/test/browser_addons_install.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_install.js
@@ -2,29 +2,31 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const ADDON_ID = "test-devtools@mozilla.org";
 const ADDON_NAME = "test-devtools";
 
 add_task(function* () {
   let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
 
   // Install this add-on, and verify that it appears in the about:debugging UI
   yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
                      "test-devtools");
 
   // Install the add-on, and verify that it disappears in the about:debugging UI
   yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
 
   yield closeAboutDebugging(tab);
 });
 
 add_task(function* () {
   let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
 
   // Start an observer that looks for the install error before
   // actually doing the install
   let top = document.querySelector(".addons-top");
   let promise = waitForMutation(top, { childList: true });
 
   // Mock the file picker to select a test addon
   let MockFilePicker = SpecialPowers.MockFilePicker;
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_reload.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+/**
+ * Returns a promise that resolves when the given add-on event is fired. The
+ * resolved value is an array of arguments passed for the event.
+ */
+function promiseAddonEvent(event) {
+  return new Promise(resolve => {
+    let listener = {
+      [event]: function(...args) {
+        AddonManager.removeAddonListener(listener);
+        resolve(args);
+      }
+    };
+
+    AddonManager.addAddonListener(listener);
+  });
+}
+
+add_task(function* () {
+  const { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
+  yield installAddon(document, "addons/unpacked/install.rdf",
+                     ADDON_NAME, ADDON_NAME);
+
+  // Retrieve the Reload button.
+  const names = [...document.querySelectorAll("#addons .target-name")];
+  const name = names.filter(element => element.textContent === ADDON_NAME)[0];
+  ok(name, "Found " + ADDON_NAME + " add-on in the list");
+  const targetElement = name.parentNode.parentNode;
+  const reloadButton = targetElement.querySelector(".reload-button");
+  ok(reloadButton, "Found its reload button");
+
+  const onDisabled = promiseAddonEvent("onDisabled");
+  const onEnabled = promiseAddonEvent("onEnabled");
+
+  const onBootstrapInstallCalled = new Promise(done => {
+    Services.obs.addObserver(function listener() {
+      Services.obs.removeObserver(listener, ADDON_NAME, false);
+      ok(true, "Add-on was installed: " + ADDON_NAME);
+      done();
+    }, ADDON_NAME, false);
+  });
+
+  reloadButton.click();
+
+  const [disabledAddon] = yield onDisabled;
+  ok(disabledAddon.name === ADDON_NAME,
+     "Add-on was disabled: " + disabledAddon.name);
+
+  const [enabledAddon] = yield onEnabled;
+  ok(enabledAddon.name === ADDON_NAME,
+     "Add-on was re-enabled: " + enabledAddon.name);
+
+  yield onBootstrapInstallCalled;
+
+  info("Uninstall addon installed earlier.");
+  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
@@ -15,16 +15,17 @@ add_task(function* () {
     let options = {"set": [
       ["devtools.chrome.enabled", false],
       ["devtools.debugger.remote-enabled", false],
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
 
   info("Install a test addon.");
   yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
                      "test-devtools");
 
   let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
   ok(!addonDebugCheckbox.checked, "Addons debugging should be disabled.");
 
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* eslint-env browser */
 /* eslint-disable mozilla/no-cpows-in-tests */
 /* exported openAboutDebugging, closeAboutDebugging, installAddon,
    uninstallAddon, waitForMutation, assertHasTarget,
-   waitForServiceWorkerRegistered, unregisterServiceWorker */
+   waitForInitialAddonList, waitForServiceWorkerRegistered,
+   unregisterServiceWorker */
 /* global sendAsyncMessage */
 
 "use strict";
 
 var { utils: Cu, classes: Cc, interfaces: Ci } = Components;
 
 const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
@@ -151,16 +152,40 @@ function* uninstallAddon(document, addon
   let names = [...addonList.querySelectorAll(".target-name")];
   names = names.map(element => element.textContent);
   ok(!names.includes(addonName),
     "After uninstall, the addon name disappears from the list of addons: "
     + names);
 }
 
 /**
+ * Returns a promise that will resolve when the add-on list has been updated.
+ *
+ * @param {Node} document
+ * @return {Promise}
+ */
+function waitForInitialAddonList(document) {
+  const addonListContainer = document.querySelector("#addons .targets");
+  let addonCount = addonListContainer.querySelectorAll(".target");
+  addonCount = addonCount ? [...addonCount].length : -1;
+  info("Waiting for add-ons to load. Current add-on count: " + addonCount);
+
+  // This relies on the network speed of the actor responding to the
+  // listAddons() request and also the speed of openAboutDebugging().
+  let result;
+  if (addonCount > 0) {
+    info("Actually, the add-ons have already loaded");
+    result = Promise.resolve();
+  } else {
+    result = waitForMutation(addonListContainer, { childList: true });
+  }
+  return result;
+}
+
+/**
  * Returns a promise that will resolve after receiving a mutation matching the
  * provided mutation options on the provided target.
  * @param {Node} target
  * @param {Object} mutationOptions
  * @return {Promise}
  */
 function waitForMutation(target, mutationOptions) {
   return new Promise(resolve => {
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -11,16 +11,17 @@ unregister = unregister
 
 addons = Add-ons
 addonDebugging.label = Enable add-on debugging
 addonDebugging.tooltip = Turning this on will allow you to debug add-ons and various other parts of the browser chrome
 addonDebugging.moreInfo = more info
 loadTemporaryAddon = Load Temporary Add-on
 extensions = Extensions
 selectAddonFromFile2 = Select Manifest File or Package (.xpi)
+reload = Reload
 
 workers = Workers
 serviceWorkers = Service Workers
 sharedWorkers = Shared Workers
 otherWorkers = Other Workers
 
 nothing = Nothing yet.
 
--- a/devtools/server/actors/addon.js
+++ b/devtools/server/actors/addon.js
@@ -78,16 +78,17 @@ BrowserAddonActor.prototype = {
       this._contextPool.addActor(this._consoleActor);
     }
 
     return {
       actor: this.actorID,
       id: this.id,
       name: this._addon.name,
       url: this.url,
+      iconURL: this._addon.iconURL,
       debuggable: this._addon.isDebuggable,
       consoleActor: this._consoleActor.actorID,
 
       traits: {
         highlightable: false,
         networkMonitor: false,
       },
     };
@@ -150,16 +151,23 @@ BrowserAddonActor.prototype = {
     this._contextPool.removeActor(this.threadActor);
 
     this.threadActor = null;
     this._sources = null;
 
     return { type: "detached" };
   },
 
+  onReload: function BAA_onReload() {
+    return this._addon.reload()
+      .then(() => {
+        return {}; // send an empty response
+      });
+  },
+
   preNest: function() {
     let e = Services.wm.getEnumerator(null);
     while (e.hasMoreElements()) {
       let win = e.getNext();
       let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils);
       windowUtils.suppressEventHandling(true);
       windowUtils.suspendTimeouts();
@@ -244,17 +252,18 @@ BrowserAddonActor.prototype = {
    */
   _findDebuggees: function (dbg) {
     return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
   }
 };
 
 BrowserAddonActor.prototype.requestTypes = {
   "attach": BrowserAddonActor.prototype.onAttach,
-  "detach": BrowserAddonActor.prototype.onDetach
+  "detach": BrowserAddonActor.prototype.onDetach,
+  "reload": BrowserAddonActor.prototype.onReload
 };
 
 /**
  * The AddonConsoleActor implements capabilities needed for the add-on web
  * console feature.
  *
  * @constructor
  * @param object aAddon