Bug 1468460 - Support context menu in WebExtensions options pages embedded inside an about:addons tab. r=mixedpuppy,robwu
authorLuca Greco <lgreco@mozilla.com>
Mon, 07 Jan 2019 20:53:13 +0000
changeset 510214 d3f022310ff3c5864f62f979e85c7ff0a27de82b
parent 510213 31e16c2d94299dcc7076b024981b1997a264e5e1
child 510215 dd5395e679a627a326a3a8b305c3f5f44329ddfa
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, robwu
bugs1468460
milestone66.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 1468460 - Support context menu in WebExtensions options pages embedded inside an about:addons tab. r=mixedpuppy,robwu Differential Revision: https://phabricator.services.mozilla.com/D9920
browser/base/content/nsContextMenu.js
browser/components/extensions/test/browser/browser-remote.ini
browser/components/extensions/test/browser/browser_ext_optionsPage_popups.js
browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/content/extensions.xul
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -235,22 +235,24 @@ nsContextMenu.prototype = {
     if (this.isRemote) {
       this.browser = gContextMenuContentData.browser;
       this.selectionInfo = gContextMenuContentData.selectionInfo;
     } else {
       this.browser = this.ownerDoc.defaultView.docShell.chromeEventHandler;
       this.selectionInfo = BrowserUtils.getSelectionDetails(window);
     }
 
+    const {gBrowser} = this.browser.ownerGlobal;
+
     this.textSelected      = this.selectionInfo.text;
     this.isTextSelected    = this.textSelected.length != 0;
     this.webExtBrowserType = this.browser.getAttribute("webextension-view-type");
     this.inWebExtBrowser   = !!this.webExtBrowserType;
-    this.inTabBrowser      = this.browser.ownerGlobal.gBrowser ?
-      !!this.browser.ownerGlobal.gBrowser.getTabForBrowser(this.browser) : false;
+    this.inTabBrowser      = gBrowser && gBrowser.getTabForBrowser ?
+      !!gBrowser.getTabForBrowser(this.browser) : false;
 
     if (context.shouldInitInlineSpellCheckerUINoChildren) {
       if (this.isRemote) {
         InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
       } else {
         InlineSpellCheckerUI.init(this.target.editor);
         InlineSpellCheckerUI.initFromEvent(document.popupRangeParent,
                                            document.popupRangeOffset);
--- a/browser/components/extensions/test/browser/browser-remote.ini
+++ b/browser/components/extensions/test/browser/browser-remote.ini
@@ -7,13 +7,13 @@
 # whether we're running from that directory from head.js
 install-to-subdir = test-oop-extensions
 tags = webextensions remote-webextensions
 skip-if = !e10s
 support-files =
   head.js
 
 [browser_ext_contentscript_nontab_connect.js]
-
+[browser_ext_optionsPage_popups.js]
 [browser_ext_popup_select.js]
 skip-if = debug || os != 'win' # FIXME: re-enable on debug build (bug 1442822)
 
 [include:browser-common.ini]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_popups.js
@@ -0,0 +1,90 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_tab_options_popups() {
+  function backgroundScript() {
+    browser.runtime.openOptionsPage();
+  }
+
+  function optionsScript() {
+    browser.test.sendMessage("options-page:loaded");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "temporary",
+
+    manifest: {
+      "permissions": ["tabs"],
+      "options_ui": {
+        "page": "options.html",
+      },
+    },
+    files: {
+      "options.html": `<!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+            <script src="options.js" type="text/javascript"></script>
+          </head>
+          <body style="height: 100px;">
+            <h1>Extensions Options</h1>
+            <a href="http://mochi.test:8888/">options page link</a>
+          </body>
+        </html>`,
+      "options.js": optionsScript,
+    },
+    background: backgroundScript,
+  });
+
+  const aboutAddonsTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:addons");
+
+  await extension.startup();
+
+  // Wait the options page to be loaded.
+  await extension.awaitMessage("options-page:loaded");
+
+  const optionsBrowser = gBrowser.selectedBrowser.contentDocument.getElementById("addon-options");
+  let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+  let popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
+
+  await BrowserTestUtils.waitForCondition(async () => {
+    BrowserTestUtils.synthesizeMouseAtCenter("a", {type: "contextmenu"}, optionsBrowser);
+
+    // It looks that syntesizeMouseAtCenter is sometimes able to trigger the mouse event on the
+    // HTML document instead of triggering it on the expected document element while running this
+    // test in --verify mode, and we are going to send the mouse event again if it didn't
+    // triggered the context menu yet.
+    return Promise.race([
+      popupShownPromise.then(() => true),
+      delay(500).then(() => false),
+    ]);
+  }, "Waiting the context menu to be shown");
+
+  let contextMenuItemIds = [
+    "context-openlinkintab",
+    "context-openlinkprivate",
+    "context-copylink",
+    "context-openlinkinusercontext-menu",
+  ];
+
+  for (const itemID of contextMenuItemIds) {
+    const item = contentAreaContextMenu.querySelector(`#${itemID}`);
+
+    ok(!item.hidden, `${itemID} should not be hidden`);
+    ok(!item.disabled, `${itemID} should not be disabled`);
+  }
+
+  // Close the context menu and ensure that it is gone, otherwise there
+  // may be intermittent failures related to shutdown leaks.
+  await BrowserTestUtils.waitForCondition(async () => {
+    let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
+    contentAreaContextMenu.hidePopup();
+    await popupHiddenPromise;
+    return contentAreaContextMenu.state === "closed";
+  }, "Wait context menu popup to be closed");
+
+  BrowserTestUtils.removeTab(aboutAddonsTab);
+
+  await extension.unload();
+});
--- a/browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js
@@ -1,34 +1,40 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 add_task(async function test_tab_options_privileges() {
   function backgroundScript() {
-    browser.runtime.onMessage.addListener(({msgName, tabId}) => {
-      if (msgName == "removeTabId") {
-        browser.tabs.remove(tabId).then(() => {
+    browser.runtime.onMessage.addListener(async ({msgName, tab}) => {
+      if (msgName == "removeTab") {
+        try {
+          const [activeTab] = await browser.tabs.query({active: true});
+          browser.test.assertEq(tab.id, activeTab.id, "tabs.getCurrent has got the expected tabId");
+          browser.test.assertEq(tab.windowId, activeTab.windowId,
+                                "tabs.getCurrent has got the expected windowId");
+          await browser.tabs.remove(tab.id);
+
           browser.test.notifyPass("options-ui-privileges");
-        }).catch(error => {
+        } catch (error) {
           browser.test.log(`Error: ${error} :: ${error.stack}`);
           browser.test.notifyFail("options-ui-privileges");
-        });
+        }
       }
     });
     browser.runtime.openOptionsPage();
   }
 
   async function optionsScript() {
     try {
       let [tab] = await browser.tabs.query({url: "http://example.com/"});
       browser.test.assertEq("http://example.com/", tab.url, "Got the expect tab");
 
       tab = await browser.tabs.getCurrent();
-      browser.runtime.sendMessage({msgName: "removeTabId", tabId: tab.id});
+      browser.runtime.sendMessage({msgName: "removeTab", tab});
     } catch (error) {
       browser.test.log(`Error: ${error} :: ${error.stack}`);
       browser.test.notifyFail("options-ui-privileges");
     }
   }
 
   const ID = "options_privileges@tests.mozilla.org";
   let extension = ExtensionTestUtils.loadExtension({
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -3093,16 +3093,40 @@ var gDetailView = {
       browser.sameProcessAsFrameLoader = policy.extension.groupFrameLoader;
     }
 
     let readyPromise;
     if (E10SUtils.canLoadURIInRemoteType(optionsURL, E10SUtils.EXTENSION_REMOTE_TYPE)) {
       browser.setAttribute("remote", "true");
       browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
       readyPromise = promiseEvent("XULFrameLoaderCreated", browser);
+
+      readyPromise.then(() => {
+        if (!browser.messageManager) {
+          // Early exit if the the extension page's XUL browser has been destroyed in the meantime
+          // (e.g. because the extension has been reloaded while the options page was still loading).
+          return;
+        }
+        const parentChromeWindow = window.docShell.parent.domWindow;
+        const parentContextMenuPopup = parentChromeWindow.document.getElementById("contentAreaContextMenu");
+
+        // Override openPopupAtScreen on the dummy menupopup element, so that we can forward
+        // "nsContextMenu.js openContextMenu"'s calls related to the extensions "options page"
+        // context menu events.
+        document.getElementById("contentAreaContextMenu").openPopupAtScreen = (...args) => {
+          return parentContextMenuPopup.openPopupAtScreen(...args);
+        };
+
+        // Subscribe a "contextmenu" listener to handle the context menus for the extension option page
+        // running in the extension process (the context menu will be handled only for extension running
+        // in OOP mode, but that's ok as it is the default on any platform that uses these extensions
+        // options pages).
+        browser.messageManager.addMessageListener(
+          "contextmenu", message => parentChromeWindow.openContextMenu(message));
+      });
     } else {
       readyPromise = promiseEvent("load", browser, true);
     }
 
     stack.appendChild(browser);
     parentNode.appendChild(stack);
 
     // Force bindings to apply synchronously.
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -71,16 +71,23 @@
     <panel type="autocomplete-richlistbox"
            id="PopupAutoComplete"
            noautofocus="true"
            hidden="true"
            norolluponanchor="true"
            nomaxresults="true" />
 
     <tooltip id="addonitem-tooltip"/>
+
+    <menupopup id="contentAreaContextMenu"
+               onpopupshowing="Cu.reportError('This dummy menupopup is not supposed to be shown');
+                               return false">
+      <!-- a dummy element used to forward the context menu related to the extension's
+           options page XUL browsers to the context menu defined in the parent chrome window -->
+    </menupopup>
   </popupset>
 
   <!-- global commands - these act on all addons, or affect the addons manager
        in some other way -->
   <commandset id="globalCommandSet">
     <!-- XXXsw remove useless oncommand attribute once bug 371900 is fixed -->
     <command id="cmd_focusSearch" oncommand=";"/>
     <command id="cmd_findAllUpdates"/>