Bug 1246034: Part 1 - [webext] Add a helper function to trigger a browserAction. r?Gijs draft
authorKris Maglione <maglione.k@gmail.com>
Mon, 07 Mar 2016 16:46:26 -0800
changeset 337575 e515811dd2ac97183eccaaac070e908fc869247a
parent 337180 b29add2c129122e4af8582cac68259c0cc96baec
child 515701 a489befbc6021d6ff24f81a481efc28704b4e882
push id12402
push usermaglione.k@gmail.com
push dateTue, 08 Mar 2016 02:03:32 +0000
reviewersGijs
bugs1246034
milestone47.0a1
Bug 1246034: Part 1 - [webext] Add a helper function to trigger a browserAction. r?Gijs MozReview-Commit-ID: JUW6oUpWiN4
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-utils.js
browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
toolkit/components/extensions/ExtensionUtils.jsm
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -1,31 +1,34 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://devtools/shared/event-emitter.js");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   EventManager,
 } = ExtensionUtils;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> BrowserAction]
 var browserActionMap = new WeakMap();
 
 function browserActionOf(extension) {
   return browserActionMap.get(extension);
 }
 
+global.browserActionOf = browserActionOf;
+
 // Responsible for the browser_action section of the manifest as well
 // as the associated popup.
 function BrowserAction(options, extension) {
   this.extension = extension;
 
   let widgetId = makeWidgetId(extension.id);
   this.id = `${widgetId}-browser-action`;
   this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
@@ -110,16 +113,49 @@ BrowserAction.prototype = {
     });
 
     this.tabContext.on("tab-select", // eslint-disable-line mozilla/balanced-listeners
                        (evt, tab) => { this.updateWindow(tab.ownerDocument.defaultView); });
 
     this.widget = widget;
   },
 
+  /**
+   * Triggers this browser action for the given window, with the same effects as
+   * if it were clicked by a user.
+   *
+   * This has no effect if the browser action is disabled for, or not
+   * present in, the given window.
+   */
+  triggerAction: Task.async(function* (window) {
+    let popup = ViewPopup.for(this.extension, window);
+    if (popup) {
+      popup.closePopup();
+      return;
+    }
+
+    let widget = this.widget.forWindow(window);
+    let tab = window.gBrowser.selectedTab;
+
+    if (!(widget && this.getProperty(tab, "enabled"))) {
+      return;
+    }
+
+    if (this.getProperty(tab, "popup")) {
+      if (this.widget.areaType == CustomizableUI.TYPE_MENU_PANEL) {
+        yield window.PanelUI.show();
+      }
+
+      let event = new window.CustomEvent("command", {bubbles: true, cancelable: true});
+      widget.node.dispatchEvent(event);
+    } else {
+      this.emit("click");
+    }
+  }),
+
   // Update the toolbar button |node| with the tab context data
   // in |tabData|.
   updateButton(node, tabData) {
     let title = tabData.title || this.extension.name;
     node.setAttribute("tooltiptext", title);
     node.setAttribute("label", title);
 
     if (tabData.badgeText) {
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -10,16 +10,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/AddonManager.jsm");
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 const INTEGER = /^[1-9]\d*$/;
 
 var {
+  DefaultWeakMap,
   EventManager,
   instanceOf,
 } = ExtensionUtils;
 
 // This file provides some useful code for the |tabs| and |windows|
 // modules. All of the code is installed on |global|, which is a scope
 // shared among the different ext-*.js scripts.
 
@@ -154,29 +155,37 @@ class BasePopup {
     this.contentReady = new Promise(resolve => {
       this._resolveContentReady = resolve;
     });
 
     this.viewNode.addEventListener(this.DESTROY_EVENT, this);
 
     this.browser = null;
     this.browserReady = this.createBrowser(viewNode, popupURI);
+
+    BasePopup.instances.get(this.window).set(extension, this);
+  }
+
+  static for(extension, window) {
+    return BasePopup.instances.get(window).get(extension);
   }
 
   destroy() {
     this.browserReady.then(() => {
       this.browser.removeEventListener("load", this, true);
       this.browser.removeEventListener("DOMTitleChanged", this, true);
       this.browser.removeEventListener("DOMWindowClose", this, true);
 
       this.viewNode.removeEventListener(this.DESTROY_EVENT, this);
 
       this.context.unload();
       this.browser.remove();
 
+      BasePopup.instances.get(this.window).delete(this.extension);
+
       this.browser = null;
       this.viewNode = null;
       this.context = null;
     });
   }
 
   // Returns the name of the event fired on `viewNode` when the popup is being
   // destroyed. This must be implemented by every subclass.
@@ -293,16 +302,23 @@ class BasePopup {
 
     this.browser.style.width = `${width}px`;
     this.browser.style.height = `${height}px`;
 
     this._resolveContentReady();
   }
 }
 
+/**
+ * A map of active popups for a given browser window.
+ *
+ * WeakMap[window -> WeakMap[Extension -> BasePopup]]
+ */
+BasePopup.instances = new DefaultWeakMap(() => new WeakMap());
+
 global.PanelPopup = class PanelPopup extends BasePopup {
   constructor(extension, imageNode, popupURL) {
     let document = imageNode.ownerDocument;
 
     let panel = document.createElement("panel");
     panel.setAttribute("id", makeWidgetId(extension.id) + "-panel");
     panel.setAttribute("class", "browser-extension-panel");
     panel.setAttribute("type", "arrow");
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
@@ -1,12 +1,19 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+function getBrowserAction(extension) {
+  let {GlobalManager, browserActionOf} = Cu.import("resource://gre/modules/Extension.jsm", {});
+
+  let ext = GlobalManager.extensionMap.get(extension.id);
+  return browserActionOf(ext);
+}
+
 function* testInArea(area) {
   let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head></html>`;
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "background": {
         "page": "data/background.html",
       },
@@ -32,78 +39,98 @@ function* testInArea(area) {
       },
 
       "data/background.html": scriptPage("background.js"),
 
       "data/background.js": function() {
         let sendClick;
         let tests = [
           () => {
+            browser.test.log(`Click browser action, expect popup "a".`);
+            sendClick({expectEvent: false, expectPopup: "a"});
+          },
+          () => {
+            browser.test.log(`Click browser action again, expect popup "a".`);
             sendClick({expectEvent: false, expectPopup: "a"});
           },
           () => {
-            sendClick({expectEvent: false, expectPopup: "a"});
+            browser.test.log(`Call triggerAction, expect popup "a" again. Leave popup open.`);
+            sendClick({expectEvent: false, expectPopup: "a", leaveOpen: true}, "trigger-action");
           },
           () => {
+            browser.test.log(`Call triggerAction again. Expect remaining popup closed.`);
+            sendClick({expectEvent: false, expectPopup: null}, "trigger-action");
+            browser.test.sendMessage("next-test", {awaitClosed: true});
+          },
+          () => {
+            browser.test.log(`Call triggerAction again. Expect popup "a" again.`);
+            sendClick({expectEvent: false, expectPopup: "a"}, "trigger-action");
+          },
+          () => {
+            browser.test.log(`Change popup URL. Click browser action. Expect popup "b".`);
             browser.browserAction.setPopup({popup: "popup-b.html"});
             sendClick({expectEvent: false, expectPopup: "b"});
           },
           () => {
+            browser.test.log(`Click browser action again. Expect popup "b" again.`);
             sendClick({expectEvent: false, expectPopup: "b"});
           },
           () => {
+            browser.test.log(`Clear popup URL. Click browser action. Expect click event.`);
             browser.browserAction.setPopup({popup: ""});
             sendClick({expectEvent: true, expectPopup: null});
           },
           () => {
+            browser.test.log(`Click browser action again. Expect another click event.`);
             sendClick({expectEvent: true, expectPopup: null});
           },
           () => {
-            browser.browserAction.setPopup({popup: "/popup-a.html"});
-            sendClick({expectEvent: false, expectPopup: "a", runNextTest: true});
+            browser.test.log(`Call triggerAction. Expect click event.`);
+            sendClick({expectEvent: true, expectPopup: null}, "trigger-action");
           },
           () => {
+            browser.test.log(`Change popup URL. Click browser action. Expect popup "a", and leave open.`);
+            browser.browserAction.setPopup({popup: "/popup-a.html"});
+            sendClick({expectEvent: false, expectPopup: "a", leaveOpen: true});
+          },
+          () => {
+            browser.test.log(`Call window.close(). Expect popup closed.`);
             browser.test.sendMessage("next-test", {expectClosed: true});
           },
         ];
 
         let expect = {};
-        sendClick = ({expectEvent, expectPopup, runNextTest}) => {
-          expect = {event: expectEvent, popup: expectPopup, runNextTest};
-          browser.test.sendMessage("send-click");
+        sendClick = ({expectEvent, expectPopup, runNextTest, awaitClosed, leaveOpen}, message = "send-click") => {
+          expect = {event: expectEvent, popup: expectPopup, runNextTest, awaitClosed, leaveOpen};
+          browser.test.sendMessage(message);
         };
 
         browser.runtime.onMessage.addListener(msg => {
           if (msg == "close-popup") {
             return;
           } else if (expect.popup) {
             browser.test.assertEq(msg, `from-popup-${expect.popup}`,
                                   "expected popup opened");
           } else {
             browser.test.fail(`unexpected popup: ${msg}`);
           }
 
           expect.popup = null;
-          if (expect.runNextTest) {
-            expect.runNextTest = false;
-            tests.shift()();
-          } else {
-            browser.test.sendMessage("next-test");
-          }
+          browser.test.sendMessage("next-test", expect);
         });
 
         browser.browserAction.onClicked.addListener(() => {
           if (expect.event) {
             browser.test.succeed("expected click event received");
           } else {
             browser.test.fail("unexpected click event");
           }
 
           expect.event = false;
-          browser.test.sendMessage("next-test");
+          browser.test.sendMessage("next-test", expect);
         });
 
         browser.test.onMessage.addListener((msg) => {
           if (msg == "close-popup") {
             browser.runtime.sendMessage("close-popup");
             return;
           }
 
@@ -123,32 +150,41 @@ function* testInArea(area) {
       },
     },
   });
 
   extension.onMessage("send-click", () => {
     clickBrowserAction(extension);
   });
 
+  extension.onMessage("trigger-action", () => {
+    getBrowserAction(extension).triggerAction(window);
+  });
+
   let widget;
   extension.onMessage("next-test", Task.async(function* (expecting = {}) {
     if (!widget) {
       widget = getBrowserActionWidget(extension);
       CustomizableUI.addWidgetToArea(widget.id, area);
     }
-    if (expecting.expectClosed) {
+    if (expecting.awaitClosed) {
+      let panel = getBrowserActionPopup(extension);
+      if (panel && panel.state != "closed") {
+        yield promisePopupHidden(panel);
+      }
+    } else if (expecting.expectClosed) {
       let panel = getBrowserActionPopup(extension);
       ok(panel, "Expect panel to exist");
       yield promisePopupShown(panel);
 
       extension.sendMessage("close-popup");
 
       yield promisePopupHidden(panel);
       ok(true, "Panel is closed");
-    } else {
+    } else if (!expecting.leaveOpen) {
       yield closeBrowserAction(extension);
     }
 
     extension.sendMessage("next-test");
   }));
 
   yield Promise.all([extension.startup(), extension.awaitFinish("browseraction-tests-done")]);
 
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -89,39 +89,34 @@ function extend(obj, ...args) {
       let descriptor = Object.getOwnPropertyDescriptor(arg, prop);
       Object.defineProperty(obj, prop, descriptor);
     }
   }
 
   return obj;
 }
 
-// Similar to a WeakMap, but returns a particular default value for
-// |get| if a key is not present.
-function DefaultWeakMap(defaultValue) {
-  this.defaultValue = defaultValue;
-  this.weakmap = new WeakMap();
-}
+/**
+ * Similar to a WeakMap, but creates a new key with the given
+ * constructor if one is not present.
+ */
+class DefaultWeakMap extends WeakMap {
+  constructor(defaultConstructor) {
+    super();
+    this.defaultConstructor = defaultConstructor;
+  }
 
-DefaultWeakMap.prototype = {
   get(key) {
-    if (this.weakmap.has(key)) {
-      return this.weakmap.get(key);
+    if (!this.has(key)) {
+      this.set(key, this.defaultConstructor());
     }
-    return this.defaultValue;
-  },
 
-  set(key, value) {
-    if (key) {
-      this.weakmap.set(key, value);
-    } else {
-      this.defaultValue = value;
-    }
-  },
-};
+    return super.get(key);
+  }
+}
 
 class SpreadArgs extends Array {
   constructor(args) {
     super();
     this.push(...args);
   }
 }