Bug 1525092 - Confirm removing an extension in HTML about:addons r=jaws
authorMark Striemer <mstriemer@mozilla.com>
Fri, 15 Mar 2019 19:19:28 +0000
changeset 464430 0fb1354119e7
parent 464429 77dbfa3a6e55
child 464431 466d9c2c222d
push id35716
push useraciure@mozilla.com
push dateSun, 17 Mar 2019 09:42:17 +0000
treeherdermozilla-central@8ee97c045359 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1525092
milestone67.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
Bug 1525092 - Confirm removing an extension in HTML about:addons r=jaws Differential Revision: https://phabricator.services.mozilla.com/D22796
browser/base/content/browser.js
toolkit/mozapps/extensions/content/aboutaddons.js
toolkit/mozapps/extensions/test/browser/browser_html_list_view.js
toolkit/mozapps/extensions/test/browser/head.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6461,16 +6461,30 @@ function BrowserCharsetReload() {
 function UpdateCurrentCharset(target) {
   let selectedCharset = CharsetMenu.foldCharset(gBrowser.selectedBrowser.characterSet);
   for (let menuItem of target.getElementsByTagName("menuitem")) {
     let isSelected = menuItem.getAttribute("charset") === selectedCharset;
     menuItem.setAttribute("checked", isSelected);
   }
 }
 
+function promptRemoveExtension(addon) {
+  let {name} = addon;
+  let brand = document.getElementById("bundle_brand").getString("brandShorterName");
+  let {getFormattedString, getString} = gNavigatorBundle;
+  let title = getFormattedString("webext.remove.confirmation.title", [name]);
+  let message = getFormattedString("webext.remove.confirmation.message", [name, brand]);
+  let btnTitle = getString("webext.remove.confirmation.button");
+  let {BUTTON_TITLE_IS_STRING: titleString, BUTTON_TITLE_CANCEL: titleCancel,
+        BUTTON_POS_0, BUTTON_POS_1, confirmEx} = Services.prompt;
+  let btnFlags = BUTTON_POS_0 * titleString + BUTTON_POS_1 * titleCancel;
+  return confirmEx(null, title, message, btnFlags, btnTitle, null, null, null,
+                    {value: 0});
+}
+
 var ToolbarContextMenu = {
   updateDownloadsAutoHide(popup) {
     let checkbox = document.getElementById("toolbar-context-autohide-downloads-button");
     let isDownloads = popup.triggerNode && ["downloads-button", "wrapper-downloads-button"].includes(popup.triggerNode.id);
     checkbox.hidden = !isDownloads;
     if (DownloadsButton.autoHideDownloadsButton) {
       checkbox.setAttribute("checked", "true");
     } else {
@@ -6510,27 +6524,17 @@ var ToolbarContextMenu = {
   },
 
   async removeExtensionForContextAction(popup) {
     let id = this._getExtensionId(popup);
     let addon = id && await AddonManager.getAddonByID(id);
     if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
       return;
     }
-    let {name} = addon;
-    let brand = document.getElementById("bundle_brand").getString("brandShorterName");
-    let {getFormattedString, getString} = gNavigatorBundle;
-    let title = getFormattedString("webext.remove.confirmation.title", [name]);
-    let message = getFormattedString("webext.remove.confirmation.message", [name, brand]);
-    let btnTitle = getString("webext.remove.confirmation.button");
-    let {BUTTON_TITLE_IS_STRING: titleString, BUTTON_TITLE_CANCEL: titleCancel,
-         BUTTON_POS_0, BUTTON_POS_1, confirmEx} = Services.prompt;
-    let btnFlags = BUTTON_POS_0 * titleString + BUTTON_POS_1 * titleCancel;
-    let response = confirmEx(null, title, message, btnFlags, btnTitle, null, null, null,
-                             {value: 0});
+    let response = promptRemoveExtension(addon);
     AMTelemetry.recordActionEvent({
       object: "browserAction",
       action: "uninstall",
       value: response ? "cancelled" : "accepted",
       extra: {addonId: addon.id},
     });
     if (response == 0) {
       addon.uninstall();
--- a/toolkit/mozapps/extensions/content/aboutaddons.js
+++ b/toolkit/mozapps/extensions/content/aboutaddons.js
@@ -1,13 +1,14 @@
 /* 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/. */
 /* eslint max-len: ["error", 80] */
 /* exported initialize, hide, show */
+/* global windowRoot */
 
 "use strict";
 
 const {XPCOMUtils} = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
@@ -311,23 +312,36 @@ class AddonCard extends HTMLElement {
             await addon.disable();
           }
           if (e.mozInputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) {
             // Refocus the open menu button so it's clear where the focus is.
             this.querySelector('[action="more-options"]').focus();
           }
           break;
         case "remove":
-          await addon.uninstall();
+          {
+            panel.hide();
+            let response = windowRoot.ownerGlobal.promptRemoveExtension(addon);
+            if (response == 0) {
+              await addon.uninstall();
+              this.sendEvent("remove");
+            } else {
+              this.sendEvent("remove-cancelled");
+            }
+          }
           break;
       }
     });
 
     this.appendChild(this.card);
   }
+
+  sendEvent(name, detail) {
+    this.dispatchEvent(new CustomEvent(name, {detail}));
+  }
 }
 customElements.define("addon-card", AddonCard);
 
 /**
  * A list view for add-ons of a certain type. It should be initialized with the
  * type of add-on to render and have section data set before being connected to
  * the document.
  *
--- a/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js
@@ -1,10 +1,12 @@
 /* eslint max-len: ["error", 80] */
 
+let promptService;
+
 let gManagerWindow;
 let gCategoryUtilities;
 
 async function loadInitialView(type) {
   gManagerWindow = await open_manager(null);
   gCategoryUtilities = new CategoryUtilities(gManagerWindow);
   await gCategoryUtilities.openType(type);
 
@@ -41,16 +43,17 @@ function waitForThemeChange(list) {
   let moveCount = 0;
   return BrowserTestUtils.waitForEvent(list, "move", () => ++moveCount == 2);
 }
 
 add_task(async function enableHtmlViews() {
   await SpecialPowers.pushPrefEnv({
     set: [["extensions.htmlaboutaddons.enabled", true]],
   });
+  promptService = mockPromptService();
 });
 
 let extensionsCreated = 0;
 
 function createExtensions(manifestExtras) {
   return manifestExtras.map(extra => ExtensionTestUtils.loadExtension({
     manifest: {
       name: "Test extension",
@@ -117,17 +120,24 @@ add_task(async function testExtensionLis
   is(doc.l10n.getAttributes(disableButton).id, "enable-addon-button",
      "The button has the enable label");
 
   // Remove the add-on.
   let removeButton = card.querySelector('[action="remove"]');
   is(doc.l10n.getAttributes(removeButton).id, "remove-addon-button",
      "The button has the remove label");
 
+  // Remove but cancel.
+  let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled");
+  removeButton.click();
+  await cancelled;
+
   let removed = BrowserTestUtils.waitForEvent(list, "remove");
+  // Tell the mock prompt service that the prompt was accepted.
+  promptService._response = 0;
   removeButton.click();
   await removed;
 
   addon = await AddonManager.getAddonByID("test@mochi.test");
   ok(!addon, "The addon is not longer found");
 
   await extension.unload();
   await closeView(win);
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -1444,8 +1444,23 @@ function assertTelemetryMatches(events, 
   let relatedEvents = snapshot.parent.filter(([timestamp, category, method]) => {
     return category == "addonsManager" && (filterMethods ? filterMethods.includes(method) : true);
   }).map(relatedEvent => relatedEvent.slice(2, 6));
 
   // Events are now [method, object, value, extra] as expected.
   Assert.deepEqual(relatedEvents, events, "The events are recorded correctly");
 }
 
+/* HTML view helpers */
+function mockPromptService() {
+  let {prompt} = Services;
+  let promptService = {
+    // The prompt returns 1 for cancelled and 0 for accepted.
+    _response: 1,
+    QueryInterface: ChromeUtils.generateQI([Ci.nsIPromptService]),
+    confirmEx: () => promptService._response,
+  };
+  Services.prompt = promptService;
+  registerCleanupFunction(() => {
+    Services.prompt = prompt;
+  });
+  return promptService;
+}