copy from browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
copy to browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js
@@ -1,261 +1,94 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
-function* testInArea(area) {
+add_task(function* test_popup_sendMessage_reply() {
let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head><body>${url}</body></html>`;
let extension = ExtensionTestUtils.loadExtension({
manifest: {
- "background": {
- "page": "data/background.html",
- },
- "browser_action": {
- "default_popup": "popup-a.html",
- "browser_style": true,
- },
- },
-
- files: {
- "popup-a.html": scriptPage("popup-a.js"),
- "popup-a.js": function() {
- window.onload = () => {
- let color = window.getComputedStyle(document.body).color;
- browser.test.assertEq("rgb(34, 36, 38)", color);
- browser.runtime.sendMessage("from-popup-a");
- };
- browser.runtime.onMessage.addListener(msg => {
- if (msg == "close-popup") {
- window.close();
- }
- });
- },
-
- "data/popup-b.html": scriptPage("popup-b.js"),
- "data/popup-b.js": function() {
- window.onload = () => {
- browser.runtime.sendMessage("from-popup-b");
- };
- },
-
- "data/popup-c.html": scriptPage("popup-c.js"),
- "data/popup-c.js": function() {
- // Close the popup before the document is fully-loaded to make sure that
- // we handle this case sanely.
- browser.runtime.sendMessage("from-popup-c");
- window.close();
- },
-
- "data/background.html": scriptPage("background.js"),
-
- "data/background.js": function() {
- let sendClick;
- let tests = [
- () => {
- browser.test.log("Open popup a");
- sendClick({expectEvent: false, expectPopup: "a"});
- },
- () => {
- browser.test.log("Open popup a again");
- sendClick({expectEvent: false, expectPopup: "a"});
- },
- () => {
- browser.test.log("Open popup c");
- browser.browserAction.setPopup({popup: "popup-c.html"});
- sendClick({expectEvent: false, expectPopup: "c", closePopup: false});
- },
- () => {
- browser.test.log("Open popup b");
- browser.browserAction.setPopup({popup: "popup-b.html"});
- sendClick({expectEvent: false, expectPopup: "b"});
- },
- () => {
- browser.test.log("Open popup b again");
- sendClick({expectEvent: false, expectPopup: "b"});
- },
- () => {
- browser.browserAction.setPopup({popup: ""});
- sendClick({expectEvent: true, expectPopup: null});
- },
- () => {
- sendClick({expectEvent: true, expectPopup: null});
- },
- () => {
- browser.browserAction.setPopup({popup: "/popup-a.html"});
- sendClick({expectEvent: false, expectPopup: "a", runNextTest: true});
- },
- () => {
- browser.test.sendMessage("next-test", {expectClosed: true});
- },
- ];
-
- let expect = {};
- sendClick = ({expectEvent, expectPopup, runNextTest, closePopup}) => {
- if (closePopup == undefined) {
- closePopup = true;
- }
-
- expect = {event: expectEvent, popup: expectPopup, runNextTest, closePopup};
- browser.test.sendMessage("send-click");
- };
-
- 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", {closePopup: expect.closePopup});
- }
- });
-
- 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.onMessage.addListener((msg) => {
- if (msg == "close-popup") {
- browser.runtime.sendMessage("close-popup");
- return;
- }
-
- if (msg != "next-test") {
- browser.test.fail("Expecting 'next-test' message");
- }
-
- if (tests.length) {
- let test = tests.shift();
- test();
- } else {
- browser.test.notifyPass("browseraction-tests-done");
- }
- });
-
- browser.test.sendMessage("next-test");
- },
- },
- });
-
- extension.onMessage("send-click", () => {
- clickBrowserAction(extension);
- });
-
- let widget;
- extension.onMessage("next-test", Task.async(function* (expecting = {}) {
- if (!widget) {
- widget = getBrowserActionWidget(extension);
- CustomizableUI.addWidgetToArea(widget.id, area);
- }
- 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 if (expecting.closePopup) {
- yield closeBrowserAction(extension);
- }
-
- extension.sendMessage("next-test");
- }));
-
- yield Promise.all([extension.startup(), extension.awaitFinish("browseraction-tests-done")]);
-
- yield extension.unload();
-
- let view = document.getElementById(widget.viewId);
- is(view, null, "browserAction view removed from document");
-}
-
-add_task(function* testBrowserActionInToolbar() {
- yield testInArea(CustomizableUI.AREA_NAVBAR);
-});
-
-add_task(function* testBrowserActionInPanel() {
- yield testInArea(CustomizableUI.AREA_PANEL);
-});
-
-add_task(function* testBrowserActionClickCanceled() {
- let extension = ExtensionTestUtils.loadExtension({
- manifest: {
"browser_action": {
"default_popup": "popup.html",
"browser_style": true,
},
+
+ "page_action": {
+ "default_popup": "popup.html",
+ "browser_style": true,
+ },
},
files: {
- "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8"></head></html>`,
+ "popup.html": scriptPage("popup.js"),
+ "popup.js": function() {
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "popup-ping") {
+ return Promise.resolve("popup-pong");
+ }
+ });
+
+ browser.runtime.sendMessage("background-ping").then(response => {
+ browser.test.sendMessage("background-ping-response", response);
+ });
+ },
+ },
+
+ background() {
+ browser.tabs.query({active: true, currentWindow: true}).then(([tab]) => {
+ return browser.pageAction.show(tab.id);
+ }).then(() => {
+ browser.test.sendMessage("page-action-ready");
+ });
+
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "background-ping") {
+ browser.runtime.sendMessage("popup-ping").then(response => {
+ browser.test.sendMessage("popup-ping-response", response);
+ });
+
+ return new Promise(resolve => {
+ // Wait long enough that we're relatively sure the docShells have
+ // been swapped. Note that this value is fairly arbitrary. The load
+ // event that triggers the swap should happen almost immediately
+ // after the message is sent. The extra quarter of a second gives us
+ // enough leeway that we can expect to respond after the swap in the
+ // vast majority of cases.
+ setTimeout(resolve, 250);
+ }).then(() => {
+ return "background-pong";
+ });
+ }
+ });
},
});
yield extension.startup();
- const {GlobalManager, Management: {global: {browserActionFor}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
-
- let ext = GlobalManager.extensionMap.get(extension.id);
- let browserAction = browserActionFor(ext);
+ {
+ clickBrowserAction(extension);
- let widget = getBrowserActionWidget(extension).forWindow(window);
-
- // Test canceled click.
- EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mousedown", button: 0}, window);
+ let pong = yield extension.awaitMessage("background-ping-response");
+ is(pong, "background-pong", "Got pong");
- isnot(browserAction.pendingPopup, null, "Have pending popup");
- is(browserAction.pendingPopup.window, window, "Have pending popup for the correct window");
-
- is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
+ pong = yield extension.awaitMessage("popup-ping-response");
+ is(pong, "popup-pong", "Got pong");
- EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mouseup", button: 0}, window);
-
- is(browserAction.pendingPopup, null, "Pending popup was cleared");
- is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
-
- // Test completed click.
- EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mousedown", button: 0}, window);
+ yield closeBrowserAction(extension);
+ }
- isnot(browserAction.pendingPopup, null, "Have pending popup");
- is(browserAction.pendingPopup.window, window, "Have pending popup for the correct window");
+ yield extension.awaitMessage("page-action-ready");
- is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
+ {
+ clickPageAction(extension);
- // We need to do these tests during the mouseup event cycle, since the click
- // and command events will be dispatched immediately after mouseup, and void
- // the results.
- let mouseUpPromise = BrowserTestUtils.waitForEvent(widget.node, "mouseup", event => {
- isnot(browserAction.pendingPopup, null, "Pending popup was not cleared");
- isnot(browserAction.pendingPopupTimeout, null, "Have a pending popup timeout");
- return true;
- });
+ let pong = yield extension.awaitMessage("background-ping-response");
+ is(pong, "background-pong", "Got pong");
- EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mouseup", button: 0}, window);
-
- yield mouseUpPromise;
+ pong = yield extension.awaitMessage("popup-ping-response");
+ is(pong, "popup-pong", "Got pong");
- is(browserAction.pendingPopup, null, "Pending popup was cleared");
- is(browserAction.pendingPopupTimeout, null, "Pending popup timeout was cleared");
-
- yield promisePopupShown(getBrowserActionPopup(extension));
- yield closeBrowserAction(extension);
+ yield closePageAction(extension);
+ }
yield extension.unload();
});
--- a/toolkit/components/extensions/MessageChannel.jsm
+++ b/toolkit/components/extensions/MessageChannel.jsm
@@ -107,17 +107,126 @@ const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
+/**
+ * Acts as a proxy for a message manager or message manager owner, and
+ * tracks docShell swaps so that messages are always sent to the same
+ * receiver, even if it is moved to a different <browser>.
+ *
+ * @param {nsIMessageSender|Element} target
+ * The target message manager on which to send messages, or the
+ * <browser> element which owns it.
+ */
+class MessageManagerProxy {
+ constructor(target) {
+ if (target instanceof Ci.nsIMessageSender) {
+ Object.defineProperty(this, "messageManager", {
+ value: target,
+ configurable: true,
+ writable: true,
+ });
+ } else {
+ this.addListeners(target);
+ }
+ }
+ /**
+ * Disposes of the proxy object, removes event listeners, and drops
+ * all references to the underlying message manager.
+ *
+ * Must be called before the last reference to the proxy is dropped,
+ * unless the underlying message manager or <browser> is also being
+ * destroyed.
+ */
+ dispose() {
+ if (this.eventTarget) {
+ this.removeListeners(this.eventTarget);
+ this.eventTarget = null;
+ } else {
+ this.messageManager = null;
+ }
+ }
+
+ /**
+ * Returns true if the given target is the same as, or owns, the given
+ * message manager.
+ *
+ * @param {nsIMessageSender|MessageManagerProxy|Element} target
+ * The message manager, MessageManagerProxy, or <browser>
+ * element agaisnt which to match.
+ * @param {nsIMessageSender} messageManager
+ * The message manager against which to match `target`.
+ *
+ * @returns {boolean}
+ * True if `messageManager` is the same object as `target`, or
+ * `target` is a MessageManagerProxy or <browser> element that
+ * is tied to it.
+ */
+ static matches(target, messageManager) {
+ return target === messageManager || target.messageManager === messageManager;
+ }
+
+ /**
+ * @property {nsIMessageSender|null} messageManager
+ * The message manager that is currently being proxied. This
+ * may change during the life of the proxy object, so should
+ * not be stored elsewhere.
+ */
+ get messageManager() {
+ return this.eventTarget && this.eventTarget.messageManager;
+ }
+
+ /**
+ * Sends a message on the proxied message manager.
+ *
+ * @param {array} args
+ * Arguments to be passed verbatim to the underlying
+ * sendAsyncMessage method.
+ * @returns {undefined}
+ */
+ sendAsyncMessage(...args) {
+ return this.messageManager.sendAsyncMessage(...args);
+ }
+
+ /**
+ * @private
+ * Adds docShell swap listeners to the message manager owner.
+ *
+ * @param {Element} target
+ * The target element.
+ */
+ addListeners(target) {
+ target.addEventListener("SwapDocShells", this);
+ this.eventTarget = target;
+ }
+
+ /**
+ * @private
+ * Removes docShell swap listeners to the message manager owner.
+ *
+ * @param {Element} target
+ * The target element.
+ */
+ removeListeners(target) {
+ target.removeEventListener("SwapDocShells", this);
+ }
+
+ handleEvent(event) {
+ if (event.type == "SwapDocShells") {
+ this.removeListeners(this.eventTarget);
+ this.addListeners(event.detail);
+ }
+ }
+}
/**
* Handles the mapping and dispatching of messages to their registered
* handlers. There is one broker per message manager and class of
* messages. Each class of messages is mapped to one native message
* name, e.g., "MessageChannel:Message", and is dispatched to handlers
* based on an internal message name, e.g., "Extension:ExecuteScript".
*/
@@ -605,37 +714,31 @@ this.MessageChannel = {
* Each handler object is a `MessageReceiver` object as passed to
* `addListener`.
*
* @param {Array<MessageHandler>} handlers
* @param {object} data
* @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target
*/
_handleMessage(handlers, data) {
- // The target passed to `receiveMessage` is sometimes a message manager
- // owner instead of a message manager, so make sure to convert it to a
- // message manager first if necessary.
- let {target} = data;
- if (!(target instanceof Ci.nsIMessageSender)) {
- target = target.messageManager;
- }
-
if (data.responseType == this.RESPONSE_NONE) {
handlers.forEach(handler => {
// The sender expects no reply, so dump any errors to the console.
new Promise(resolve => {
resolve(handler.receiveMessage(data));
}).catch(e => {
Cu.reportError(e.stack ? `${e}\n${e.stack}` : e.message || e);
});
});
// Note: Unhandled messages are silently dropped.
return;
}
+ let target = new MessageManagerProxy(data.target);
+
let deferred = {
sender: data.sender,
messageManager: target,
};
deferred.promise = new Promise((resolve, reject) => {
deferred.reject = reject;
this._callHandlers(handlers, data).then(resolve, reject);
@@ -668,16 +771,20 @@ this.MessageChannel = {
"columnNumber", "message", "stack", "result"]) {
if (key in error) {
response.error[key] = error[key];
}
}
}
target.sendAsyncMessage(MESSAGE_RESPONSE, response);
+ }).catch(e => {
+ Cu.reportError(e);
+ }).then(() => {
+ target.dispose();
});
this._addPendingResponse(deferred);
},
/**
* Handles message callbacks from the response brokers.
*
@@ -766,17 +873,17 @@ this.MessageChannel = {
* The message manager for which to abort brokers.
* @param {object} reason
* An object describing the reason the responses were aborted.
* Will be passed to the promise rejection handler of all aborted
* responses.
*/
abortMessageManager(target, reason) {
for (let response of this.pendingResponses) {
- if (response.messageManager === target) {
+ if (MessageManagerProxy.matches(response.messageManager, target)) {
response.reject(reason);
}
}
},
observe(subject, topic, data) {
switch (topic) {
case "message-manager-close":