Bug 1338827 - Add button to remove add-on in about:debugging r?jdescottes draft
authorMark Striemer <mstriemer@mozilla.com>
Thu, 18 May 2017 10:40:14 -0500
changeset 580499 e5c3fd46d7718b5de5c37b8855e1242a188186db
parent 579779 d30ef114da504eaa276e798ddadb2e08517dddc6
child 580500 754f635bbba54862f8fb486f89459b471babea0d
child 581384 2e1cb0db1c06fecfde8ee08c8b76397866ae619f
push id59582
push userbmo:mstriemer@mozilla.com
push dateThu, 18 May 2017 17:09:38 +0000
reviewersjdescottes
bugs1338827
milestone55.0a1
Bug 1338827 - Add button to remove add-on in about:debugging r?jdescottes MozReview-Commit-ID: HqpzhIM1MGV
devtools/client/aboutdebugging/components/addons/target.js
devtools/client/aboutdebugging/modules/addon.js
devtools/client/aboutdebugging/test/browser.ini
devtools/client/aboutdebugging/test/browser_addons_reload.js
devtools/client/aboutdebugging/test/browser_addons_remove.js
devtools/client/aboutdebugging/test/head.js
devtools/client/locales/en-US/aboutdebugging.properties
--- a/devtools/client/aboutdebugging/components/addons/target.js
+++ b/devtools/client/aboutdebugging/components/addons/target.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-env browser */
 
 "use strict";
 
 const { createClass, DOM: dom, PropTypes } =
   require("devtools/client/shared/vendor/react");
-const { debugAddon } = require("../../modules/addon");
+const { debugAddon, uninstallAddon } = require("../../modules/addon");
 const Services = require("Services");
 
 loader.lazyImporter(this, "BrowserToolboxProcess",
   "resource://devtools/client/framework/ToolboxProcess.jsm");
 
 loader.lazyRequireGetter(this, "DebuggerClient",
   "devtools/shared/client/main", true);
 
@@ -55,16 +55,21 @@ module.exports = createClass({
     }).isRequired
   },
 
   debug() {
     let { target } = this.props;
     debugAddon(target.addonID);
   },
 
+  uninstall() {
+    let { target } = this.props;
+    uninstallAddon(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 => {
@@ -99,13 +104,19 @@ module.exports = createClass({
           disabled: debugDisabled,
         }, Strings.GetStringFromName("debug")),
         dom.button({
           className: "reload-button addon-target-button",
           onClick: this.reload,
           disabled: !canBeReloaded,
           title: !canBeReloaded ?
             Strings.GetStringFromName("reloadDisabledTooltip") : ""
-        }, Strings.GetStringFromName("reload"))
+        }, Strings.GetStringFromName("reload")),
+        target.temporarilyInstalled
+          ? dom.button({
+            className: "uninstall-button addon-target-button",
+            onClick: this.uninstall,
+          }, Strings.GetStringFromName("remove"))
+          : null,
       ),
     );
   }
 });
--- a/devtools/client/aboutdebugging/modules/addon.js
+++ b/devtools/client/aboutdebugging/modules/addon.js
@@ -1,23 +1,29 @@
 /* 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";
 
 loader.lazyImporter(this, "BrowserToolboxProcess",
   "resource://devtools/client/framework/ToolboxProcess.jsm");
+loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 
 let toolbox = null;
 
 exports.debugAddon = function (addonID) {
   if (toolbox) {
     toolbox.close();
   }
 
   toolbox = BrowserToolboxProcess.init({
     addonID,
     onClose: () => {
       toolbox = null;
     }
   });
 };
+
+exports.uninstallAddon = async function (addonID) {
+  let addon = await AddonManager.getAddonByID(addonID);
+  return addon && addon.uninstall();
+};
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -27,16 +27,17 @@ tags = webextensions
 tags = webextensions
 [browser_addons_debug_webextension_nobg.js]
 tags = webextensions
 [browser_addons_debug_webextension_popup.js]
 tags = webextensions
 [browser_addons_debugging_initial_state.js]
 [browser_addons_install.js]
 [browser_addons_reload.js]
+[browser_addons_remove.js]
 [browser_addons_toggle_debug.js]
 [browser_page_not_found.js]
 [browser_service_workers.js]
 [browser_service_workers_fetch_flag.js]
 [browser_service_workers_multi_content_process.js]
 skip-if = !e10s # This test is only valid in e10s
 [browser_service_workers_not_compatible.js]
 [browser_service_workers_push.js]
--- a/devtools/client/aboutdebugging/test/browser_addons_reload.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_reload.js
@@ -1,74 +1,25 @@
 /* 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);
-  });
-}
-
-function* tearDownAddon(addon) {
-  const onUninstalled = promiseAddonEvent("onUninstalled");
-  addon.uninstall();
-  const [uninstalledAddon] = yield onUninstalled;
-  is(uninstalledAddon.id, addon.id,
-     `Add-on was uninstalled: ${uninstalledAddon.id}`);
-}
-
 function getReloadButton(document, addonName) {
   const names = getInstalledAddonNames(document);
   const name = names.filter(element => element.textContent === addonName)[0];
   ok(name, `Found ${addonName} add-on in the list`);
   const targetElement = name.parentNode.parentNode;
   const reloadButton = targetElement.querySelector(".reload-button");
   info(`Found reload button for ${addonName}`);
   return reloadButton;
 }
 
-function installAddonWithManager(filePath) {
-  return new Promise((resolve, reject) => {
-    AddonManager.getInstallForFile(filePath, install => {
-      if (!install) {
-        throw new Error(`An install was not created for ${filePath}`);
-      }
-      install.addListener({
-        onDownloadFailed: reject,
-        onDownloadCancelled: reject,
-        onInstallFailed: reject,
-        onInstallCancelled: reject,
-        onInstallEnded: resolve
-      });
-      install.install();
-    });
-  });
-}
-
-function getAddonByID(addonId) {
-  return new Promise(resolve => {
-    AddonManager.getAddonByID(addonId, addon => resolve(addon));
-  });
-}
-
 /**
  * Creates a web extension from scratch in a temporary location.
  * The object must be removed when you're finished working with it.
  */
 class TempWebExt {
   constructor(addonId) {
     this.addonId = addonId;
     this.tmpDir = FileUtils.getDir("TmpD", ["browser_addons_reload"]);
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_remove.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function getTargetEl(document, id) {
+  return document.querySelector(`[data-addon-id="${id}"]`);
+}
+
+function getRemoveButton(document, id) {
+  return document.querySelector(`[data-addon-id="${id}"] .uninstall-button`);
+}
+
+add_task(function* () {
+  const addonID = "test-devtools@mozilla.org";
+  const addonName = "test-devtools";
+
+  const { 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,
+    path: "addons/unpacked/install.rdf",
+    name: addonName,
+  });
+
+  ok(getTargetEl(document, addonID), "add-on is shown");
+
+  // Click the remove button and wait for the DOM to change.
+  const addonListMutation = waitForMutation(
+    getTemporaryAddonList(document),
+    { childList: true });
+  getRemoveButton(document, addonID).click();
+  yield addonListMutation;
+
+  ok(!getTargetEl(document, addonID), "add-on is not shown");
+
+  yield closeAboutDebugging(tab);
+});
+
+add_task(function* onlyTempInstalledAddonsCanBeRemoved() {
+  const { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
+  const onAddonListUpdated = waitForMutation(getAddonList(document),
+                                             { childList: true });
+  yield installAddonWithManager(getSupportsFile("addons/bug1273184.xpi").file);
+  yield onAddonListUpdated;
+  const addon = yield getAddonByID("bug1273184@tests");
+
+  const removeButton = getRemoveButton(document, addon.id);
+  ok(!removeButton, "remove button is not shown");
+
+  yield tearDownAddon(addon);
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -3,17 +3,18 @@
 
 /* eslint-env browser */
 /* exported openAboutDebugging, changeAboutDebuggingHash, closeAboutDebugging,
    installAddon, uninstallAddon, waitForMutation, waitForContentMutation, assertHasTarget,
    getServiceWorkerList, getTabList, openPanel, waitForInitialAddonList,
    waitForServiceWorkerRegistered, unregisterServiceWorker,
    waitForDelayedStartupFinished, setupTestAboutDebuggingWebExtension,
    waitForServiceWorkerActivation, enableServiceWorkerDebugging,
-   getServiceWorkerContainer */
+   getServiceWorkerContainer, promiseAddonEvent, installAddonWithManager, getAddonByID,
+   tearDownAddon */
 /* import-globals-from ../../framework/test/shared-head.js */
 
 "use strict";
 
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
@@ -431,8 +432,63 @@ function enableServiceWorkerDebugging() 
       ["dom.serviceWorkers.testing.enabled", true],
       // Force single content process.
       ["dom.ipc.processCount", 1],
     ]};
     SpecialPowers.pushPrefEnv(options, done);
     Services.ppmm.releaseCachedProcesses();
   });
 }
+
+/**
+ * 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);
+  });
+}
+
+/**
+ * Install an add-on using the AddonManager so it does not show up as temporary.
+ */
+function installAddonWithManager(filePath) {
+  return new Promise((resolve, reject) => {
+    AddonManager.getInstallForFile(filePath, install => {
+      if (!install) {
+        throw new Error(`An install was not created for ${filePath}`);
+      }
+      install.addListener({
+        onDownloadFailed: reject,
+        onDownloadCancelled: reject,
+        onInstallFailed: reject,
+        onInstallCancelled: reject,
+        onInstallEnded: resolve
+      });
+      install.install();
+    });
+  });
+}
+
+function getAddonByID(addonId) {
+  return new Promise(resolve => {
+    AddonManager.getAddonByID(addonId, addon => resolve(addon));
+  });
+}
+
+/**
+ * Uninstall an add-on.
+ */
+function* tearDownAddon(addon) {
+  const onUninstalled = promiseAddonEvent("onUninstalled");
+  addon.uninstall();
+  const [uninstalledAddon] = yield onUninstalled;
+  is(uninstalledAddon.id, addon.id,
+     `Add-on was uninstalled: ${uninstalledAddon.id}`);
+}
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -87,16 +87,20 @@ selectAddonFromFile2 = Select Manifest F
 # This string is displayed as a label of the button that reloads a given addon.
 reload = Reload
 
 # LOCALIZATION NOTE (reloadDisabledTooltip):
 # This string is displayed in a tooltip that appears when hovering over a
 # disabled 'reload' button.
 reloadDisabledTooltip = Only temporarily installed add-ons can be reloaded
 
+# LOCALIZATION NOTE (remove):
+# This string is displayed as a label of the button that will remove a given addon.
+remove = Remove
+
 # LOCALIZATION NOTE (location):
 # This string is displayed as a label for the filesystem location of an extension.
 location = Location
 
 # LOCALIZATION NOTE (workers):
 # This string is displayed as a header of the about:debugging#workers page.
 workers = Workers