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 522319 0fb1354119e7fbc2da3bc65b360c930f394c1bae
parent 522318 77dbfa3a6e551cac15d2d30ee9ce608efb0645b9
child 522320 466d9c2c222dff3d7b8e64658dad991c48cb5518
push id10871
push usercbrindusan@mozilla.com
push dateMon, 18 Mar 2019 15:49:32 +0000
treeherdermozilla-beta@018abdd16060 [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;
+}