Bug 1246034: Part 1 - [webext] Add a helper function to trigger a browserAction. r?Gijs
MozReview-Commit-ID: JUW6oUpWiN4
--- 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);
}
}