Bug 1401610 - Add "Remove Extension" context menu item to browserAction. r=dao
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Wed, 05 Sep 2018 10:02:52 +0000
changeset 434759 1ea4e6ffa2e6
parent 434758 3c5a69821efd
child 434760 22c07b1ad906
push id68781
push userdgottwald@mozilla.com
push dateWed, 05 Sep 2018 11:37:44 +0000
treeherderautoland@1ea4e6ffa2e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdao
bugs1401610
milestone64.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 1401610 - Add "Remove Extension" context menu item to browserAction. r=dao Differential Revision: https://phabricator.services.mozilla.com/D3565
browser/base/content/browser.js
browser/base/content/browser.xul
browser/components/customizableui/content/panelUI.inc.xul
browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/browser.properties
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -11,16 +11,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/NotificationDB.jsm");
 
 const {WebExtensionPolicy} = Cu.getGlobalForObject(Services);
 
 // lazy module getters
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+  AddonManager: "resource://gre/modules/AddonManager.jsm",
   BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   CFRPageActions: "resource://activity-stream/lib/CFRPageActions.jsm",
   CharsetMenu: "resource://gre/modules/CharsetMenu.jsm",
   Color: "resource://gre/modules/Color.jsm",
   ContentSearch: "resource:///modules/ContentSearch.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
@@ -6273,54 +6274,76 @@ 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 UpdateDownloadsAutoHide(popup) {
-  let checkbox = popup.querySelector(".customize-context-autoHide");
-  let isDownloads = popup.triggerNode && ["downloads-button", "wrapper-downloads-button"].includes(popup.triggerNode.id);
-  checkbox.hidden = !isDownloads;
-  if (this.window.DownloadsButton.autoHideDownloadsButton) {
-    checkbox.setAttribute("checked", "true");
-  } else {
-    checkbox.removeAttribute("checked");
-  }
-}
-
-function onDownloadsAutoHideChange(event) {
-  let autoHide = event.target.getAttribute("checked") == "true";
-  Services.prefs.setBoolPref("browser.download.autohideButton", autoHide);
-}
-
-function getUnwrappedTriggerNode(popup) {
-  // Toolbar buttons are wrapped in customize mode. Unwrap if necessary.
-  let {triggerNode} = popup;
-  if (triggerNode && gCustomizeMode.isWrappedToolbarItem(triggerNode)) {
-    return triggerNode.firstElementChild;
-  }
-  return triggerNode;
-}
-
-function UpdateManageExtension(popup) {
-  let checkbox = popup.querySelector(".customize-context-manageExtension");
-  let separator = checkbox.nextElementSibling;
-  let node = getUnwrappedTriggerNode(popup);
-  let isWebExt = node && node.hasAttribute("data-extensionid");
-  checkbox.hidden = separator.hidden = !isWebExt;
-}
-
-function openAboutAddonsForContextAction(popup) {
-  let id = getUnwrappedTriggerNode(popup).getAttribute("data-extensionid");
-  let viewID = "addons://detail/" + encodeURIComponent(id);
-  BrowserOpenAddonsMgr(viewID);
-}
+var ToolbarContextMenu = {
+  updateDownloadsAutoHide(popup) {
+    let checkbox = popup.querySelector(".customize-context-autoHide");
+    let isDownloads = popup.triggerNode && ["downloads-button", "wrapper-downloads-button"].includes(popup.triggerNode.id);
+    checkbox.hidden = !isDownloads;
+    if (DownloadsButton.autoHideDownloadsButton) {
+      checkbox.setAttribute("checked", "true");
+    } else {
+      checkbox.removeAttribute("checked");
+    }
+  },
+
+  onDownloadsAutoHideChange(event) {
+    let autoHide = event.target.getAttribute("checked") == "true";
+    Services.prefs.setBoolPref("browser.download.autohideButton", autoHide);
+  },
+
+  _getUnwrappedTriggerNode(popup) {
+    // Toolbar buttons are wrapped in customize mode. Unwrap if necessary.
+    let {triggerNode} = popup;
+    if (triggerNode && gCustomizeMode.isWrappedToolbarItem(triggerNode)) {
+      return triggerNode.firstElementChild;
+    }
+    return triggerNode;
+  },
+
+  updateExtension(popup) {
+    let removeExtension = popup.querySelector(".customize-context-removeExtension");
+    let manageExtension = removeExtension.nextElementSibling;
+    let separator = manageExtension.nextElementSibling;
+    let node = this._getUnwrappedTriggerNode(popup);
+    let isWebExt = node && node.hasAttribute("data-extensionid");
+    removeExtension.hidden = manageExtension.hidden = separator.hidden = !isWebExt;
+  },
+
+  async removeExtensionForContextAction(popup) {
+    let id = this._getUnwrappedTriggerNode(popup).getAttribute("data-extensionid");
+    let addon = await AddonManager.getAddonByID(id);
+    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});
+    if (response == 0) {
+      addon.uninstall();
+    }
+  },
+
+  openAboutAddonsForContextAction(popup) {
+    let id = this._getUnwrappedTriggerNode(popup).getAttribute("data-extensionid");
+    let viewID = "addons://detail/" + encodeURIComponent(id);
+    BrowserOpenAddonsMgr(viewID);
+  },
+};
 
 var gPageStyleMenu = {
   // This maps from a <browser> element (or, more specifically, a
   // browser's permanentKey) to an Object that contains the most recent
   // information about the browser content's stylesheets. That Object
   // is populated via the PageStyle:StyleSheets message from the content
   // process. The Object should have the following structure:
   //
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -360,29 +360,34 @@ xmlns="http://www.w3.org/1999/xhtml"
                      oncommand="SidebarUI.reversePosition()"/>
       <toolbarseparator/>
       <toolbarbutton label="&sidebarMenuClose.label;"
                      class="subviewbutton"
                      oncommand="SidebarUI.hide()"/>
     </panel>
 
     <menupopup id="toolbar-context-menu"
-               onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator')); UpdateDownloadsAutoHide(this); UpdateManageExtension(this)">
-      <menuitem oncommand="openAboutAddonsForContextAction(this.parentElement)"
+               onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator')); ToolbarContextMenu.updateDownloadsAutoHide(this); ToolbarContextMenu.updateExtension(this)">
+      <menuitem oncommand="ToolbarContextMenu.removeExtensionForContextAction(this.parentElement)"
+                accesskey="&customizeMenu.removeExtension.accesskey;"
+                label="&customizeMenu.removeExtension.label;"
+                contexttype="toolbaritem"
+                class="customize-context-removeExtension"/>
+      <menuitem oncommand="ToolbarContextMenu.openAboutAddonsForContextAction(this.parentElement)"
                 accesskey="&customizeMenu.manageExtension.accesskey;"
                 label="&customizeMenu.manageExtension.label;"
                 contexttype="toolbaritem"
                 class="customize-context-manageExtension"/>
       <menuseparator/>
       <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
                 accesskey="&customizeMenu.pinToOverflowMenu.accesskey;"
                 label="&customizeMenu.pinToOverflowMenu.label;"
                 contexttype="toolbaritem"
                 class="customize-context-moveToPanel"/>
-      <menuitem oncommand="onDownloadsAutoHideChange(event)"
+      <menuitem oncommand="ToolbarContextMenu.onDownloadsAutoHideChange(event)"
                 type="checkbox"
                 accesskey="&customizeMenu.autoHideDownloadsButton.accesskey;"
                 label="&customizeMenu.autoHideDownloadsButton.label;"
                 contexttype="toolbaritem"
                 class="customize-context-autoHide"/>
       <menuitem oncommand="gCustomizeMode.removeFromArea(document.popupNode)"
                 accesskey="&customizeMenu.removeFromToolbar.accesskey;"
                 label="&customizeMenu.removeFromToolbar.label;"
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -24,18 +24,23 @@
                       accesskey="&overflowCustomizeToolbar.accesskey;"
                       label="&overflowCustomizeToolbar.label;"/>
     </panelview>
   </panelmultiview>
   <!-- This menu is here because not having it in the menu in which it's used flickers
        when hover styles overlap. See https://bugzilla.mozilla.org/show_bug.cgi?id=1378427 .
        -->
   <menupopup id="customizationPanelItemContextMenu"
-             onpopupshowing="gCustomizeMode.onPanelContextMenuShowing(event); UpdateManageExtension(this)">
-    <menuitem oncommand="openAboutAddonsForContextAction(this.parentElement)"
+             onpopupshowing="gCustomizeMode.onPanelContextMenuShowing(event); ToolbarContextMenu.updateExtension(this)">
+    <menuitem oncommand="ToolbarContextMenu.removeExtensionForContextAction(this.parentElement)"
+              accesskey="&customizeMenu.removeExtension.accesskey;"
+              label="&customizeMenu.removeExtension.label;"
+              contexttype="toolbaritem"
+              class="customize-context-removeExtension"/>
+    <menuitem oncommand="ToolbarContextMenu.openAboutAddonsForContextAction(this.parentElement)"
               accesskey="&customizeMenu.manageExtension.accesskey;"
               label="&customizeMenu.manageExtension.label;"
               contexttype="toolbaritem"
               class="customize-context-manageExtension"/>
     <menuseparator/>
     <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
               id="customizationPanelItemContextMenuPin"
               accesskey="&customizeMenu.pinToOverflowMenu.accesskey;"
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
@@ -100,16 +100,30 @@ add_task(async function browseraction_po
   ok(!item.hidden);
   ok(item.disabled);
 
   await closeContextMenu(contentAreaContextMenu);
 
   await extension.unload();
 });
 
+function openContextMenu(menuId, targetId) {
+  return openChromeContextMenu(menuId, "#" + CSS.escape(targetId));
+}
+
+function waitForElementShown(element) {
+  let win = element.ownerGlobal;
+  let dwu = win.windowUtils;
+  return BrowserTestUtils.waitForCondition(() => {
+    info("Waiting for overflow button to have non-0 size");
+    let bounds = dwu.getBoundsWithoutFlushing(element);
+    return bounds.width > 0 && bounds.height > 0;
+  });
+}
+
 add_task(async function browseraction_contextmenu_manage_extension() {
   let id = "addon_id@example.com";
   let buttonId = `${makeWidgetId(id)}-browser-action`;
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "applications": {
         "gecko": {id},
       },
@@ -120,27 +134,26 @@ add_task(async function browseraction_co
     },
     useAddonManager: "temporary",
     files: {
       "options.html": `<script src="options.js"></script>`,
       "options.js": `browser.test.sendMessage("options-loaded");`,
     },
   });
 
-  function openContextMenu(menuId, targetId) {
-    return openChromeContextMenu(menuId, "#" + CSS.escape(targetId));
-  }
-
   function checkVisibility(menu, visible) {
+    let removeExtension = menu.querySelector(".customize-context-removeExtension");
     let manageExtension = menu.querySelector(".customize-context-manageExtension");
     let separator = manageExtension.nextElementSibling;
 
     info(`Check visibility`);
-    is(manageExtension.hidden, !visible, `Manage Extension should be ${visible ? "visible" : "hidden"}`);
-    is(separator.hidden, !visible, `Separator after Manage Extension should be ${visible ? "visible" : "hidden"}`);
+    let expected = visible ? "visible" : "hidden";
+    is(removeExtension.hidden, !visible, `Remove Extension should be ${expected}`);
+    is(manageExtension.hidden, !visible, `Manage Extension should be ${expected}`);
+    is(separator.hidden, !visible, `Separator after Manage Extension should be ${expected}`);
   }
 
   async function testContextMenu(menuId, customizing) {
     info(`Open browserAction context menu in ${menuId}`);
     let menu = await openContextMenu(menuId, buttonId);
     await checkVisibility(menu, true);
 
     info(`Choosing 'Manage Extension' in ${menuId} should load options`);
@@ -158,26 +171,16 @@ add_task(async function browseraction_co
       await customizationReady;
     } else {
       gBrowser.removeTab(tab);
     }
 
     return menu;
   }
 
-  function waitForElementShown(element) {
-    let win = element.ownerGlobal;
-    let dwu = win.windowUtils;
-    return BrowserTestUtils.waitForCondition(() => {
-      info("Waiting for overflow button to have non-0 size");
-      let bounds = dwu.getBoundsWithoutFlushing(element);
-      return bounds.width > 0 && bounds.height > 0;
-    });
-  }
-
   async function main(customizing) {
     if (customizing) {
       info("Enter customize mode");
       let customizationReady = BrowserTestUtils.waitForEvent(gNavToolbox, "customizationready");
       gCustomizeMode.enter();
       await customizationReady;
     }
 
@@ -242,8 +245,122 @@ add_task(async function browseraction_co
 
   info("Run tests in customize mode");
   await main(true);
 
   info("Close the dummy tab and finish");
   gBrowser.removeTab(dummyTab);
   await extension.unload();
 });
+
+add_task(async function browseraction_contextmenu_remove_extension() {
+  let id = "addon_id@example.com";
+  let name = "Awesome Add-on";
+  let buttonId = `${makeWidgetId(id)}-browser-action`;
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      name,
+      "applications": {
+        "gecko": {id},
+      },
+      "browser_action": {},
+    },
+    useAddonManager: "temporary",
+  });
+  let brand = Services.strings.createBundle("chrome://branding/locale/brand.properties")
+    .GetStringFromName("brandShorterName");
+  let {prompt} = Services;
+  let promptService = {
+    _response: 1,
+    QueryInterface: ChromeUtils.generateQI([Ci.nsIPromptService]),
+    confirmEx: function(...args) {
+      promptService._confirmExArgs = args;
+      return promptService._response;
+    },
+  };
+  Services.prompt = promptService;
+  registerCleanupFunction(() => {
+    Services.prompt = prompt;
+  });
+
+  async function testContextMenu(menuId, customizing) {
+    info(`Open browserAction context menu in ${menuId}`);
+    let menu = await openContextMenu(menuId, buttonId);
+
+    info(`Choosing 'Remove Extension' in ${menuId} should show confirm dialog`);
+    let removeExtension = menu.querySelector(".customize-context-removeExtension");
+    await closeChromeContextMenu(menuId, removeExtension);
+    is(promptService._confirmExArgs[1], `Remove ${name}`);
+    is(promptService._confirmExArgs[2], `Remove ${name} from ${brand}?`);
+    is(promptService._confirmExArgs[4], "Remove");
+    return menu;
+  }
+
+  async function main(customizing) {
+    if (customizing) {
+      info("Enter customize mode");
+      let customizationReady = BrowserTestUtils.waitForEvent(gNavToolbox, "customizationready");
+      gCustomizeMode.enter();
+      await customizationReady;
+    }
+
+    info("Test toolbar context menu in browserAction");
+    await testContextMenu("toolbar-context-menu", customizing);
+
+    info("Pin the browserAction and another button to the overflow menu");
+    CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
+
+    info("Wait until the overflow menu is ready");
+    let overflowButton = document.getElementById("nav-bar-overflow-button");
+    let icon = document.getAnonymousElementByAttribute(overflowButton, "class", "toolbarbutton-icon");
+    await waitForElementShown(icon);
+
+    if (!customizing) {
+      info("Open overflow menu");
+      let menu = document.getElementById("widget-overflow");
+      let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+      overflowButton.click();
+      await shown;
+    }
+
+    info("Test overflow menu context menu in browserAction");
+    await testContextMenu("customizationPanelItemContextMenu", customizing);
+
+    info("Restore initial state");
+    CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_NAVBAR);
+
+    if (customizing) {
+      info("Exit customize mode");
+      let afterCustomization = BrowserTestUtils.waitForEvent(gNavToolbox, "aftercustomization");
+      gCustomizeMode.exit();
+      await afterCustomization;
+    }
+  }
+
+  await extension.startup();
+
+  info("Run tests in normal mode");
+  await main(false);
+
+  info("Run tests in customize mode");
+  await main(true);
+
+  let addon = await AddonManager.getAddonByID(id);
+  ok(addon, "Addon is still installed");
+
+  promptService._response = 0;
+  let uninstalled = new Promise((resolve) => {
+    AddonManager.addAddonListener({
+      onUninstalled(addon) {
+        is(addon.id, id, "The expected add-on has been uninstalled");
+        AddonManager.removeAddonListener(this);
+        resolve();
+      },
+    });
+  });
+  await testContextMenu("toolbar-context-menu", false);
+  await uninstalled;
+
+  addon = await AddonManager.getAddonByID(id);
+  ok(!addon, "Addon has been uninstalled");
+
+  await extension.unload();
+});
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -438,16 +438,18 @@ These should match what Safari and other
 <!ENTITY customizeMenu.removeFromToolbar.label "Remove from Toolbar">
 <!ENTITY customizeMenu.removeFromToolbar.accesskey "R">
 <!ENTITY customizeMenu.addMoreItems.label "Add More Items…">
 <!ENTITY customizeMenu.addMoreItems.accesskey "A">
 <!ENTITY customizeMenu.autoHideDownloadsButton.label "Auto-Hide in Toolbar">
 <!ENTITY customizeMenu.autoHideDownloadsButton.accesskey "A">
 <!ENTITY customizeMenu.manageExtension.label "Manage Extension">
 <!ENTITY customizeMenu.manageExtension.accesskey "E">
+<!ENTITY customizeMenu.removeExtension.label "Remove Extension">
+<!ENTITY customizeMenu.removeExtension.accesskey "v">
 
 <!-- LOCALIZATION NOTE (moreMenu.label) This label is used in the new Photon
     app (hamburger) menu. When clicked, it opens a subview that contains
     secondary commands. -->
 <!ENTITY moreMenu.label "More">
 
 <!ENTITY openCmd.commandkey           "l">
 <!ENTITY urlbar.placeholder2          "Search or enter address">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -153,16 +153,25 @@ webextPerms.hostDescription.tooManySites
 # %2$S is replaced with the name of the current search engine
 # %3$S is replaced with the name of the new search engine
 webext.defaultSearch.description=%1$S would like to change your default search engine from %2$S to %3$S. Is that OK?
 webext.defaultSearchYes.label=Yes
 webext.defaultSearchYes.accessKey=Y
 webext.defaultSearchNo.label=No
 webext.defaultSearchNo.accessKey=N
 
+# LOCALIZATION NOTE (webext.remove.confirmation.title)
+# %S is the name of the extension which is about to be removed.
+webext.remove.confirmation.title=Remove %S
+# LOCALIZATION NOTE (webext.remove.confirmation.message)
+# %1$S is the name of the extension which is about to be removed.
+# %2$S is brandShorterName
+webext.remove.confirmation.message=Remove %1$S from %2$S?
+webext.remove.confirmation.button=Remove
+
 # LOCALIZATION NOTE (addonPostInstall.message1)
 # %1$S is replaced with the localized named of the extension that was
 # just installed.
 # %2$S is replaced with the localized name of the application.
 addonPostInstall.message1=%1$S has been added to %2$S.
 addonPostInstall.okay.label=OK
 addonPostInstall.okay.key=O