Bug 1324255 make webextension panels focused when opened, r?Gijs,rpl draft
authorShane Caraveo <scaraveo@mozilla.com>
Tue, 13 Feb 2018 16:16:36 -0700
changeset 754695 3f2166d1338d6c44502b5bf64ba3a57b2673ff63
parent 754572 e43f2f6ea111c2d059d95fa9a71516b869a69698
push id98955
push usermixedpuppy@gmail.com
push dateTue, 13 Feb 2018 23:17:05 +0000
reviewersGijs, rpl
bugs1324255
milestone60.0a1
Bug 1324255 make webextension panels focused when opened, r?Gijs,rpl MozReview-Commit-ID: DfLPUOqlPc6
browser/components/extensions/ExtensionPopups.jsm
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_currentWindow.js
browser/components/extensions/test/browser/browser_ext_getViews.js
browser/components/extensions/test/browser/browser_ext_popup_focus.js
toolkit/components/extensions/ext-browser-content.js
--- a/browser/components/extensions/ExtensionPopups.jsm
+++ b/browser/components/extensions/ExtensionPopups.jsm
@@ -73,16 +73,17 @@ class BasePopup {
 
     extension.callOnClose(this);
 
     this.contentReady = new Promise(resolve => {
       this._resolveContentReady = resolve;
     });
 
     this.viewNode.addEventListener(this.DESTROY_EVENT, this);
+    this.panel.addEventListener("popuppositioned", this, {once: true, capture: true});
 
     this.browser = null;
     this.browserLoaded = new Promise((resolve, reject) => {
       this.browserLoadedDeferred = {resolve, reject};
     });
     this.browserReady = this.createBrowser(viewNode, popupURL);
 
     BasePopup.instances.get(this.window).set(extension, this);
@@ -206,16 +207,26 @@ class BasePopup {
 
   handleEvent(event) {
     switch (event.type) {
       case this.DESTROY_EVENT:
         if (!this.destroyed) {
           this.destroy();
         }
         break;
+      case "popuppositioned":
+        if (!this.destroyed) {
+          this.browserLoaded.then(() => {
+            if (this.destroyed) {
+              return;
+            }
+            this.browser.messageManager.sendAsyncMessage("Extension:GrabFocus", {});
+          });
+        }
+        break;
     }
   }
 
   createBrowser(viewNode, popupURL = null) {
     let document = viewNode.ownerDocument;
 
     let stack = document.createElementNS(XUL_NS, "stack");
     stack.setAttribute("class", "webextension-popup-stack");
@@ -433,16 +444,17 @@ class ViewPopup extends BasePopup {
    *        browser was destroyed before it was fully loaded, and the popup
    *        should be closed, or `true` otherwise.
    */
   async attach(viewNode) {
     this.viewNode = viewNode;
     this.viewNode.addEventListener(this.DESTROY_EVENT, this);
     this.viewNode.setAttribute("closemenu", "none");
 
+    this.panel.addEventListener("popuppositioned", this, {once: true, capture: true});
     if (this.extension.remote) {
       this.panel.setAttribute("remote", "true");
     }
 
     // Wait until the browser element is fully initialized, and give it at least
     // a short grace period to finish loading its initial content, if necessary.
     //
     // In practice, the browser that was created by the mousdown handler should
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -111,16 +111,17 @@ skip-if = (os == 'win' && ccov) # Bug 14
 [browser_ext_pageAction_popup_resize.js]
 [browser_ext_pageAction_show_matches.js]
 [browser_ext_pageAction_simple.js]
 [browser_ext_pageAction_telemetry.js]
 [browser_ext_pageAction_title.js]
 [browser_ext_popup_api_injection.js]
 [browser_ext_popup_background.js]
 [browser_ext_popup_corners.js]
+[browser_ext_popup_focus.js]
 [browser_ext_popup_sendMessage.js]
 [browser_ext_popup_shutdown.js]
 [browser_ext_runtime_openOptionsPage.js]
 [browser_ext_runtime_openOptionsPage_uninstall.js]
 [browser_ext_runtime_setUninstallURL.js]
 [browser_ext_sessions_forgetClosedTab.js]
 [browser_ext_sessions_forgetClosedWindow.js]
 [browser_ext_sessions_getRecentlyClosed.js]
--- a/browser/components/extensions/test/browser/browser_ext_currentWindow.js
+++ b/browser/components/extensions/test/browser/browser_ext_currentWindow.js
@@ -99,39 +99,35 @@ add_task(async function() {
     extension.sendMessage(kind + "-check-current1");
     is((await extension.awaitMessage("result")), winId, `${name} is on top (check 1) [${kind}]`);
     extension.sendMessage(kind + "-check-current2");
     is((await extension.awaitMessage("result")), winId, `${name} is on top (check 2) [${kind}]`);
     extension.sendMessage(kind + "-check-current3");
     is((await extension.awaitMessage("result")), winId, `${name} is on top (check 3) [${kind}]`);
   }
 
-  await focusWindow(win1);
-  await checkWindow("background", winId1, "win1");
-  await focusWindow(win2);
-  await checkWindow("background", winId2, "win2");
-
   async function triggerPopup(win, callback) {
     await clickBrowserAction(extension, win);
     await awaitExtensionPanel(extension, win);
 
     await extension.awaitMessage("popup-ready");
 
     await callback();
 
     closeBrowserAction(extension, win);
   }
 
-  // Set focus to some other window.
-  await focusWindow(window);
-
+  await focusWindow(win1);
+  await checkWindow("background", winId1, "win1");
   await triggerPopup(win1, async function() {
     await checkWindow("popup", winId1, "win1");
   });
 
+  await focusWindow(win2);
+  await checkWindow("background", winId2, "win2");
   await triggerPopup(win2, async function() {
     await checkWindow("popup", winId2, "win2");
   });
 
   async function triggerPage(winId, name) {
     extension.sendMessage("background-open-page", winId);
     await extension.awaitMessage("page-ready");
     await checkWindow("page", winId, name);
--- a/browser/components/extensions/test/browser/browser_ext_getViews.js
+++ b/browser/components/extensions/test/browser/browser_ext_getViews.js
@@ -143,32 +143,28 @@ add_task(async function() {
 
   let tabId2 = await openTab(winId2);
 
   await checkViews("background", 2, 0, 0);
   await checkViewsWithFilter({windowId: winId2}, 1);
   await checkViewsWithFilter({tabId: tabId2}, 1);
 
   async function triggerPopup(win, callback) {
+    // Window needs focus to open popups.
+    await focusWindow(win);
     await clickBrowserAction(extension, win);
     await awaitExtensionPanel(extension, win);
 
     await extension.awaitMessage("popup-ready");
 
     await callback();
 
     closeBrowserAction(extension, win);
   }
 
-  // The popup occasionally closes prematurely if we open it immediately here.
-  // I'm not sure what causes it to close (it's something internal, and seems to
-  // be focus-related, but it's not caused by JS calling hidePopup), but even a
-  // short timeout seems to consistently fix it.
-  await new Promise(resolve => win1.setTimeout(resolve, 10));
-
   await triggerPopup(win1, async function() {
     await checkViews("background", 2, 1, 0);
     await checkViews("popup", 2, 1, 1);
     await checkViewsWithFilter({windowId: winId1}, 2);
     await checkViewsWithFilter({type: "popup", tabId: -1}, 1);
   });
 
   await triggerPopup(win2, async function() {
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_focus.js
@@ -0,0 +1,73 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const DUMMY_PAGE = "http://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+
+add_task(async function testPageActionFocus() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "page_action": {
+        "default_popup": "popup.html",
+        "show_matches": ["<all_urls>"],
+      },
+    },
+    files: {
+      "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8">
+        <script src="popup.js"></script>
+        </head><body>
+        </body></html>
+      `,
+      "popup.js": function() {
+        window.addEventListener("focus", (event) => {
+          browser.test.assertEq(true, document.hasFocus(), "document should be focused");
+          browser.test.notifyPass("focused");
+        }, {once: true});
+      },
+    },
+  });
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, DUMMY_PAGE);
+
+  await extension.startup();
+  let finish = extension.awaitFinish("focused");
+  clickPageAction(extension);
+  await finish;
+
+  let panelId = `${makeWidgetId(extension.id)}-panel`;
+  let panel = document.getElementById(panelId);
+  panel.hidePopup();
+  await BrowserTestUtils.removeTab(tab);
+  await extension.unload();
+});
+
+add_task(async function testBrowserActionFocus() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "browser_action": {"default_popup": "popup.html"},
+    },
+    files: {
+      "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8">
+        <script src="popup.js"></script>
+        </head><body>
+        </body></html>
+      `,
+      "popup.js": function() {
+        window.addEventListener("focus", (event) => {
+          browser.test.assertEq(true, document.hasFocus(), "document should be focused");
+          browser.test.notifyPass("focused");
+        }, {once: true});
+      },
+    },
+  });
+  await extension.startup();
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, DUMMY_PAGE);
+  let finish = extension.awaitFinish("focused");
+  clickBrowserAction(extension);
+  await finish;
+
+  await closeBrowserAction(extension);
+
+  await BrowserTestUtils.removeTab(tab);
+  await extension.unload();
+});
--- a/toolkit/components/extensions/ext-browser-content.js
+++ b/toolkit/components/extensions/ext-browser-content.js
@@ -109,16 +109,20 @@ const BrowserListener = {
   receiveMessage({name, data}) {
     if (name === "Extension:InitBrowser") {
       this.init(data);
     } else if (name === "Extension:UnblockParser") {
       if (this.unblockParser) {
         this.unblockParser();
         this.blockingPromise = null;
       }
+    } else if (name === "Extension:GrabFocus") {
+      content.window.requestAnimationFrame(() => {
+        Services.focus.focusedWindow = content.window;
+      });
     }
   },
 
   loadStylesheets() {
     let winUtils = getWinUtils(content);
 
     for (let url of this.stylesheets) {
       winUtils.addSheet(ExtensionCommon.stylesheetMap.get(url), winUtils.AGENT_SHEET);
@@ -299,16 +303,17 @@ const BrowserListener = {
     }
 
     sendAsyncMessage("Extension:BrowserResized", result);
   },
 };
 
 addMessageListener("Extension:InitBrowser", BrowserListener);
 addMessageListener("Extension:UnblockParser", BrowserListener);
+addMessageListener("Extension:GrabFocus", BrowserListener);
 
 var WebBrowserChrome = {
   onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) {
     // isAppTab is the value for the docShell that received the click.  We're
     // handling this in the top-level frame and want traversal behavior to
     // match the value for this frame rather than any subframe, so we pass
     // through the docShell.isAppTab value rather than what we were handed.
     return BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, docShell.isAppTab);