Bug 1068087: Add a simple mechanism for content pages to communicate with chrome. r=mconley

--HG--
extra : rebase_source : 542edc3f702b793e0709b2ab360649be0d309736
This commit is contained in:
Dave Townsend 2015-01-08 12:39:53 -08:00
parent 6467d2d950
commit b756a0b9e6
9 changed files with 989 additions and 6 deletions

View File

@ -2475,12 +2475,6 @@
// Make sure to unregister any open URIs.
this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
// Give others a chance to swap state.
let event = new CustomEvent("SwapDocShells", {"detail": aOtherBrowser});
ourBrowser.dispatchEvent(event);
event = new CustomEvent("SwapDocShells", {"detail": ourBrowser});
aOtherBrowser.dispatchEvent(event);
// Swap the docshells
ourBrowser.swapDocShells(aOtherBrowser);

View File

@ -10,6 +10,11 @@ let Cr = Components.results;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/RemotePageManager.jsm");
// Creates a new PageListener for this frame. This will listen for page loads
// and for those that match URLs provided by the parent process will set up
// a dedicated message port and notify the parent process.
new PageListener(this);
var global = this;

View File

@ -1085,6 +1085,12 @@
if (this.isRemoteBrowser != aOtherBrowser.isRemoteBrowser)
throw new Error("Can only swap docshells between browsers in the same process.");
// Give others a chance to swap state.
let event = new CustomEvent("SwapDocShells", {"detail": aOtherBrowser});
this.dispatchEvent(event);
event = new CustomEvent("SwapDocShells", {"detail": this});
aOtherBrowser.dispatchEvent(event);
// We need to swap fields that are tied to our docshell or related to
// the loaded page
// Fields which are built as a result of notifactions (pageshow/hide,

View File

@ -0,0 +1,522 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["RemotePages", "RemotePageManager", "PageListener"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
function MessageListener() {
this.listeners = new Map();
}
MessageListener.prototype = {
keys: function() {
return this.listeners.keys();
},
has: function(name) {
return this.listeners.has(name);
},
callListeners: function(message) {
let listeners = this.listeners.get(message.name);
if (!listeners) {
return;
}
for (let listener of listeners.values()) {
try {
listener(message);
}
catch (e) {
Cu.reportError(e);
}
}
},
addMessageListener: function(name, callback) {
if (!this.listeners.has(name))
this.listeners.set(name, new Set([callback]));
else
this.listeners.get(name).add(callback);
},
removeMessageListener: function(name, callback) {
if (!this.listeners.has(name))
return;
this.listeners.get(name).delete(callback);
},
}
/**
* Creates a RemotePages object which listens for new remote pages of a
* particular URL. A "RemotePage:Init" message will be dispatched to this object
* for every page loaded. Message listeners added to this object receive
* messages from all loaded pages from the requested url.
*/
this.RemotePages = function(url) {
this.url = url;
this.messagePorts = new Set();
this.listener = new MessageListener();
this.destroyed = false;
RemotePageManager.addRemotePageListener(url, this.portCreated.bind(this));
this.portMessageReceived = this.portMessageReceived.bind(this);
}
RemotePages.prototype = {
url: null,
messagePorts: null,
listener: null,
destroyed: null,
destroy: function() {
RemotePageManager.removeRemotePageListener(this.url);
for (let port of this.messagePorts.values()) {
this.removeMessagePort(port);
}
this.messagePorts = null;
this.listener = null;
this.destroyed = true;
},
// Called when a page matching the url has loaded in a frame.
portCreated: function(port) {
this.messagePorts.add(port);
port.addMessageListener("RemotePage:Unload", this.portMessageReceived);
for (let name of this.listener.keys()) {
this.registerPortListener(port, name);
}
this.listener.callListeners({ target: port, name: "RemotePage:Init" });
},
// A message has been received from one of the pages
portMessageReceived: function(message) {
this.listener.callListeners(message);
if (message.name == "RemotePage:Unload")
this.removeMessagePort(message.target);
},
// A page has closed
removeMessagePort: function(port) {
for (let name of this.listener.keys()) {
port.removeMessageListener(name, this.portMessageReceived);
}
port.removeMessageListener("RemotePage:Unload", this.portMessageReceived);
this.messagePorts.delete(port);
},
registerPortListener: function(port, name) {
port.addMessageListener(name, this.portMessageReceived);
},
// Sends a message to all known pages
sendAsyncMessage: function(name, data = null) {
for (let port of this.messagePorts.values()) {
port.sendAsyncMessage(name, data);
}
},
addMessageListener: function(name, callback) {
if (this.destroyed) {
throw new Error("RemotePages has been destroyed");
}
if (!this.listener.has(name)) {
for (let port of this.messagePorts.values()) {
this.registerPortListener(port, name)
}
}
this.listener.addMessageListener(name, callback);
},
removeMessageListener: function(name, callback) {
if (this.destroyed) {
throw new Error("RemotePages has been destroyed");
}
this.listener.removeMessageListener(name, callback);
},
};
// Only exposes the public properties of the MessagePort
function publicMessagePort(port) {
let properties = ["addMessageListener", "removeMessageListener",
"sendAsyncMessage", "destroy"];
let clean = {};
for (let property of properties) {
clean[property] = port[property].bind(port);
}
if (port instanceof ChromeMessagePort) {
Object.defineProperty(clean, "browser", {
get: function() {
return port.browser;
}
});
}
return clean;
}
/*
* A message port sits on each side of the process boundary for every remote
* page. Each has a port ID that is unique to the message manager it talks
* through.
*
* We roughly implement the same contract as nsIMessageSender and
* nsIMessageListenerManager
*/
function MessagePort(messageManager, portID) {
this.messageManager = messageManager;
this.portID = portID;
this.destroyed = false;
this.listener = new MessageListener();
this.message = this.message.bind(this);
this.messageManager.addMessageListener("RemotePage:Message", this.message);
}
MessagePort.prototype = {
messageManager: null,
portID: null,
destroyed: null,
listener: null,
_browser: null,
remotePort: null,
// Called when the message manager used to connect to the other process has
// changed, i.e. when a tab is detached.
swapMessageManager: function(messageManager) {
this.messageManager.removeMessageListener("RemotePage:Message", this.message);
this.messageManager = messageManager;
this.messageManager.addMessageListener("RemotePage:Message", this.message);
},
/* Adds a listener for messages. Many callbacks can be registered for the
* same message if necessary. An attempt to register the same callback for the
* same message twice will be ignored. When called the callback is passed an
* object with these properties:
* target: This message port
* name: The message name
* data: Any data sent with the message
*/
addMessageListener: function(name, callback) {
if (this.destroyed) {
throw new Error("Message port has been destroyed");
}
this.listener.addMessageListener(name, callback);
},
/*
* Removes a listener for messages.
*/
removeMessageListener: function(name, callback) {
if (this.destroyed) {
throw new Error("Message port has been destroyed");
}
this.listener.removeMessageListener(name, callback);
},
// Sends a message asynchronously to the other process
sendAsyncMessage: function(name, data = null) {
if (this.destroyed) {
throw new Error("Message port has been destroyed");
}
this.messageManager.sendAsyncMessage("RemotePage:Message", {
portID: this.portID,
name: name,
data: data,
});
},
// Called to destroy this port
destroy: function() {
try {
// This can fail in the child process if the tab has already been closed
this.messageManager.removeMessageListener("RemotePage:Message", this.message);
}
catch (e) { }
this.messageManager = null;
this.destroyed = true;
this.portID = null;
this.listener = null;
},
};
// The chome side of a message port
function ChromeMessagePort(browser, portID) {
MessagePort.call(this, browser.messageManager, portID);
this._browser = browser;
this._permanentKey = browser.permanentKey;
Services.obs.addObserver(this, "message-manager-disconnect", false);
this.publicPort = publicMessagePort(this);
this.swapBrowsers = this.swapBrowsers.bind(this);
this._browser.addEventListener("SwapDocShells", this.swapBrowsers, false);
}
ChromeMessagePort.prototype = Object.create(MessagePort.prototype);
Object.defineProperty(ChromeMessagePort.prototype, "browser", {
get: function() {
return this._browser;
}
});
// Called when the docshell is being swapped with another browser. We have to
// update to use the new browser's message manager
ChromeMessagePort.prototype.swapBrowsers = function({ detail: newBrowser }) {
// We can see this event for the new browser before the swap completes so
// check that the browser we're tracking has our permanentKey.
if (this._browser.permanentKey != this._permanentKey)
return;
this._browser.removeEventListener("SwapDocShells", this.swapBrowsers, false);
this._browser = newBrowser;
this.swapMessageManager(newBrowser.messageManager);
this._browser.addEventListener("SwapDocShells", this.swapBrowsers, false);
}
// Called when a message manager has been disconnected indicating that the
// tab has closed or crashed
ChromeMessagePort.prototype.observe = function(messageManager) {
if (messageManager != this.messageManager)
return;
this.listener.callListeners({
target: this.publicPort,
name: "RemotePage:Unload",
data: null,
});
this.destroy();
};
// Called when a message is received from the message manager. This could
// have come from any port in the message manager so verify the port ID.
ChromeMessagePort.prototype.message = function({ data: messagedata }) {
if (this.destroyed || (messagedata.portID != this.portID)) {
return;
}
let message = {
target: this.publicPort,
name: messagedata.name,
data: messagedata.data,
};
this.listener.callListeners(message);
if (messagedata.name == "RemotePage:Unload")
this.destroy();
};
ChromeMessagePort.prototype.destroy = function() {
this._browser.removeEventListener("SwapDocShells", this.swapBrowsers, false);
this._browser = null;
Services.obs.removeObserver(this, "message-manager-disconnect");
MessagePort.prototype.destroy.call(this);
};
// The content side of a message port
function ChildMessagePort(contentFrame, window) {
let portID = Services.appinfo.processID + ":" + ChildMessagePort.prototype.nextPortID++;
MessagePort.call(this, contentFrame, portID);
this.window = window;
// Add functionality to the content page
Cu.exportFunction(this.sendAsyncMessage.bind(this), window, {
defineAs: "sendAsyncMessage",
});
Cu.exportFunction(this.addMessageListener.bind(this), window, {
defineAs: "addMessageListener",
allowCallbacks: true,
});
Cu.exportFunction(this.removeMessageListener.bind(this), window, {
defineAs: "removeMessageListener",
allowCallbacks: true,
});
// Send a message for load events
let loadListener = () => {
this.sendAsyncMessage("RemotePage:Load");
window.removeEventListener("load", loadListener, false);
};
window.addEventListener("load", loadListener, false);
// Destroy the port when the window is unloaded
window.addEventListener("unload", () => {
try {
this.sendAsyncMessage("RemotePage:Unload");
}
catch (e) {
// If the tab has been closed the frame message manager has already been
// destroyed
}
this.destroy();
}, false);
// Tell the main process to set up its side of the message pipe.
this.messageManager.sendAsyncMessage("RemotePage:InitPort", {
portID: portID,
url: window.location.toString(),
});
}
ChildMessagePort.prototype = Object.create(MessagePort.prototype);
ChildMessagePort.prototype.nextPortID = 0;
// Called when a message is received from the message manager. This could
// have come from any port in the message manager so verify the port ID.
ChildMessagePort.prototype.message = function({ data: messagedata }) {
if (this.destroyed || (messagedata.portID != this.portID)) {
return;
}
let message = {
name: messagedata.name,
data: messagedata.data,
};
this.listener.callListeners(Cu.cloneInto(message, this.window));
};
ChildMessagePort.prototype.destroy = function() {
this.window = null;
MessagePort.prototype.destroy.call(this);
}
// Allows callers to register to connect to specific content pages. Registration
// is done through the addRemotePageListener method
let RemotePageManagerInternal = {
// The currently registered remote pages
pages: new Map(),
// Initialises all the needed listeners
init: function() {
Services.mm.addMessageListener("RemotePage:InitListener", this.initListener.bind(this));
Services.mm.addMessageListener("RemotePage:InitPort", this.initPort.bind(this));
},
// Registers interest in a remote page. A callback is called with a port for
// the new page when loading begins (i.e. the page hasn't actually loaded yet).
// Only one callback can be registered per URL.
addRemotePageListener: function(url, callback) {
if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
throw new Error("RemotePageManager can only be used in the main process.");
if (this.pages.has(url)) {
throw new Error("Remote page already registered: " + url);
}
this.pages.set(url, callback);
// Notify all the frame scripts of the new registration
Services.mm.broadcastAsyncMessage("RemotePage:Register", { urls: [url] });
},
// Removes any interest in a remote page.
removeRemotePageListener: function(url) {
if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
throw new Error("RemotePageManager can only be used in the main process.");
if (!this.pages.has(url)) {
throw new Error("Remote page is not registered: " + url);
}
// Notify all the frame scripts of the removed registration
Services.mm.broadcastAsyncMessage("RemotePage:Unregister", { urls: [url] });
this.pages.delete(url);
},
// A listener is requesting the list of currently registered urls
initListener: function({ target: browser }) {
browser.messageManager.sendAsyncMessage("RemotePage:Register", { urls: [u for (u of this.pages.keys())] })
},
// A remote page has been created and a port is ready in the content side
initPort: function({ target: browser, data: { url, portID } }) {
let callback = this.pages.get(url);
if (!callback) {
Cu.reportError("Unexpected remote page load: " + url);
return;
}
let port = new ChromeMessagePort(browser, portID);
callback(port.publicPort);
}
};
if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
RemotePageManagerInternal.init();
// The public API for the above object
this.RemotePageManager = {
addRemotePageListener: RemotePageManagerInternal.addRemotePageListener.bind(RemotePageManagerInternal),
removeRemotePageListener: RemotePageManagerInternal.removeRemotePageListener.bind(RemotePageManagerInternal),
};
// Listens in a frame for new pages to be loaded
function PageListener(contentFrame) {
let registeredURLs = new Set();
let observer = (window) => {
// Ignore windows from other frames
if (window.top != contentFrame.content)
return;
let url = window.location.toString();
if (!registeredURLs.has(url))
return;
// Set up the child side of the message port
let port = new ChildMessagePort(contentFrame, window);
};
Services.obs.addObserver(observer, "chrome-document-global-created", false);
Services.obs.addObserver(observer, "content-document-global-created", false);
// A message from chrome telling us what pages to listen for
contentFrame.addMessageListener("RemotePage:Register", ({ data }) => {
for (let url of data.urls)
registeredURLs.add(url);
});
// A message from chrome telling us what pages to stop listening for
contentFrame.addMessageListener("RemotePage:Unregister", ({ data }) => {
for (let url of data.urls)
registeredURLs.delete(url);
});
contentFrame.sendAsyncMessage("RemotePage:InitListener");
}

View File

@ -45,6 +45,12 @@ XPCOMUtils.defineLazyGetter(Services, "crashmanager", () => {
});
#endif
XPCOMUtils.defineLazyGetter(Services, "mm", () => {
return Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageBroadcaster)
.QueryInterface(Ci.nsIFrameScriptLoader);
});
let initTable = [
#ifdef MOZ_WIDGET_ANDROID
["androidBridge", "@mozilla.org/android/bridge;1", "nsIAndroidBridge"],
@ -52,6 +58,7 @@ let initTable = [
["appShell", "@mozilla.org/appshell/appShellService;1", "nsIAppShellService"],
["cache", "@mozilla.org/network/cache-service;1", "nsICacheService"],
["cache2", "@mozilla.org/netwerk/cache-storage-service;1", "nsICacheStorageService"],
["cpmm", "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"],
["console", "@mozilla.org/consoleservice;1", "nsIConsoleService"],
["contentPrefs", "@mozilla.org/content-pref/service;1", "nsIContentPrefService"],
["cookies", "@mozilla.org/cookiemanager;1", "nsICookieManager2"],
@ -63,6 +70,7 @@ let initTable = [
["logins", "@mozilla.org/login-manager;1", "nsILoginManager"],
["obs", "@mozilla.org/observer-service;1", "nsIObserverService"],
["perms", "@mozilla.org/permissionmanager;1", "nsIPermissionManager"],
["ppmm", "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster"],
["prompt", "@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService"],
#ifdef MOZ_ENABLE_PROFILER_SPS
["profiler", "@mozilla.org/tools/profiler;1", "nsIProfiler"],

View File

@ -41,6 +41,7 @@ EXTRA_JS_MODULES += [
'PropertyListUtils.jsm',
'RemoteController.jsm',
'RemoteFinder.jsm',
'RemotePageManager.jsm',
'RemoteSecurityUI.jsm',
'RemoteWebNavigation.jsm',
'RemoteWebProgress.jsm',

View File

@ -1,6 +1,7 @@
[DEFAULT]
support-files =
dummy_page.html
testremotepagemanager.html
[browser_Battery.js]
[browser_Deprecated.js]
@ -8,5 +9,6 @@ support-files =
skip-if = e10s # Bug ?????? - test already uses content scripts, but still fails only under e10s.
[browser_Geometry.js]
[browser_InlineSpellChecker.js]
[browser_RemotePageManager.js]
[browser_RemoteWebNavigation.js]
[browser_Troubleshoot.js]

View File

@ -0,0 +1,379 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const TEST_URL = "http://www.example.com/browser/toolkit/modules/tests/browser/testremotepagemanager.html";
let { RemotePages, RemotePageManager } = Cu.import("resource://gre/modules/RemotePageManager.jsm", {});
function failOnMessage(message) {
ok(false, "Should not have seen message " + message.name);
}
function waitForMessage(port, message, expectedPort = port) {
return new Promise((resolve) => {
function listener(message) {
is(message.target, expectedPort, "Message should be from the right port.");
port.removeMessageListener(listener);
resolve(message);
}
port.addMessageListener(message, listener);
});
}
function waitForPort(url, createTab = true) {
return new Promise((resolve) => {
RemotePageManager.addRemotePageListener(url, (port) => {
RemotePageManager.removeRemotePageListener(url);
waitForMessage(port, "RemotePage:Load").then(() => resolve(port));
});
if (createTab)
gBrowser.selectedTab = gBrowser.addTab(url);
});
}
function waitForPage(pages) {
return new Promise((resolve) => {
function listener({ target }) {
pages.removeMessageListener("RemotePage:Init", listener);
waitForMessage(target, "RemotePage:Load").then(() => resolve(target));
}
pages.addMessageListener("RemotePage:Init", listener);
gBrowser.selectedTab = gBrowser.addTab(TEST_URL);
});
}
// Test that opening a page creates a port, sends the load event and then
// navigating to a new page sends the unload event. Going back should create a
// new port
add_task(function* init_navigate() {
let port = yield waitForPort(TEST_URL);
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
let loaded = new Promise(resolve => {
function listener() {
gBrowser.selectedBrowser.removeEventListener("load", listener, true);
resolve();
}
gBrowser.selectedBrowser.addEventListener("load", listener, true);
gBrowser.loadURI("about:blank");
});
yield waitForMessage(port, "RemotePage:Unload");
// Port should be destroyed now
try {
port.addMessageListener("Foo", failOnMessage);
ok(false, "Should have seen exception");
}
catch (e) {
ok(true, "Should have seen exception");
}
try {
port.sendAsyncMessage("Foo");
ok(false, "Should have seen exception");
}
catch (e) {
ok(true, "Should have seen exception");
}
yield loaded;
gBrowser.goBack();
port = yield waitForPort(TEST_URL, false);
port.sendAsyncMessage("Ping2");
let message = yield waitForMessage(port, "Pong2");
port.destroy();
gBrowser.removeCurrentTab();
});
// Test that opening a page creates a port, sends the load event and then
// closing the tab sends the unload event
add_task(function* init_close() {
let port = yield waitForPort(TEST_URL);
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
let unloadPromise = waitForMessage(port, "RemotePage:Unload");
gBrowser.removeCurrentTab();
yield unloadPromise;
// Port should be destroyed now
try {
port.addMessageListener("Foo", failOnMessage);
ok(false, "Should have seen exception");
}
catch (e) {
ok(true, "Should have seen exception");
}
try {
port.sendAsyncMessage("Foo");
ok(false, "Should have seen exception");
}
catch (e) {
ok(true, "Should have seen exception");
}
});
// Tests that we can send messages to individual pages even when more than one
// is open
add_task(function* multiple_ports() {
let port1 = yield waitForPort(TEST_URL);
is(port1.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
let port2 = yield waitForPort(TEST_URL);
is(port2.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
port2.addMessageListener("Pong", failOnMessage);
port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 });
let message = yield waitForMessage(port1, "Pong");
port2.removeMessageListener("Pong", failOnMessage);
is(message.data.str, "foobar", "String should pass through");
is(message.data.counter, 1, "Counter should be incremented");
port1.addMessageListener("Pong", failOnMessage);
port2.sendAsyncMessage("Ping", { str: "foobaz", counter: 5 });
message = yield waitForMessage(port2, "Pong");
port1.removeMessageListener("Pong", failOnMessage);
is(message.data.str, "foobaz", "String should pass through");
is(message.data.counter, 6, "Counter should be incremented");
let unloadPromise = waitForMessage(port2, "RemotePage:Unload");
gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser));
yield unloadPromise;
try {
port2.addMessageListener("Pong", failOnMessage);
ok(false, "Should not have been able to add a new message listener to a destroyed port.");
}
catch (e) {
ok(true, "Should not have been able to add a new message listener to a destroyed port.");
}
port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 });
message = yield waitForMessage(port1, "Pong");
is(message.data.str, "foobar", "String should pass through");
is(message.data.counter, 1, "Counter should be incremented");
unloadPromise = waitForMessage(port1, "RemotePage:Unload");
gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser));
yield unloadPromise;
});
// Tests that swapping browser docshells doesn't break the ports
add_task(function* browser_switch() {
let port1 = yield waitForPort(TEST_URL);
is(port1.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
let browser1 = gBrowser.selectedBrowser;
port1.sendAsyncMessage("SetCookie", { value: "om nom" });
let port2 = yield waitForPort(TEST_URL);
is(port2.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
let browser2 = gBrowser.selectedBrowser;
port2.sendAsyncMessage("SetCookie", { value: "om nom nom" });
port2.addMessageListener("Cookie", failOnMessage);
port1.sendAsyncMessage("GetCookie");
let message = yield waitForMessage(port1, "Cookie");
port2.removeMessageListener("Cookie", failOnMessage);
is(message.data.value, "om nom", "Should have the right cookie");
port1.addMessageListener("Cookie", failOnMessage);
port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
message = yield waitForMessage(port2, "Cookie");
port1.removeMessageListener("Cookie", failOnMessage);
is(message.data.value, "om nom nom", "Should have the right cookie");
browser1.swapDocShells(browser2);
is(port1.browser, browser2, "Should have noticed the swap");
is(port2.browser, browser1, "Should have noticed the swap");
// Cookies should have stayed the same
port2.addMessageListener("Cookie", failOnMessage);
port1.sendAsyncMessage("GetCookie");
message = yield waitForMessage(port1, "Cookie");
port2.removeMessageListener("Cookie", failOnMessage);
is(message.data.value, "om nom", "Should have the right cookie");
port1.addMessageListener("Cookie", failOnMessage);
port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
message = yield waitForMessage(port2, "Cookie");
port1.removeMessageListener("Cookie", failOnMessage);
is(message.data.value, "om nom nom", "Should have the right cookie");
browser1.swapDocShells(browser2);
is(port1.browser, browser1, "Should have noticed the swap");
is(port2.browser, browser2, "Should have noticed the swap");
// Cookies should have stayed the same
port2.addMessageListener("Cookie", failOnMessage);
port1.sendAsyncMessage("GetCookie");
message = yield waitForMessage(port1, "Cookie");
port2.removeMessageListener("Cookie", failOnMessage);
is(message.data.value, "om nom", "Should have the right cookie");
port1.addMessageListener("Cookie", failOnMessage);
port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
message = yield waitForMessage(port2, "Cookie");
port1.removeMessageListener("Cookie", failOnMessage);
is(message.data.value, "om nom nom", "Should have the right cookie");
let unloadPromise = waitForMessage(port2, "RemotePage:Unload");
gBrowser.removeTab(gBrowser.getTabForBrowser(browser2));
yield unloadPromise;
unloadPromise = waitForMessage(port1, "RemotePage:Unload");
gBrowser.removeTab(gBrowser.getTabForBrowser(browser1));
yield unloadPromise;
});
// Tests that removeMessageListener in chrome works
add_task(function* remove_chrome_listener() {
let port = yield waitForPort(TEST_URL);
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
// This relies on messages sent arriving in the same order. Pong will be
// sent back before Pong2 so if removeMessageListener fails the test will fail
port.addMessageListener("Pong", failOnMessage);
port.removeMessageListener("Pong", failOnMessage);
port.sendAsyncMessage("Ping", { str: "remove_listener", counter: 27 });
port.sendAsyncMessage("Ping2");
yield waitForMessage(port, "Pong2");
let unloadPromise = waitForMessage(port, "RemotePage:Unload");
gBrowser.removeCurrentTab();
yield unloadPromise;
});
// Tests that removeMessageListener in content works
add_task(function* remove_content_listener() {
let port = yield waitForPort(TEST_URL);
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
// This relies on messages sent arriving in the same order. Pong3 would be
// sent back before Pong2 so if removeMessageListener fails the test will fail
port.addMessageListener("Pong3", failOnMessage);
port.sendAsyncMessage("Ping3");
port.sendAsyncMessage("Ping2");
yield waitForMessage(port, "Pong2");
let unloadPromise = waitForMessage(port, "RemotePage:Unload");
gBrowser.removeCurrentTab();
yield unloadPromise;
});
// Test RemotePages works
add_task(function* remote_pages_basic() {
let pages = new RemotePages(TEST_URL);
let port = yield waitForPage(pages);
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
// Listening to global messages should work
let unloadPromise = waitForMessage(pages, "RemotePage:Unload", port);
gBrowser.removeCurrentTab();
yield unloadPromise;
pages.destroy();
// RemotePages should be destroyed now
try {
pages.addMessageListener("Foo", failOnMessage);
ok(false, "Should have seen exception");
}
catch (e) {
ok(true, "Should have seen exception");
}
try {
pages.sendAsyncMessage("Foo");
ok(false, "Should have seen exception");
}
catch (e) {
ok(true, "Should have seen exception");
}
});
// Test sending messages to all remote pages works
add_task(function* remote_pages_multiple() {
let pages = new RemotePages(TEST_URL);
let port1 = yield waitForPage(pages);
let port2 = yield waitForPage(pages);
let pongPorts = [];
yield new Promise((resolve) => {
function listener({ name, target, data }) {
is(name, "Pong", "Should have seen the right response.");
is(data.str, "remote_pages", "String should pass through");
is(data.counter, 43, "Counter should be incremented");
pongPorts.push(target);
if (pongPorts.length == 2)
resolve();
}
pages.addMessageListener("Pong", listener);
pages.sendAsyncMessage("Ping", { str: "remote_pages", counter: 42 });
});
// We don't make any guarantees about which order messages are sent to known
// pages so the pongs could have come back in any order.
isnot(pongPorts[0], pongPorts[1], "Should have received pongs from different ports");
ok(pongPorts.indexOf(port1) >= 0, "Should have seen a pong from port1");
ok(pongPorts.indexOf(port2) >= 0, "Should have seen a pong from port2");
// After destroy we should see no messages
pages.addMessageListener("RemotePage:Unload", failOnMessage);
pages.destroy();
gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser));
gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser));
});
// Test sending various types of data across the boundary
add_task(function* send_data() {
let port = yield waitForPort(TEST_URL);
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
let data = {
integer: 45,
real: 45.78,
str: "foobar",
array: [1, 2, 3, 5, 27]
};
port.sendAsyncMessage("SendData", data);
let message = yield waitForMessage(port, "ReceivedData");
ok(message.data.result, message.data.status);
gBrowser.removeCurrentTab();
});
// Test sending an object of data across the boundary
add_task(function* send_data2() {
let port = yield waitForPort(TEST_URL);
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
let data = {
integer: 45,
real: 45.78,
str: "foobar",
array: [1, 2, 3, 5, 27]
};
port.sendAsyncMessage("SendData2", {data});
let message = yield waitForMessage(port, "ReceivedData2");
ok(message.data.result, message.data.status);
gBrowser.removeCurrentTab();
});

View File

@ -0,0 +1,66 @@
<!DOCTYPE HTML>
<html>
<head>
<script type="text/javascript">
addMessageListener("Ping", function(message) {
sendAsyncMessage("Pong", {
str: message.data.str,
counter: message.data.counter + 1
});
});
addMessageListener("Ping2", function(message) {
sendAsyncMessage("Pong2", message.data);
});
function neverCalled() {
sendAsyncMessage("Pong3");
}
addMessageListener("Pong3", neverCalled);
removeMessageListener("Pong3", neverCalled);
function testData(data) {
var response = {
result: true,
status: "All data correctly received"
}
function compare(prop, expected) {
if (uneval(data[prop]) == uneval(expected))
return;
if (response.result)
response.status = "";
response.result = false;
response.status += "Property " + prop + " should have been " + expected + " but was " + data[prop] + "\n";
}
compare("integer", 45);
compare("real", 45.78);
compare("str", "foobar");
compare("array", [1, 2, 3, 5, 27]);
return response;
}
addMessageListener("SendData", function(message) {
sendAsyncMessage("ReceivedData", testData(message.data));
});
addMessageListener("SendData2", function(message) {
sendAsyncMessage("ReceivedData2", testData(message.data.data));
});
var cookie = "nom";
addMessageListener("SetCookie", function(message) {
cookie = message.data.value;
});
addMessageListener("GetCookie", function(message) {
sendAsyncMessage("Cookie", { value: cookie });
});
</script>
</head>
<body>
</body>
</html>