summaryrefslogtreecommitdiffstats
path: root/remote/shared/messagehandler/test
diff options
context:
space:
mode:
Diffstat (limited to 'remote/shared/messagehandler/test')
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/browser.ini17
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/browser_filter_top_browsing_context.js83
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/browser_only_content_process.js46
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/browser_two_tabs.js46
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/browser_two_tabs_with_params.js47
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/browser_two_windows.js47
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/browser_with_frames.js39
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/doc_messagehandler_broadcasting_xul.xhtml3
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/head.js53
-rw-r--r--remote/shared/messagehandler/test/browser/browser.ini21
-rw-r--r--remote/shared/messagehandler/test/browser/browser_events_dispatcher.js453
-rw-r--r--remote/shared/messagehandler/test/browser/browser_events_handler.js57
-rw-r--r--remote/shared/messagehandler/test/browser/browser_events_module.js283
-rw-r--r--remote/shared/messagehandler/test/browser/browser_frame_context_utils.js95
-rw-r--r--remote/shared/messagehandler/test/browser/browser_handle_command_errors.js221
-rw-r--r--remote/shared/messagehandler/test/browser/browser_handle_command_retry.js243
-rw-r--r--remote/shared/messagehandler/test/browser/browser_handle_simple_command.js206
-rw-r--r--remote/shared/messagehandler/test/browser/browser_registry.js38
-rw-r--r--remote/shared/messagehandler/test/browser/browser_session_data.js272
-rw-r--r--remote/shared/messagehandler/test/browser/browser_session_data_broadcast.js335
-rw-r--r--remote/shared/messagehandler/test/browser/browser_session_data_browser_element.js98
-rw-r--r--remote/shared/messagehandler/test/browser/browser_session_data_constructor_race.js54
-rw-r--r--remote/shared/messagehandler/test/browser/head.js186
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/ModuleRegistry.sys.mjs30
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/root/command.sys.mjs80
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/root/event.sys.mjs21
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/root/invalid.sys.mjs4
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/root/rootOnly.sys.mjs70
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal-in-root/command.sys.mjs28
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal-in-root/event.sys.mjs31
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal/command.sys.mjs114
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal/commandwindowglobalonly.sys.mjs41
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal/event.sys.mjs26
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal/eventemitter.sys.mjs81
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal/eventnointercept.sys.mjs16
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal/retry.sys.mjs84
-rw-r--r--remote/shared/messagehandler/test/browser/webdriver/browser.ini9
-rw-r--r--remote/shared/messagehandler/test/browser/webdriver/browser_session_execute_command_errors.js40
-rw-r--r--remote/shared/messagehandler/test/xpcshell/test_Errors.js99
-rw-r--r--remote/shared/messagehandler/test/xpcshell/test_SessionData.js296
-rw-r--r--remote/shared/messagehandler/test/xpcshell/xpcshell.ini6
41 files changed, 4019 insertions, 0 deletions
diff --git a/remote/shared/messagehandler/test/browser/broadcast/browser.ini b/remote/shared/messagehandler/test/browser/broadcast/browser.ini
new file mode 100644
index 0000000000..a2f989aaf0
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/browser.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+tags = remote
+subsuite = remote
+support-files =
+ doc_messagehandler_broadcasting_xul.xhtml
+ head.js
+ !/remote/shared/messagehandler/test/browser/head.js
+ !/remote/shared/messagehandler/test/browser/resources/*
+prefs =
+ remote.messagehandler.modulecache.useBrowserTestRoot=true
+
+[browser_filter_top_browsing_context.js]
+[browser_only_content_process.js]
+[browser_two_tabs.js]
+[browser_two_tabs_with_params.js]
+[browser_two_windows.js]
+[browser_with_frames.js]
diff --git a/remote/shared/messagehandler/test/browser/broadcast/browser_filter_top_browsing_context.js b/remote/shared/messagehandler/test/browser/broadcast/browser_filter_top_browsing_context.js
new file mode 100644
index 0000000000..74bc971850
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/browser_filter_top_browsing_context.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const COM_TEST_PAGE = "https://example.com/document-builder.sjs?html=COM";
+const FRAME_TEST_PAGE = createTestMarkupWithFrames();
+
+add_task(async function test_broadcasting_filter_top_browsing_context() {
+ info("Navigate the initial tab to the COM test URL");
+ const tab1 = gBrowser.selectedTab;
+ await loadURL(tab1.linkedBrowser, COM_TEST_PAGE);
+ const browsingContext1 = tab1.linkedBrowser.browsingContext;
+
+ info("Open a second tab on the frame test URL");
+ const tab2 = await addTab(FRAME_TEST_PAGE);
+ const browsingContext2 = tab2.linkedBrowser.browsingContext;
+
+ const contextsForTab2 = tab2.linkedBrowser.browsingContext.getAllBrowsingContextsInSubtree();
+ is(
+ contextsForTab2.length,
+ 4,
+ "Frame test tab has 3 children contexts (4 in total)"
+ );
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-broadcasting_filter_top_browsing_context"
+ );
+
+ const broadcastValue1 = await sendBroadcastForTopBrowsingContext(
+ browsingContext1,
+ rootMessageHandler
+ );
+
+ ok(
+ Array.isArray(broadcastValue1),
+ "The broadcast returned an array of values"
+ );
+
+ is(broadcastValue1.length, 1, "The broadcast returned one value as expected");
+
+ ok(
+ broadcastValue1.includes("broadcast-" + browsingContext1.id),
+ "The broadcast returned the expected value from tab1"
+ );
+
+ const broadcastValue2 = await sendBroadcastForTopBrowsingContext(
+ browsingContext2,
+ rootMessageHandler
+ );
+
+ ok(
+ Array.isArray(broadcastValue2),
+ "The broadcast returned an array of values"
+ );
+
+ is(broadcastValue2.length, 4, "The broadcast returned 4 values as expected");
+
+ for (const context of contextsForTab2) {
+ ok(
+ broadcastValue2.includes("broadcast-" + context.id),
+ "The broadcast contains the value for browsing context " + context.id
+ );
+ }
+
+ rootMessageHandler.destroy();
+});
+
+function sendBroadcastForTopBrowsingContext(
+ topBrowsingContext,
+ rootMessageHandler
+) {
+ return sendTestBroadcastCommand(
+ "commandwindowglobalonly",
+ "testBroadcast",
+ {},
+ {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: topBrowsingContext.browserId,
+ },
+ rootMessageHandler
+ );
+}
diff --git a/remote/shared/messagehandler/test/browser/broadcast/browser_only_content_process.js b/remote/shared/messagehandler/test/browser/broadcast/browser_only_content_process.js
new file mode 100644
index 0000000000..d5090c701e
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/browser_only_content_process.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_broadcasting_only_content_process() {
+ info("Navigate the initial tab to the test URL");
+ const tab1 = gBrowser.selectedTab;
+ await loadURL(
+ tab1.linkedBrowser,
+ "https://example.com/document-builder.sjs?html=tab"
+ );
+ const browsingContext1 = tab1.linkedBrowser.browsingContext;
+
+ info("Open a new tab on a parent process about: page");
+ await addTab("about:robots");
+
+ info("Open a new tab on a XUL page");
+ await addTab(
+ getRootDirectory(gTestPath) + "doc_messagehandler_broadcasting_xul.xhtml"
+ );
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-broadcasting_only_content_process"
+ );
+ const broadcastValue = await sendTestBroadcastCommand(
+ "commandwindowglobalonly",
+ "testBroadcast",
+ {},
+ contextDescriptorAll,
+ rootMessageHandler
+ );
+
+ ok(
+ Array.isArray(broadcastValue),
+ "The broadcast returned an array of values"
+ );
+
+ is(broadcastValue.length, 1, "The broadcast returned 1 value as expected");
+ ok(
+ broadcastValue.includes("broadcast-" + browsingContext1.id),
+ "The broadcast returned the expected value from tab1"
+ );
+
+ rootMessageHandler.destroy();
+});
diff --git a/remote/shared/messagehandler/test/browser/broadcast/browser_two_tabs.js b/remote/shared/messagehandler/test/browser/broadcast/browser_two_tabs.js
new file mode 100644
index 0000000000..16b97e2a0a
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/browser_two_tabs.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE = "https://example.com/document-builder.sjs?html=tab";
+
+add_task(async function test_broadcasting_two_tabs_command() {
+ info("Navigate the initial tab to the test URL");
+ const tab1 = gBrowser.selectedTab;
+ await loadURL(tab1.linkedBrowser, TEST_PAGE);
+ const browsingContext1 = tab1.linkedBrowser.browsingContext;
+
+ info("Open a new tab on the same test URL");
+ const tab2 = await addTab(TEST_PAGE);
+ const browsingContext2 = tab2.linkedBrowser.browsingContext;
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-broadcasting_two_tabs_command"
+ );
+
+ const broadcastValue = await sendTestBroadcastCommand(
+ "commandwindowglobalonly",
+ "testBroadcast",
+ {},
+ contextDescriptorAll,
+ rootMessageHandler
+ );
+
+ ok(
+ Array.isArray(broadcastValue),
+ "The broadcast returned an array of values"
+ );
+
+ is(broadcastValue.length, 2, "The broadcast returned 2 values as expected");
+
+ ok(
+ broadcastValue.includes("broadcast-" + browsingContext1.id),
+ "The broadcast returned the expected value from tab1"
+ );
+ ok(
+ broadcastValue.includes("broadcast-" + browsingContext2.id),
+ "The broadcast returned the expected value from tab2"
+ );
+ rootMessageHandler.destroy();
+});
diff --git a/remote/shared/messagehandler/test/browser/broadcast/browser_two_tabs_with_params.js b/remote/shared/messagehandler/test/browser/broadcast/browser_two_tabs_with_params.js
new file mode 100644
index 0000000000..261b8c4cd6
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/browser_two_tabs_with_params.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE = "https://example.com/document-builder.sjs?html=tab";
+
+add_task(async function test_broadcasting_two_tabs_with_params_command() {
+ info("Navigate the initial tab to the test URL");
+ const tab1 = gBrowser.selectedTab;
+ await loadURL(tab1.linkedBrowser, TEST_PAGE);
+ const browsingContext1 = tab1.linkedBrowser.browsingContext;
+
+ info("Open a new tab on the same test URL");
+ const tab2 = await addTab(TEST_PAGE);
+ const browsingContext2 = tab2.linkedBrowser.browsingContext;
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-broadcasting_two_tabs_command"
+ );
+
+ const broadcastValue = await sendTestBroadcastCommand(
+ "commandwindowglobalonly",
+ "testBroadcastWithParameter",
+ {
+ value: "some-value",
+ },
+ contextDescriptorAll,
+ rootMessageHandler
+ );
+ ok(
+ Array.isArray(broadcastValue),
+ "The broadcast returned an array of values"
+ );
+
+ is(broadcastValue.length, 2, "The broadcast returned 2 values as expected");
+
+ ok(
+ broadcastValue.includes("broadcast-" + browsingContext1.id + "-some-value"),
+ "The broadcast returned the expected value from tab1"
+ );
+ ok(
+ broadcastValue.includes("broadcast-" + browsingContext2.id + "-some-value"),
+ "The broadcast returned the expected value from tab2"
+ );
+ rootMessageHandler.destroy();
+});
diff --git a/remote/shared/messagehandler/test/browser/broadcast/browser_two_windows.js b/remote/shared/messagehandler/test/browser/broadcast/browser_two_windows.js
new file mode 100644
index 0000000000..f59bebba69
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/browser_two_windows.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE = "https://example.com/document-builder.sjs?html=tab";
+
+add_task(async function test_broadcasting_two_windows_command() {
+ const window1Browser = gBrowser.selectedTab.linkedBrowser;
+ await loadURL(window1Browser, TEST_PAGE);
+ const browsingContext1 = window1Browser.browsingContext;
+
+ const window2 = await BrowserTestUtils.openNewBrowserWindow();
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(window2));
+
+ const window2Browser = window2.gBrowser.selectedBrowser;
+ await loadURL(window2Browser, TEST_PAGE);
+ const browsingContext2 = window2Browser.browsingContext;
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-broadcasting_two_windows_command"
+ );
+ const broadcastValue = await sendTestBroadcastCommand(
+ "commandwindowglobalonly",
+ "testBroadcast",
+ {},
+ contextDescriptorAll,
+ rootMessageHandler
+ );
+
+ ok(
+ Array.isArray(broadcastValue),
+ "The broadcast returned an array of values"
+ );
+ is(broadcastValue.length, 2, "The broadcast returned 2 values as expected");
+
+ ok(
+ broadcastValue.includes("broadcast-" + browsingContext1.id),
+ "The broadcast returned the expected value from tab1"
+ );
+ ok(
+ broadcastValue.includes("broadcast-" + browsingContext2.id),
+ "The broadcast returned the expected value from tab2"
+ );
+
+ rootMessageHandler.destroy();
+});
diff --git a/remote/shared/messagehandler/test/browser/broadcast/browser_with_frames.js b/remote/shared/messagehandler/test/browser/broadcast/browser_with_frames.js
new file mode 100644
index 0000000000..7cb23e3309
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/browser_with_frames.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_broadcasting_with_frames() {
+ info("Navigate the initial tab to the test URL");
+ const tab = gBrowser.selectedTab;
+ await loadURL(tab.linkedBrowser, createTestMarkupWithFrames());
+
+ const contexts = tab.linkedBrowser.browsingContext.getAllBrowsingContextsInSubtree();
+ is(contexts.length, 4, "Test tab has 3 children contexts (4 in total)");
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-broadcasting_with_frames"
+ );
+ const broadcastValue = await sendTestBroadcastCommand(
+ "commandwindowglobalonly",
+ "testBroadcast",
+ {},
+ contextDescriptorAll,
+ rootMessageHandler
+ );
+
+ ok(
+ Array.isArray(broadcastValue),
+ "The broadcast returned an array of values"
+ );
+ is(broadcastValue.length, 4, "The broadcast returned 4 values as expected");
+
+ for (const context of contexts) {
+ ok(
+ broadcastValue.includes("broadcast-" + context.id),
+ "The broadcast contains the value for browsing context " + context.id
+ );
+ }
+
+ rootMessageHandler.destroy();
+});
diff --git a/remote/shared/messagehandler/test/browser/broadcast/doc_messagehandler_broadcasting_xul.xhtml b/remote/shared/messagehandler/test/browser/broadcast/doc_messagehandler_broadcasting_xul.xhtml
new file mode 100644
index 0000000000..91f3503ac3
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/doc_messagehandler_broadcasting_xul.xhtml
@@ -0,0 +1,3 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <box id="box" style="background-color: red;">Test chrome broadcasting</box>
+</window>
diff --git a/remote/shared/messagehandler/test/browser/broadcast/head.js b/remote/shared/messagehandler/test/browser/broadcast/head.js
new file mode 100644
index 0000000000..7bbe96ae97
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/head.js
@@ -0,0 +1,53 @@
+/* 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";
+
+/* import-globals-from ../head.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/remote/shared/messagehandler/test/browser/head.js",
+ this
+);
+
+/**
+ * Broadcast the provided method to WindowGlobal contexts on a MessageHandler
+ * network.
+ * Returns a promise which will resolve the result of the command broadcast.
+ *
+ * @param {String} module
+ * The name of the module implementing the command to broadcast.
+ * @param {String} command
+ * The name of the command to broadcast.
+ * @param {Object} params
+ * The parameters for the command.
+ * @param {ContextDescriptor} contextDescriptor
+ * The context descriptor to use for this broadcast
+ * @param {RootMessageHandler} rootMessageHandler
+ * The root of the MessageHandler network.
+ * @return {Promise.<Array>}
+ * Promise which resolves an array where each item is the result of the
+ * command handled by an individual context.
+ */
+function sendTestBroadcastCommand(
+ module,
+ command,
+ params,
+ contextDescriptor,
+ rootMessageHandler
+) {
+ const { WindowGlobalMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs"
+ );
+
+ info("Send a test broadcast command");
+ return rootMessageHandler.handleCommand({
+ moduleName: module,
+ commandName: command,
+ params,
+ destination: {
+ contextDescriptor,
+ type: WindowGlobalMessageHandler.type,
+ },
+ });
+}
diff --git a/remote/shared/messagehandler/test/browser/browser.ini b/remote/shared/messagehandler/test/browser/browser.ini
new file mode 100644
index 0000000000..00cd152370
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+tags = remote
+subsuite = remote
+support-files =
+ head.js
+ resources/*
+prefs =
+ remote.messagehandler.modulecache.useBrowserTestRoot=true
+
+[browser_events_dispatcher.js]
+[browser_events_handler.js]
+[browser_events_module.js]
+[browser_frame_context_utils.js]
+[browser_handle_command_errors.js]
+[browser_handle_command_retry.js]
+[browser_handle_simple_command.js]
+[browser_registry.js]
+[browser_session_data.js]
+[browser_session_data_broadcast.js]
+[browser_session_data_browser_element.js]
+[browser_session_data_constructor_race.js]
diff --git a/remote/shared/messagehandler/test/browser/browser_events_dispatcher.js b/remote/shared/messagehandler/test/browser/browser_events_dispatcher.js
new file mode 100644
index 0000000000..4f7a3ea203
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_events_dispatcher.js
@@ -0,0 +1,453 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { WindowGlobalMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs"
+);
+
+/**
+ * Check the basic behavior of on/off.
+ */
+add_task(async function test_add_remove_event_listener() {
+ const tab = await addTab("https://example.com/document-builder.sjs?html=tab");
+ const browsingContext = tab.linkedBrowser.browsingContext;
+ const contextDescriptor = {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext.browserId,
+ };
+
+ const root = createRootMessageHandler("session-id-event");
+ const monitoringEvents = await setupEventMonitoring(root);
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(await isSubscribed(root, browsingContext), false);
+
+ info("Add an listener for eventemitter.testEvent");
+ const events = [];
+ const onEvent = (event, data) => events.push(data.text);
+ await root.eventsDispatcher.on(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+ is(await isSubscribed(root, browsingContext), true);
+
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(events.length, 1);
+
+ info(
+ "Remove a listener for a callback not added before and check that the first one is still registered"
+ );
+ const anotherCallback = () => {};
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ anotherCallback
+ );
+ is(await isSubscribed(root, browsingContext), true);
+
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(events.length, 2);
+
+ info("Remove the listener for eventemitter.testEvent");
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+ is(await isSubscribed(root, browsingContext), false);
+
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(events.length, 2);
+
+ info("Add the listener for eventemitter.testEvent again");
+ await root.eventsDispatcher.on(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+ is(await isSubscribed(root, browsingContext), true);
+
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(events.length, 3);
+
+ info("Remove the listener for eventemitter.testEvent");
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+ is(await isSubscribed(root, browsingContext), false);
+
+ info("Remove the listener again to check the API will not throw");
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+
+ root.destroy();
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Check that two callbacks can subscribe to the same event in the same context
+ * in parallel.
+ */
+add_task(async function test_two_callbacks() {
+ const tab = await addTab("https://example.com/document-builder.sjs?html=tab");
+ const browsingContext = tab.linkedBrowser.browsingContext;
+ const contextDescriptor = {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext.browserId,
+ };
+
+ const root = createRootMessageHandler("session-id-event");
+ const monitoringEvents = await setupEventMonitoring(root);
+
+ info("Add an listener for eventemitter.testEvent");
+ const events = [];
+ const onEvent = (event, data) => events.push(data.text);
+ await root.eventsDispatcher.on(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(events.length, 1);
+
+ info("Add another listener for eventemitter.testEvent");
+ const otherevents = [];
+ const otherCallback = (event, data) => otherevents.push(data.text);
+ await root.eventsDispatcher.on(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ otherCallback
+ );
+ is(await isSubscribed(root, browsingContext), true);
+
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(events.length, 2);
+ is(otherevents.length, 1);
+
+ info("Remove the other listener for eventemitter.testEvent");
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ otherCallback
+ );
+ is(await isSubscribed(root, browsingContext), true);
+
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(events.length, 3);
+ is(otherevents.length, 1);
+
+ info("Remove the first listener for eventemitter.testEvent");
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+ is(await isSubscribed(root, browsingContext), false);
+
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(events.length, 3);
+ is(otherevents.length, 1);
+
+ root.destroy();
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Check that two callbacks can subscribe to the same event in the two contexts.
+ */
+add_task(async function test_two_contexts() {
+ const tab1 = await addTab("https://example.com/document-builder.sjs?html=1");
+ const browsingContext1 = tab1.linkedBrowser.browsingContext;
+
+ const tab2 = await addTab("https://example.com/document-builder.sjs?html=2");
+ const browsingContext2 = tab2.linkedBrowser.browsingContext;
+
+ const contextDescriptor1 = {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext1.browserId,
+ };
+ const contextDescriptor2 = {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext2.browserId,
+ };
+
+ const root = createRootMessageHandler("session-id-event");
+
+ const monitoringEvents = await setupEventMonitoring(root);
+
+ const events1 = [];
+ const onEvent1 = (event, data) => events1.push(data.text);
+ await root.eventsDispatcher.on(
+ "eventemitter.testEvent",
+ contextDescriptor1,
+ onEvent1
+ );
+ is(await isSubscribed(root, browsingContext1), true);
+ is(await isSubscribed(root, browsingContext2), false);
+
+ const events2 = [];
+ const onEvent2 = (event, data) => events2.push(data.text);
+ await root.eventsDispatcher.on(
+ "eventemitter.testEvent",
+ contextDescriptor2,
+ onEvent2
+ );
+ is(await isSubscribed(root, browsingContext1), true);
+ is(await isSubscribed(root, browsingContext2), true);
+
+ await emitTestEvent(root, browsingContext1, monitoringEvents);
+ is(events1.length, 1);
+ is(events2.length, 0);
+
+ await emitTestEvent(root, browsingContext2, monitoringEvents);
+ is(events1.length, 1);
+ is(events2.length, 1);
+
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptor1,
+ onEvent1
+ );
+ is(await isSubscribed(root, browsingContext1), false);
+ is(await isSubscribed(root, browsingContext2), true);
+
+ // No event expected here since the module for browsingContext1 is no longer
+ // subscribed
+ await emitTestEvent(root, browsingContext1, monitoringEvents);
+ is(events1.length, 1);
+ is(events2.length, 1);
+
+ // Whereas the module for browsingContext2 is still subscribed
+ await emitTestEvent(root, browsingContext2, monitoringEvents);
+ is(events1.length, 1);
+ is(events2.length, 2);
+
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptor2,
+ onEvent2
+ );
+ is(await isSubscribed(root, browsingContext1), false);
+ is(await isSubscribed(root, browsingContext2), false);
+
+ await emitTestEvent(root, browsingContext1, monitoringEvents);
+ await emitTestEvent(root, browsingContext2, monitoringEvents);
+ is(events1.length, 1);
+ is(events2.length, 2);
+
+ root.destroy();
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab1);
+});
+
+/**
+ * Check that adding and removing first listener for the specific context and then
+ * for the global context works as expected.
+ */
+add_task(
+ async function test_remove_context_event_listener_and_then_global_event_listener() {
+ const tab = await addTab(
+ "https://example.com/document-builder.sjs?html=tab"
+ );
+ const browsingContext = tab.linkedBrowser.browsingContext;
+ const contextDescriptor = {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext.browserId,
+ };
+ const contextDescriptorAll = {
+ type: ContextDescriptorType.All,
+ };
+
+ const root = createRootMessageHandler("session-id-event");
+ const monitoringEvents = await setupEventMonitoring(root);
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(await isSubscribed(root, browsingContext), false);
+
+ info("Add an listener for eventemitter.testEvent");
+ const events = [];
+ const onEvent = (event, data) => events.push(data.text);
+ await root.eventsDispatcher.on(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+ is(await isSubscribed(root, browsingContext), true);
+
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(events.length, 1);
+
+ info(
+ "Add another listener for eventemitter.testEvent, using global context"
+ );
+ const eventsAll = [];
+ const onEventAll = (event, data) => eventsAll.push(data.text);
+ await root.eventsDispatcher.on(
+ "eventemitter.testEvent",
+ contextDescriptorAll,
+ onEventAll
+ );
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(eventsAll.length, 1);
+ is(events.length, 2);
+
+ info("Remove the first listener for eventemitter.testEvent");
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+
+ info("Check that we are still subscribed to eventemitter.testEvent");
+ is(await isSubscribed(root, browsingContext), true);
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(eventsAll.length, 2);
+ is(events.length, 2);
+
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptorAll,
+ onEventAll
+ );
+ is(await isSubscribed(root, browsingContext), false);
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(eventsAll.length, 2);
+ is(events.length, 2);
+
+ root.destroy();
+ gBrowser.removeTab(tab);
+ }
+);
+
+/**
+ * Check that adding and removing first listener for the global context and then
+ * for the specific context works as expected.
+ */
+add_task(
+ async function test_global_event_listener_and_then_remove_context_event_listener() {
+ const tab = await addTab(
+ "https://example.com/document-builder.sjs?html=tab"
+ );
+ const browsingContext = tab.linkedBrowser.browsingContext;
+ const contextDescriptor = {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext.browserId,
+ };
+ const contextDescriptorAll = {
+ type: ContextDescriptorType.All,
+ };
+
+ const root = createRootMessageHandler("session-id-event");
+ const monitoringEvents = await setupEventMonitoring(root);
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(await isSubscribed(root, browsingContext), false);
+
+ info("Add an listener for eventemitter.testEvent");
+ const events = [];
+ const onEvent = (event, data) => events.push(data.text);
+ await root.eventsDispatcher.on(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+ is(await isSubscribed(root, browsingContext), true);
+
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(events.length, 1);
+
+ info(
+ "Add another listener for eventemitter.testEvent, using global context"
+ );
+ const eventsAll = [];
+ const onEventAll = (event, data) => eventsAll.push(data.text);
+ await root.eventsDispatcher.on(
+ "eventemitter.testEvent",
+ contextDescriptorAll,
+ onEventAll
+ );
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(eventsAll.length, 1);
+ is(events.length, 2);
+
+ info("Remove the global listener for eventemitter.testEvent");
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptorAll,
+ onEventAll
+ );
+
+ info(
+ "Check that we are still subscribed to eventemitter.testEvent for the specific context"
+ );
+ is(await isSubscribed(root, browsingContext), true);
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(eventsAll.length, 1);
+ is(events.length, 3);
+
+ await root.eventsDispatcher.off(
+ "eventemitter.testEvent",
+ contextDescriptor,
+ onEvent
+ );
+
+ is(await isSubscribed(root, browsingContext), false);
+ await emitTestEvent(root, browsingContext, monitoringEvents);
+ is(eventsAll.length, 1);
+ is(events.length, 3);
+
+ root.destroy();
+ gBrowser.removeTab(tab);
+ }
+);
+
+async function setupEventMonitoring(root) {
+ const monitoringEvents = [];
+ const onMonitoringEvent = (event, data) => monitoringEvents.push(data.text);
+ root.on("eventemitter.monitoringEvent", onMonitoringEvent);
+
+ registerCleanupFunction(() =>
+ root.off("eventemitter.monitoringEvent", onMonitoringEvent)
+ );
+
+ return monitoringEvents;
+}
+
+async function emitTestEvent(root, browsingContext, monitoringEvents) {
+ const count = monitoringEvents.length;
+ info("Call eventemitter.emitTestEvent");
+ await root.handleCommand({
+ moduleName: "eventemitter",
+ commandName: "emitTestEvent",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ });
+
+ // The monitoring event is always emitted, regardless of the status of the
+ // module. Wait for catching this event before resuming the assertions.
+ info("Wait for the monitoring event");
+ await BrowserTestUtils.waitForCondition(
+ () => monitoringEvents.length >= count + 1
+ );
+ is(monitoringEvents.length, count + 1);
+}
+
+function isSubscribed(root, browsingContext) {
+ info("Call eventemitter.isSubscribed");
+ return root.handleCommand({
+ moduleName: "eventemitter",
+ commandName: "isSubscribed",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ });
+}
diff --git a/remote/shared/messagehandler/test/browser/browser_events_handler.js b/remote/shared/messagehandler/test/browser/browser_events_handler.js
new file mode 100644
index 0000000000..4898a5957b
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_events_handler.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that the window-global-handler-created event gets emitted for each
+ * individual frame's browsing context.
+ */
+add_task(async function test_windowGlobalHandlerCreated() {
+ const events = [];
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-event_with_frames"
+ );
+
+ info("Add a new session data item to get window global handlers created");
+ await rootMessageHandler.addSessionData({
+ moduleName: "command",
+ category: "browser_session_data_browser_element",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ values: [true],
+ });
+
+ const onEvent = (evtName, wrappedEvt) => {
+ if (wrappedEvt.name === "window-global-handler-created") {
+ console.info(`Received event for context ${wrappedEvt.data.contextId}`);
+ events.push(wrappedEvt.data);
+ }
+ };
+ rootMessageHandler.on("message-handler-event", onEvent);
+
+ info("Navigate the initial tab to the test URL");
+ const browser = gBrowser.selectedTab.linkedBrowser;
+ await loadURL(browser, createTestMarkupWithFrames());
+
+ const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
+ is(contexts.length, 4, "Test tab has 3 children contexts (4 in total)");
+
+ // Wait for all the events
+ await TestUtils.waitForCondition(() => events.length >= 4);
+
+ for (const context of contexts) {
+ const contextEvents = events.filter(evt => {
+ return (
+ evt.contextId === context.id &&
+ evt.innerWindowId === context.currentWindowGlobal.innerWindowId
+ );
+ });
+ is(contextEvents.length, 1, `Found event for context ${context.id}`);
+ }
+
+ rootMessageHandler.off("message-handler-event", onEvent);
+ rootMessageHandler.destroy();
+});
diff --git a/remote/shared/messagehandler/test/browser/browser_events_module.js b/remote/shared/messagehandler/test/browser/browser_events_module.js
new file mode 100644
index 0000000000..4032559e9a
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_events_module.js
@@ -0,0 +1,283 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { RootMessageHandlerRegistry } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs"
+);
+const { RootMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs"
+);
+const { WindowGlobalMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs"
+);
+
+/**
+ * Emit an event from a WindowGlobal module triggered by a specific command.
+ * Check that the event is emitted on the RootMessageHandler as well as on
+ * the parent process MessageHandlerRegistry.
+ */
+add_task(async function test_event() {
+ const tab = BrowserTestUtils.addTab(
+ gBrowser,
+ "https://example.com/document-builder.sjs?html=tab"
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ const browsingContext = tab.linkedBrowser.browsingContext;
+
+ const rootMessageHandler = createRootMessageHandler("session-id-event");
+
+ // Events are emitted both as generic message-handler-event events as well
+ // as under their own name. We expect to receive the event for both.
+ const onHandlerEvent = rootMessageHandler.once("message-handler-event");
+ const onNamedEvent = rootMessageHandler.once("event-from-window-global");
+ // MessageHandlerRegistry should forward all the message-handler-events.
+ const onRegistryEvent = RootMessageHandlerRegistry.once(
+ "message-handler-registry-event"
+ );
+
+ callTestEmitEvent(rootMessageHandler, browsingContext.id);
+
+ const messageHandlerEvent = await onHandlerEvent;
+ is(
+ messageHandlerEvent.name,
+ "event-from-window-global",
+ "Received event on the ROOT MessageHandler"
+ );
+ is(
+ messageHandlerEvent.data.text,
+ `event from ${browsingContext.id}`,
+ "Received the expected payload"
+ );
+
+ const namedEvent = await onNamedEvent;
+ is(
+ namedEvent.text,
+ `event from ${browsingContext.id}`,
+ "Received the expected payload"
+ );
+
+ const registryEvent = await onRegistryEvent;
+ is(
+ registryEvent,
+ messageHandlerEvent,
+ "The event forwarded by the MessageHandlerRegistry is identical to the MessageHandler event"
+ );
+
+ rootMessageHandler.destroy();
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Emit an event from a Root module triggered by a specific command.
+ * Check that the event is emitted on the RootMessageHandler.
+ */
+add_task(async function test_root_event() {
+ const rootMessageHandler = createRootMessageHandler("session-id-root_event");
+
+ // events are emitted both as generic message-handler-event events as
+ // well as under their own name. We expect to receive the event for both.
+ const onHandlerEvent = rootMessageHandler.once("message-handler-event");
+ const onNamedEvent = rootMessageHandler.once("event-from-root");
+
+ rootMessageHandler.handleCommand({
+ moduleName: "event",
+ commandName: "testEmitRootEvent",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ });
+
+ const { name, data } = await onHandlerEvent;
+ is(name, "event-from-root", "Received event on the ROOT MessageHandler");
+ is(data.text, "event from root", "Received the expected payload");
+
+ const namedEvent = await onNamedEvent;
+ is(namedEvent.text, "event from root", "Received the expected payload");
+
+ rootMessageHandler.destroy();
+});
+
+/**
+ * Emit an event from a windowglobal-in-root module triggered by a specific command.
+ * Check that the event is emitted on the RootMessageHandler.
+ */
+add_task(async function test_windowglobal_in_root_event() {
+ const tab = BrowserTestUtils.addTab(
+ gBrowser,
+ "https://example.com/document-builder.sjs?html=tab"
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ const browsingContext = tab.linkedBrowser.browsingContext;
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-windowglobal_in_root_event"
+ );
+
+ // events are emitted both as generic message-handler-event events as
+ // well as under their own name. We expect to receive the event for both.
+ const onHandlerEvent = rootMessageHandler.once("message-handler-event");
+ const onNamedEvent = rootMessageHandler.once(
+ "event-from-window-global-in-root"
+ );
+ rootMessageHandler.handleCommand({
+ moduleName: "event",
+ commandName: "testEmitWindowGlobalInRootEvent",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ });
+
+ const { name, data } = await onHandlerEvent;
+ is(
+ name,
+ "event-from-window-global-in-root",
+ "Received event on the ROOT MessageHandler"
+ );
+ is(
+ data.text,
+ `windowglobal-in-root event for ${browsingContext.id}`,
+ "Received the expected payload"
+ );
+
+ const namedEvent = await onNamedEvent;
+ is(
+ namedEvent.text,
+ `windowglobal-in-root event for ${browsingContext.id}`,
+ "Received the expected payload"
+ );
+
+ rootMessageHandler.destroy();
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Emit an event from a windowglobal module, but from 2 different sessions.
+ * Check that the event is emitted by the corresponding RootMessageHandler as
+ * well as by the parent process MessageHandlerRegistry.
+ */
+add_task(async function test_event_multisession() {
+ const tab = BrowserTestUtils.addTab(
+ gBrowser,
+ "https://example.com/document-builder.sjs?html=tab"
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ const browsingContextId = tab.linkedBrowser.browsingContext.id;
+
+ const root1 = createRootMessageHandler("session-id-event_multisession-1");
+ let root1Events = 0;
+ const onRoot1Event = function(evtName, wrappedEvt) {
+ if (wrappedEvt.name === "event-from-window-global") {
+ root1Events++;
+ }
+ };
+ root1.on("message-handler-event", onRoot1Event);
+
+ const root2 = createRootMessageHandler("session-id-event_multisession-2");
+ let root2Events = 0;
+ const onRoot2Event = function(evtName, wrappedEvt) {
+ if (wrappedEvt.name === "event-from-window-global") {
+ root2Events++;
+ }
+ };
+ root2.on("message-handler-event", onRoot2Event);
+
+ let registryEvents = 0;
+ const onRegistryEvent = function(evtName, wrappedEvt) {
+ if (wrappedEvt.name === "event-from-window-global") {
+ registryEvents++;
+ }
+ };
+ RootMessageHandlerRegistry.on(
+ "message-handler-registry-event",
+ onRegistryEvent
+ );
+
+ callTestEmitEvent(root1, browsingContextId);
+ callTestEmitEvent(root2, browsingContextId);
+
+ info("Wait for root1 event to be received");
+ await TestUtils.waitForCondition(() => root1Events === 1);
+ info("Wait for root2 event to be received");
+ await TestUtils.waitForCondition(() => root2Events === 1);
+
+ await TestUtils.waitForTick();
+ is(root1Events, 1, "Session 1 only received 1 event");
+ is(root2Events, 1, "Session 2 only received 1 event");
+ is(
+ registryEvents,
+ 2,
+ "MessageHandlerRegistry forwarded events from both sessions"
+ );
+
+ root1.off("message-handler-event", onRoot1Event);
+ root2.off("message-handler-event", onRoot2Event);
+ RootMessageHandlerRegistry.off(
+ "message-handler-registry-event",
+ onRegistryEvent
+ );
+ root1.destroy();
+ root2.destroy();
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Test that events can be emitted from individual frame contexts and that
+ * events going through a shared content process MessageHandlerRegistry are not
+ * duplicated.
+ */
+add_task(async function test_event_with_frames() {
+ info("Navigate the initial tab to the test URL");
+ const tab = gBrowser.selectedTab;
+ await loadURL(tab.linkedBrowser, createTestMarkupWithFrames());
+
+ const contexts = tab.linkedBrowser.browsingContext.getAllBrowsingContextsInSubtree();
+ is(contexts.length, 4, "Test tab has 3 children contexts (4 in total)");
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-event_with_frames"
+ );
+
+ const rootEvents = [];
+ const onRootEvent = function(evtName, wrappedEvt) {
+ if (wrappedEvt.name === "event-from-window-global") {
+ rootEvents.push(wrappedEvt.data.text);
+ }
+ };
+ rootMessageHandler.on("message-handler-event", onRootEvent);
+
+ const namedEvents = [];
+ const onNamedEvent = (name, event) => namedEvents.push(event.text);
+ rootMessageHandler.on("event-from-window-global", onNamedEvent);
+
+ for (const context of contexts) {
+ callTestEmitEvent(rootMessageHandler, context.id);
+ info("Wait for root event to be received in both event arrays");
+ await TestUtils.waitForCondition(() =>
+ [namedEvents, rootEvents].every(events =>
+ events.includes(`event from ${context.id}`)
+ )
+ );
+ }
+
+ info("Wait for a bit and check that we did not receive duplicated events");
+ await TestUtils.waitForTick();
+ is(rootEvents.length, 4, "Only received 4 events");
+
+ rootMessageHandler.off("message-handler-event", onRootEvent);
+ rootMessageHandler.off("event-from-window-global", onNamedEvent);
+ rootMessageHandler.destroy();
+});
+
+function callTestEmitEvent(rootMessageHandler, browsingContextId) {
+ rootMessageHandler.handleCommand({
+ moduleName: "event",
+ commandName: "testEmitEvent",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+}
diff --git a/remote/shared/messagehandler/test/browser/browser_frame_context_utils.js b/remote/shared/messagehandler/test/browser/browser_frame_context_utils.js
new file mode 100644
index 0000000000..eec9ce048f
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_frame_context_utils.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { isBrowsingContextCompatible } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/transports/FrameContextUtils.sys.mjs"
+);
+const TEST_COM_PAGE = "https://example.com/document-builder.sjs?html=com";
+const TEST_NET_PAGE = "https://example.net/document-builder.sjs?html=net";
+
+// Test helpers from FrameContextUtils in various processes.
+add_task(async function() {
+ const tab1 = BrowserTestUtils.addTab(gBrowser, TEST_COM_PAGE);
+ const contentBrowser1 = tab1.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(contentBrowser1);
+ const browserId1 = contentBrowser1.browsingContext.browserId;
+
+ const tab2 = BrowserTestUtils.addTab(gBrowser, TEST_NET_PAGE);
+ const contentBrowser2 = tab2.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(contentBrowser2);
+ const browserId2 = contentBrowser2.browsingContext.browserId;
+
+ const { extension, sidebarBrowser } = await installSidebarExtension();
+
+ const tab3 = BrowserTestUtils.addTab(
+ gBrowser,
+ `moz-extension://${extension.uuid}/tab.html`
+ );
+ const { bcId } = await extension.awaitMessage("tab-loaded");
+ const tabExtensionBrowser = BrowsingContext.get(bcId).top.embedderElement;
+
+ const parentBrowser1 = createParentBrowserElement(tab1, "content");
+ const parentBrowser2 = createParentBrowserElement(tab1, "chrome");
+
+ info("Check browsing context compatibility for content browser 1");
+ await checkBrowsingContextCompatible(contentBrowser1, undefined, true);
+ await checkBrowsingContextCompatible(contentBrowser1, browserId1, true);
+ await checkBrowsingContextCompatible(contentBrowser1, browserId2, false);
+
+ info("Check browsing context compatibility for content browser 2");
+ await checkBrowsingContextCompatible(contentBrowser2, undefined, true);
+ await checkBrowsingContextCompatible(contentBrowser2, browserId1, false);
+ await checkBrowsingContextCompatible(contentBrowser2, browserId2, true);
+
+ info("Check browsing context compatibility for parent browser 1");
+ await checkBrowsingContextCompatible(parentBrowser1, undefined, false);
+ await checkBrowsingContextCompatible(parentBrowser1, browserId1, false);
+ await checkBrowsingContextCompatible(parentBrowser1, browserId2, false);
+
+ info("Check browsing context compatibility for parent browser 2");
+ await checkBrowsingContextCompatible(parentBrowser2, undefined, false);
+ await checkBrowsingContextCompatible(parentBrowser2, browserId1, false);
+ await checkBrowsingContextCompatible(parentBrowser2, browserId2, false);
+
+ info("Check browsing context compatibility for extension");
+ await checkBrowsingContextCompatible(sidebarBrowser, undefined, false);
+ await checkBrowsingContextCompatible(sidebarBrowser, browserId1, false);
+ await checkBrowsingContextCompatible(sidebarBrowser, browserId2, false);
+
+ info("Check browsing context compatibility for extension viewed in a tab");
+ await checkBrowsingContextCompatible(tabExtensionBrowser, undefined, false);
+ await checkBrowsingContextCompatible(tabExtensionBrowser, browserId1, false);
+ await checkBrowsingContextCompatible(tabExtensionBrowser, browserId2, false);
+
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab3);
+ await extension.unload();
+});
+
+async function checkBrowsingContextCompatible(browser, browserId, expected) {
+ const options = { browserId };
+ info("Check browsing context compatibility from the parent process");
+ is(isBrowsingContextCompatible(browser.browsingContext, options), expected);
+
+ info(
+ "Check browsing context compatibility from the browsing context's process"
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [browserId, expected],
+ (_browserId, _expected) => {
+ const FrameContextUtils = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/transports/FrameContextUtils.sys.mjs"
+ );
+ is(
+ FrameContextUtils.isBrowsingContextCompatible(content.browsingContext, {
+ browserId: _browserId,
+ }),
+ _expected
+ );
+ }
+ );
+}
diff --git a/remote/shared/messagehandler/test/browser/browser_handle_command_errors.js b/remote/shared/messagehandler/test/browser/browser_handle_command_errors.js
new file mode 100644
index 0000000000..0a46255d89
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_handle_command_errors.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { RootMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs"
+);
+const { WindowGlobalMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs"
+);
+
+// Check that errors from WindowGlobal modules can be caught by the consumer
+// of the RootMessageHandler.
+add_task(async function test_module_error() {
+ const browsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+
+ const rootMessageHandler = createRootMessageHandler("session-id-error");
+
+ info("Call a module method which will throw");
+ try {
+ await rootMessageHandler.handleCommand({
+ moduleName: "commandwindowglobalonly",
+ commandName: "testError",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+ ok(false, "Error from window global module was not caught");
+ } catch (e) {
+ ok(true, "Error from window global module caught");
+ }
+
+ rootMessageHandler.destroy();
+});
+
+// Check that sending commands to incorrect destinations creates an error which
+// can be caught by the consumer of the RootMessageHandler.
+add_task(async function test_destination_error() {
+ const rootMessageHandler = createRootMessageHandler("session-id-error");
+
+ const fakeBrowsingContextId = -1;
+ ok(
+ !BrowsingContext.get(fakeBrowsingContextId),
+ "No browsing context matches fakeBrowsingContextId"
+ );
+
+ info("Call a valid module method, but on a non-existent browsing context id");
+ Assert.throws(
+ () =>
+ rootMessageHandler.handleCommand({
+ moduleName: "commandwindowglobalonly",
+ commandName: "testOnlyInWindowGlobal",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: fakeBrowsingContextId,
+ },
+ }),
+ err => err.message == `Unable to find a BrowsingContext for id -1`
+ );
+
+ rootMessageHandler.destroy();
+});
+
+add_task(async function test_invalid_module_error() {
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-missing_module"
+ );
+
+ info("Attempt to call a Root module which has a syntax error");
+ Assert.throws(
+ () =>
+ rootMessageHandler.handleCommand({
+ moduleName: "invalid",
+ commandName: "someMethod",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ }),
+ err =>
+ err.name === "SyntaxError" &&
+ err.message == "expected expression, got ';'"
+ );
+
+ rootMessageHandler.destroy();
+});
+
+add_task(async function test_missing_root_module_error() {
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-missing_module"
+ );
+
+ info("Attempt to call a Root module which doesn't exist");
+ Assert.throws(
+ () =>
+ rootMessageHandler.handleCommand({
+ moduleName: "missingmodule",
+ commandName: "someMethod",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ }),
+ err =>
+ err.name == "UnsupportedCommandError" &&
+ err.message ==
+ `missingmodule.someMethod not supported for destination ROOT`
+ );
+
+ rootMessageHandler.destroy();
+});
+
+add_task(async function test_missing_windowglobal_module_error() {
+ const browsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-missing_windowglobal_module"
+ );
+
+ info("Attempt to call a WindowGlobal module which doesn't exist");
+ Assert.throws(
+ () =>
+ rootMessageHandler.handleCommand({
+ moduleName: "missingmodule",
+ commandName: "someMethod",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ }),
+ err =>
+ err.name == "UnsupportedCommandError" &&
+ err.message ==
+ `missingmodule.someMethod not supported for destination WINDOW_GLOBAL`
+ );
+
+ rootMessageHandler.destroy();
+});
+
+add_task(async function test_missing_root_method_error() {
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-missing_root_method"
+ );
+
+ info("Attempt to call an invalid method on a Root module");
+ Assert.throws(
+ () =>
+ rootMessageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "wrongMethod",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ }),
+ err =>
+ err.name == "UnsupportedCommandError" &&
+ err.message == `command.wrongMethod not supported for destination ROOT`
+ );
+
+ rootMessageHandler.destroy();
+});
+
+add_task(async function test_missing_windowglobal_method_error() {
+ const browsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-missing_windowglobal_method"
+ );
+
+ info("Attempt to call an invalid method on a WindowGlobal module");
+ Assert.throws(
+ () =>
+ rootMessageHandler.handleCommand({
+ moduleName: "commandwindowglobalonly",
+ commandName: "wrongMethod",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ }),
+ err =>
+ err.name == "UnsupportedCommandError" &&
+ err.message ==
+ `commandwindowglobalonly.wrongMethod not supported for destination WINDOW_GLOBAL`
+ );
+
+ rootMessageHandler.destroy();
+});
+
+/**
+ * This test checks that even if a command is rerouted to another command after
+ * the RootMessageHandler, we still check the new command and log a useful
+ * error message.
+ *
+ * This illustrates why it is important to perform the command check at each
+ * layer of the MessageHandler network.
+ */
+add_task(async function test_missing_intermediary_method_error() {
+ const browsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-missing_intermediary_method"
+ );
+
+ info(
+ "Call a (valid) command that relies on another (missing) command on a WindowGlobal module"
+ );
+ await Assert.rejects(
+ rootMessageHandler.handleCommand({
+ moduleName: "commandwindowglobalonly",
+ commandName: "testMissingIntermediaryMethod",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ }),
+ err =>
+ err.name == "UnsupportedCommandError" &&
+ err.message ==
+ `commandwindowglobalonly.missingMethod not supported for destination WINDOW_GLOBAL`
+ );
+
+ rootMessageHandler.destroy();
+});
diff --git a/remote/shared/messagehandler/test/browser/browser_handle_command_retry.js b/remote/shared/messagehandler/test/browser/browser_handle_command_retry.js
new file mode 100644
index 0000000000..1d26ee01ee
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_handle_command_retry.js
@@ -0,0 +1,243 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { WindowGlobalMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs"
+);
+
+// We are forcing the actors to shutdown while queries are unresolved.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Actor 'MessageHandlerFrame' destroyed before query 'MessageHandlerFrameParent:sendCommand' was resolved/
+);
+
+// The tests in this file assert the retry behavior for MessageHandler commands.
+// We call "blocked" commands from resources/modules/windowglobal/retry.jsm and
+// then trigger reload and navigations to simulate AbortErrors and force the
+// MessageHandler to retry the commands, when possible.
+
+// Test that without retry behavior, a pending command rejects when the
+// underlying JSWindowActor pair is destroyed.
+add_task(async function test_no_retry() {
+ const tab = BrowserTestUtils.addTab(
+ gBrowser,
+ "https://example.com/document-builder.sjs?html=tab"
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ const browsingContext = tab.linkedBrowser.browsingContext;
+
+ const rootMessageHandler = createRootMessageHandler("session-id-no-retry");
+
+ try {
+ info("Call a module method which will throw");
+ const onBlockedOneTime = rootMessageHandler.handleCommand({
+ moduleName: "retry",
+ commandName: "blockedOneTime",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ });
+
+ // Reloading the tab will reject the pending query with an AbortError.
+ await BrowserTestUtils.reloadTab(tab);
+
+ try {
+ await onBlockedOneTime;
+ ok("false", "onBlockedOneTime should not have resolved");
+ } catch (e) {
+ is(
+ e.name,
+ "AbortError",
+ "Caught the expected abort error when reloading"
+ );
+ }
+ } finally {
+ await cleanup(rootMessageHandler, tab);
+ }
+});
+
+// Test various commands, which all need a different number of "retries" to
+// succeed. Check that they only resolve when the expected number of "retries"
+// was reached. For commands which require more "retries" than we allow, check
+// that we still fail with an AbortError once all the attempts are consumed.
+add_task(async function test_retry() {
+ const tab = BrowserTestUtils.addTab(
+ gBrowser,
+ "https://example.com/document-builder.sjs?html=tab"
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ const browsingContext = tab.linkedBrowser.browsingContext;
+
+ const rootMessageHandler = createRootMessageHandler("session-id-retry");
+
+ try {
+ // This command will return if called twice.
+ const onBlockedOneTime = rootMessageHandler.handleCommand({
+ moduleName: "retry",
+ commandName: "blockedOneTime",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ params: {
+ foo: "bar",
+ },
+ retryOnAbort: true,
+ });
+
+ // This command will return if called three times.
+ const onBlockedTenTimes = rootMessageHandler.handleCommand({
+ moduleName: "retry",
+ commandName: "blockedTenTimes",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ params: {
+ foo: "baz",
+ },
+ retryOnAbort: true,
+ });
+
+ // This command will return if called twelve times, which is greater than the
+ // maximum amount of retries allowed.
+ const onBlockedElevenTimes = rootMessageHandler.handleCommand({
+ moduleName: "retry",
+ commandName: "blockedElevenTimes",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ retryOnAbort: true,
+ });
+
+ info("Reload one time");
+ await BrowserTestUtils.reloadTab(tab);
+
+ info("blockedOneTime should resolve on the first retry");
+ let { callsToCommand, foo } = await onBlockedOneTime;
+ is(
+ callsToCommand,
+ 2,
+ "The command was called twice (initial call + 1 retry)"
+ );
+ is(foo, "bar", "The parameter was sent when the command was retried");
+
+ // We already reloaded 1 time. Reload 9 more times to unblock blockedTenTimes.
+ for (let i = 2; i < 11; i++) {
+ info("blockedTenTimes/blockedElevenTimes should not have resolved yet");
+ ok(!(await hasPromiseResolved(onBlockedTenTimes)));
+ ok(!(await hasPromiseResolved(onBlockedElevenTimes)));
+
+ info(`Reload the tab (time: ${i})`);
+ await BrowserTestUtils.reloadTab(tab);
+ }
+
+ info("blockedTenTimes should resolve on the 10th reload");
+ ({ callsToCommand, foo } = await onBlockedTenTimes);
+ is(
+ callsToCommand,
+ 11,
+ "The command was called 11 times (initial call + 10 retry)"
+ );
+ is(foo, "baz", "The parameter was sent when the command was retried");
+
+ info("Reload one more time");
+ await BrowserTestUtils.reloadTab(tab);
+
+ try {
+ info(
+ "The call to blockedElevenTimes now exceeds the maximum attempts allowed"
+ );
+ await onBlockedElevenTimes;
+ ok("false", "blockedElevenTimes should not have resolved");
+ } catch (e) {
+ is(
+ e.name,
+ "AbortError",
+ "Caught the expected abort error when reloading"
+ );
+ }
+ } finally {
+ await cleanup(rootMessageHandler, tab);
+ }
+});
+
+// Test cross-group navigations to check that the retry mechanism will
+// transparently switch to the new Browsing Context created by the cross-group
+// navigation.
+add_task(async function test_retry_cross_group() {
+ const tab = BrowserTestUtils.addTab(
+ gBrowser,
+ "https://example.com/document-builder.sjs?html=COM" +
+ // Attach an unload listener to prevent the page from going into bfcache,
+ // so that pending queries will be rejected with an AbortError.
+ "<script type='text/javascript'>window.onunload = function() {};</script>"
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ const browsingContext = tab.linkedBrowser.browsingContext;
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-retry-cross-group"
+ );
+
+ try {
+ // This command hangs and only returns if the current domain is example.net.
+ // We send the command while on example.com, perform a series of reload and
+ // navigations, and the retry mechanism should allow onBlockedOnNetDomain to
+ // resolve.
+ const onBlockedOnNetDomain = rootMessageHandler.handleCommand({
+ moduleName: "retry",
+ commandName: "blockedOnNetDomain",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ params: {
+ foo: "bar",
+ },
+ retryOnAbort: true,
+ });
+
+ info("Reload one time");
+ await BrowserTestUtils.reloadTab(tab);
+
+ info("blockedOnNetDomain should not have resolved yet");
+ ok(!(await hasPromiseResolved(onBlockedOnNetDomain)));
+
+ info(
+ "Navigate to example.net with COOP headers to destroy browsing context"
+ );
+ await loadURL(
+ tab.linkedBrowser,
+ "https://example.net/document-builder.sjs?headers=Cross-Origin-Opener-Policy:same-origin&html=NET"
+ );
+
+ info("blockedOnNetDomain should resolve now");
+ let { foo } = await onBlockedOnNetDomain;
+ is(foo, "bar", "The parameter was sent when the command was retried");
+ } finally {
+ await cleanup(rootMessageHandler, tab);
+ }
+});
+
+async function cleanup(rootMessageHandler, tab) {
+ const browsingContext = tab.linkedBrowser.browsingContext;
+ // Cleanup global JSM state in the test module.
+ await rootMessageHandler.handleCommand({
+ moduleName: "retry",
+ commandName: "cleanup",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ });
+
+ rootMessageHandler.destroy();
+ gBrowser.removeTab(tab);
+}
diff --git a/remote/shared/messagehandler/test/browser/browser_handle_simple_command.js b/remote/shared/messagehandler/test/browser/browser_handle_simple_command.js
new file mode 100644
index 0000000000..a5024e5cce
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_handle_simple_command.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { RootMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs"
+);
+const { WindowGlobalMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs"
+);
+
+// Test calling methods only implemented in the root version of a module.
+add_task(async function test_rootModule_command() {
+ const rootMessageHandler = createRootMessageHandler("session-id-rootModule");
+ const rootValue = await rootMessageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "testRootModule",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ });
+
+ is(
+ rootValue,
+ "root-value",
+ "Retrieved the expected value from testRootModule"
+ );
+
+ rootMessageHandler.destroy();
+});
+
+// Test calling methods only implemented in the windowglobal-in-root version of
+// a module.
+add_task(async function test_windowglobalInRootModule_command() {
+ const browsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-windowglobalInRootModule"
+ );
+ const interceptedValue = await rootMessageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "testInterceptModule",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+
+ is(
+ interceptedValue,
+ "intercepted-value",
+ "Retrieved the expected value from testInterceptModule"
+ );
+
+ rootMessageHandler.destroy();
+});
+
+// Test calling methods only implemented in the windowglobal version of a
+// module.
+add_task(async function test_windowglobalModule_command() {
+ const browsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-windowglobalModule"
+ );
+ const windowGlobalValue = await rootMessageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "testWindowGlobalModule",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+
+ is(
+ windowGlobalValue,
+ "windowglobal-value",
+ "Retrieved the expected value from testWindowGlobalModule"
+ );
+
+ rootMessageHandler.destroy();
+});
+
+// Test calling a method on a module which is only available in the "windowglobal"
+// folder. This will check that the MessageHandler/ModuleCache correctly moves
+// on to the next layer when no implementation can be found in the root layer.
+add_task(async function test_windowglobalOnlyModule_command() {
+ const browsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+
+ const rootMessageHandler = createRootMessageHandler(
+ "session-id-windowglobalOnlyModule"
+ );
+ const windowGlobalOnlyValue = await rootMessageHandler.handleCommand({
+ moduleName: "commandwindowglobalonly",
+ commandName: "testOnlyInWindowGlobal",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+
+ is(
+ windowGlobalOnlyValue,
+ "only-in-windowglobal",
+ "Retrieved the expected value from testOnlyInWindowGlobal"
+ );
+
+ rootMessageHandler.destroy();
+});
+
+// Try to create 2 sessions which will both set values in individual modules
+// via a command `testSetValue`, and then retrieve the values via another
+// command `testGetValue`.
+// This will ensure that different sessions use different module instances.
+add_task(async function test_multisession() {
+ const browsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+
+ const rootMessageHandler1 = createRootMessageHandler(
+ "session-id-multisession-1"
+ );
+ const rootMessageHandler2 = createRootMessageHandler(
+ "session-id-multisession-2"
+ );
+
+ info("Set value for session 1");
+ await rootMessageHandler1.handleCommand({
+ moduleName: "command",
+ commandName: "testSetValue",
+ params: { value: "session1-value" },
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+
+ info("Set value for session 2");
+ await rootMessageHandler2.handleCommand({
+ moduleName: "command",
+ commandName: "testSetValue",
+ params: { value: "session2-value" },
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+
+ const session1Value = await rootMessageHandler1.handleCommand({
+ moduleName: "command",
+ commandName: "testGetValue",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+
+ is(
+ session1Value,
+ "session1-value",
+ "Retrieved the expected value for session 1"
+ );
+
+ const session2Value = await rootMessageHandler2.handleCommand({
+ moduleName: "command",
+ commandName: "testGetValue",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+
+ is(
+ session2Value,
+ "session2-value",
+ "Retrieved the expected value for session 2"
+ );
+
+ rootMessageHandler1.destroy();
+ rootMessageHandler2.destroy();
+});
+
+// Test calling a method from the windowglobal-in-root module which will
+// internally forward to the windowglobal module and will return a composite
+// result built both in parent and content process.
+add_task(async function test_forwarding_command() {
+ const browsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+
+ const rootMessageHandler = createRootMessageHandler("session-id-forwarding");
+ const interceptAndForwardValue = await rootMessageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "testInterceptAndForwardModule",
+ params: { id: "value" },
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+
+ is(
+ interceptAndForwardValue,
+ "intercepted-and-forward+forward-to-windowglobal-value",
+ "Retrieved the expected value from testInterceptAndForwardModule"
+ );
+
+ rootMessageHandler.destroy();
+});
diff --git a/remote/shared/messagehandler/test/browser/browser_registry.js b/remote/shared/messagehandler/test/browser/browser_registry.js
new file mode 100644
index 0000000000..271597526e
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_registry.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { MessageHandlerRegistry } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.sys.mjs"
+);
+const { RootMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs"
+);
+
+add_task(async function test_messageHandlerRegistry_API() {
+ const sessionId = 1;
+ const type = RootMessageHandler.type;
+
+ const rootMessageHandlerRegistry = new MessageHandlerRegistry(type);
+
+ const rootMessageHandler = rootMessageHandlerRegistry.getOrCreateMessageHandler(
+ sessionId
+ );
+ ok(rootMessageHandler, "Valid ROOT MessageHandler created");
+
+ const contextId = rootMessageHandler.contextId;
+ ok(contextId, "ROOT MessageHandler has a valid contextId");
+
+ is(
+ rootMessageHandler,
+ rootMessageHandlerRegistry.getExistingMessageHandler(sessionId),
+ "ROOT MessageHandler can be retrieved from the registry"
+ );
+
+ rootMessageHandler.destroy();
+ ok(
+ !rootMessageHandlerRegistry.getExistingMessageHandler(sessionId),
+ "Destroyed ROOT MessageHandler is no longer returned by the Registry"
+ );
+});
diff --git a/remote/shared/messagehandler/test/browser/browser_session_data.js b/remote/shared/messagehandler/test/browser/browser_session_data.js
new file mode 100644
index 0000000000..f5ba9d585c
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_session_data.js
@@ -0,0 +1,272 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { MessageHandlerRegistry } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.sys.mjs"
+);
+const { RootMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs"
+);
+const { SessionData } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs"
+);
+
+const TEST_PAGE = "http://example.com/document-builder.sjs?html=tab";
+
+add_task(async function test_sessionData() {
+ info("Navigate the initial tab to the test URL");
+ const tab1 = gBrowser.selectedTab;
+ await loadURL(tab1.linkedBrowser, TEST_PAGE);
+
+ const sessionId = "sessionData-test";
+
+ const rootMessageHandlerRegistry = new MessageHandlerRegistry(
+ RootMessageHandler.type
+ );
+
+ const rootMessageHandler = rootMessageHandlerRegistry.getOrCreateMessageHandler(
+ sessionId
+ );
+ ok(rootMessageHandler, "Valid ROOT MessageHandler created");
+
+ const sessionData = rootMessageHandler.sessionData;
+ ok(
+ sessionData instanceof SessionData,
+ "ROOT MessageHandler has a valid sessionData"
+ );
+
+ let sessionDataSnapshot = await getSessionDataFromContent();
+ is(sessionDataSnapshot.size, 0, "session data is empty");
+
+ info("Store a string value in session data");
+ sessionData.updateSessionData([
+ {
+ method: "add",
+ moduleName: "fakemodule",
+ category: "testCategory",
+ contextDescriptor: contextDescriptorAll,
+ values: ["value-1"],
+ },
+ ]);
+
+ sessionDataSnapshot = await getSessionDataFromContent();
+ is(sessionDataSnapshot.size, 1, "session data contains 1 session");
+ ok(sessionDataSnapshot.has(sessionId));
+ let snapshot = sessionDataSnapshot.get(sessionId);
+ ok(Array.isArray(snapshot));
+ is(snapshot.length, 1);
+
+ const stringDataItem = snapshot[0];
+ checkSessionDataItem(
+ stringDataItem,
+ "fakemodule",
+ "testCategory",
+ ContextDescriptorType.All,
+ "value-1"
+ );
+
+ info("Store a number value in session data");
+ sessionData.updateSessionData([
+ {
+ method: "add",
+ moduleName: "fakemodule",
+ category: "testCategory",
+ contextDescriptor: contextDescriptorAll,
+ values: [12],
+ },
+ ]);
+ snapshot = (await getSessionDataFromContent()).get(sessionId);
+ is(snapshot.length, 2);
+
+ const numberDataItem = snapshot[1];
+ checkSessionDataItem(
+ numberDataItem,
+ "fakemodule",
+ "testCategory",
+ ContextDescriptorType.All,
+ 12
+ );
+
+ info("Store a boolean value in session data");
+ sessionData.updateSessionData([
+ {
+ method: "add",
+ moduleName: "fakemodule",
+ category: "testCategory",
+ contextDescriptor: contextDescriptorAll,
+ values: [true],
+ },
+ ]);
+ snapshot = (await getSessionDataFromContent()).get(sessionId);
+ is(snapshot.length, 3);
+
+ const boolDataItem = snapshot[2];
+ checkSessionDataItem(
+ boolDataItem,
+ "fakemodule",
+ "testCategory",
+ ContextDescriptorType.All,
+ true
+ );
+
+ info("Remove one value");
+ sessionData.updateSessionData([
+ {
+ method: "remove",
+ moduleName: "fakemodule",
+ category: "testCategory",
+ contextDescriptor: contextDescriptorAll,
+ values: [12],
+ },
+ ]);
+ snapshot = (await getSessionDataFromContent()).get(sessionId);
+ is(snapshot.length, 2);
+ checkSessionDataItem(
+ snapshot[0],
+ "fakemodule",
+ "testCategory",
+ ContextDescriptorType.All,
+ "value-1"
+ );
+ checkSessionDataItem(
+ snapshot[1],
+ "fakemodule",
+ "testCategory",
+ ContextDescriptorType.All,
+ true
+ );
+
+ info("Remove all values");
+ sessionData.updateSessionData([
+ {
+ method: "remove",
+ moduleName: "fakemodule",
+ category: "testCategory",
+ contextDescriptor: contextDescriptorAll,
+ values: ["value-1", true],
+ },
+ ]);
+ snapshot = (await getSessionDataFromContent()).get(sessionId);
+ is(snapshot.length, 0, "Session data is now empty");
+
+ info("Add another value before destroy");
+ sessionData.updateSessionData([
+ {
+ method: "add",
+ moduleName: "fakemodule",
+ category: "testCategory",
+ contextDescriptor: contextDescriptorAll,
+ values: ["value-2"],
+ },
+ ]);
+ snapshot = (await getSessionDataFromContent()).get(sessionId);
+ is(snapshot.length, 1);
+ checkSessionDataItem(
+ snapshot[0],
+ "fakemodule",
+ "testCategory",
+ ContextDescriptorType.All,
+ "value-2"
+ );
+
+ sessionData.destroy();
+ sessionDataSnapshot = await getSessionDataFromContent();
+ is(sessionDataSnapshot.size, 0, "session data should be empty again");
+});
+
+add_task(async function test_sessionDataRootOnlyModule() {
+ const sessionId = "sessionData-test-rootOnly";
+
+ const rootMessageHandler = createRootMessageHandler(sessionId);
+ ok(rootMessageHandler, "Valid ROOT MessageHandler created");
+
+ await BrowserTestUtils.loadURI(
+ gBrowser,
+ "https://example.com/document-builder.sjs?html=tab"
+ );
+
+ const windowGlobalCreated = rootMessageHandler.once("message-handler-event");
+
+ info("Test that adding SessionData items works the root module");
+ // Updating the session data on the root message handler should not cause
+ // failures for other message handlers if the module only exists for root.
+ await rootMessageHandler.addSessionData({
+ moduleName: "rootOnly",
+ category: "session_data_root_only",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ values: [true],
+ });
+
+ await windowGlobalCreated;
+ ok(true, "Window global has been initialized");
+
+ let sessionDataReceivedByRoot = await rootMessageHandler.handleCommand({
+ moduleName: "rootOnly",
+ commandName: "getSessionDataReceived",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ });
+
+ is(sessionDataReceivedByRoot.length, 1);
+ is(sessionDataReceivedByRoot[0].category, "session_data_root_only");
+ is(sessionDataReceivedByRoot[0].added.length, 1);
+ is(sessionDataReceivedByRoot[0].added[0], true);
+ is(
+ sessionDataReceivedByRoot[0].contextDescriptor.type,
+ ContextDescriptorType.All
+ );
+
+ info("Now test that removing items also works on the root module");
+ await rootMessageHandler.removeSessionData({
+ moduleName: "rootOnly",
+ category: "session_data_root_only",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ values: [true],
+ });
+
+ sessionDataReceivedByRoot = await rootMessageHandler.handleCommand({
+ moduleName: "rootOnly",
+ commandName: "getSessionDataReceived",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ });
+
+ is(sessionDataReceivedByRoot.length, 2);
+ is(sessionDataReceivedByRoot[1].category, "session_data_root_only");
+ is(sessionDataReceivedByRoot[1].removed.length, 1);
+ is(sessionDataReceivedByRoot[1].removed[0], true);
+ is(
+ sessionDataReceivedByRoot[1].contextDescriptor.type,
+ ContextDescriptorType.All
+ );
+
+ rootMessageHandler.destroy();
+});
+
+function checkSessionDataItem(item, moduleName, category, contextType, value) {
+ is(item.moduleName, moduleName, "Data item has the expected module name");
+ is(item.category, category, "Data item has the expected category");
+ is(
+ item.contextDescriptor.type,
+ contextType,
+ "Data item has the expected context type"
+ );
+ is(item.value, value, "Data item has the expected value");
+}
+
+function getSessionDataFromContent() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const { readSessionData } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/sessiondata/SessionDataReader.sys.mjs"
+ );
+ return readSessionData();
+ });
+}
diff --git a/remote/shared/messagehandler/test/browser/browser_session_data_broadcast.js b/remote/shared/messagehandler/test/browser/browser_session_data_broadcast.js
new file mode 100644
index 0000000000..8cfafc16a1
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_session_data_broadcast.js
@@ -0,0 +1,335 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { RootMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs"
+);
+
+const TEST_PAGE = "https://example.com/document-builder.sjs?html=tab";
+
+add_task(async function test_session_data_broadcast() {
+ const tab1 = gBrowser.selectedTab;
+ await loadURL(tab1.linkedBrowser, TEST_PAGE);
+ const browsingContext1 = tab1.linkedBrowser.browsingContext;
+
+ const root = createRootMessageHandler("session-id-event");
+
+ info("Add a new session data item, expect one return value");
+ const value1 = await addSessionData(root, ["text-1"]);
+ is(value1.length, 1);
+ is(value1[0].addedData, "text-1");
+ is(value1[0].removedData, "");
+ is(value1[0].sessionData, "text-1");
+ is(value1[0].contextId, browsingContext1.id);
+
+ info("Add two session data items, expect one return value with both items");
+ const value2 = await addSessionData(root, ["text-2", "text-3"]);
+ is(value2.length, 1);
+ is(value2[0].addedData, "text-2, text-3");
+ is(value2[0].removedData, "");
+ is(value2[0].sessionData, "text-1, text-2, text-3");
+ is(value2[0].contextId, browsingContext1.id);
+
+ info("Try to add an existing data item, expect no return value");
+ const value3 = await addSessionData(root, ["text-1"]);
+ is(value3.length, 0);
+
+ info("Add an existing and a new item, expect only the new item to return");
+ const value4 = await addSessionData(root, ["text-1", "text-4"]);
+ is(value4.length, 1);
+ is(value4[0].addedData, "text-4");
+ is(value4[0].removedData, "");
+ is(value4[0].sessionData, "text-1, text-2, text-3, text-4");
+ is(value4[0].contextId, browsingContext1.id);
+
+ info("Remove an item, expect only the new item to return");
+ const value5 = await removeSessionData(root, ["text-3"]);
+ is(value5.length, 1);
+ is(value5[0].addedData, "");
+ is(value5[0].removedData, "text-3");
+ is(value5[0].sessionData, "text-1, text-2, text-4");
+ is(value5[0].contextId, browsingContext1.id);
+
+ info("Open a new tab on the same test URL");
+ const tab2 = await addTab(TEST_PAGE);
+ const browsingContext2 = tab2.linkedBrowser.browsingContext;
+
+ info("Add a new session data item, check both contexts have the same data.");
+ const value6 = await addSessionData(root, ["text-5"]);
+ is(value6.length, 2);
+ is(value6[0].addedData, "text-5");
+ is(value6[0].removedData, "");
+ is(value6[0].sessionData, "text-1, text-2, text-4, text-5");
+ is(value6[0].contextId, browsingContext1.id);
+ is(value6[1].addedData, "text-5");
+ // "text-1, text-2, text-4" were added as initial session data and
+ // "text-5" was added afterwards.
+ is(value6[1].sessionData, "text-1, text-2, text-4, text-5");
+ is(value6[1].contextId, browsingContext2.id);
+
+ info("Remove a missing item, expect no return value");
+ const value7 = await removeSessionData(root, ["text-missing"]);
+ is(value7.length, 0);
+
+ info("Remove an existing and a missing item");
+ const value8 = await removeSessionData(root, ["text-2", "text-missing"]);
+ is(value8.length, 2);
+ is(value8[0].addedData, "");
+ is(value8[0].removedData, "text-2");
+ is(value8[0].sessionData, "text-1, text-4, text-5");
+ is(value8[0].contextId, browsingContext1.id);
+ is(value8[1].addedData, "");
+ is(value8[1].removedData, "text-2");
+ is(value8[1].sessionData, "text-1, text-4, text-5");
+ is(value8[1].contextId, browsingContext2.id);
+
+ info("Add multiple items at once");
+ const value9 = await updateSessionData(root, [
+ {
+ method: "add",
+ values: ["text-6"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ },
+ {
+ method: "add",
+ values: ["text-7"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext2.browserId,
+ },
+ },
+ ]);
+ is(value9.length, 2);
+ is(value9[0].addedData, "text-6");
+ is(value9[0].removedData, "");
+ is(value9[0].sessionData, "text-1, text-4, text-5, text-6");
+ is(value9[0].contextId, browsingContext1.id);
+ is(value9[1].addedData, "text-6, text-7");
+ is(value9[1].removedData, "");
+ is(value9[1].sessionData, "text-1, text-4, text-5, text-6, text-7");
+ is(value9[1].contextId, browsingContext2.id);
+
+ info("Remove multiple items at once");
+ const value10 = await updateSessionData(root, [
+ {
+ method: "remove",
+ values: ["text-5"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ },
+ {
+ method: "remove",
+ values: ["text-7"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext2.browserId,
+ },
+ },
+ ]);
+ is(value10.length, 2);
+ is(value10[0].addedData, "");
+ is(value10[0].removedData, "text-5");
+ is(value10[0].sessionData, "text-1, text-4, text-6");
+ is(value10[0].contextId, browsingContext1.id);
+ is(value10[1].addedData, "");
+ is(value10[1].removedData, "text-5, text-7");
+ is(value10[1].sessionData, "text-1, text-4, text-6");
+ is(value10[1].contextId, browsingContext2.id);
+
+ info("Add and remove at once");
+ const value11 = await updateSessionData(root, [
+ {
+ method: "add",
+ values: ["text-8"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ },
+ {
+ method: "remove",
+ values: ["text-6"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ },
+ ]);
+ is(value11.length, 2);
+ is(value11[0].addedData, "text-8");
+ is(value11[0].removedData, "text-6");
+ is(value11[0].sessionData, "text-1, text-4, text-8");
+ is(value11[0].contextId, browsingContext1.id);
+ is(value11[1].addedData, "text-8");
+ is(value11[1].removedData, "text-6");
+ is(value11[1].sessionData, "text-1, text-4, text-8");
+ is(value11[1].contextId, browsingContext2.id);
+
+ info(
+ "Add session data item to all contexts and remove this event for one context"
+ );
+ // Add first an event for one context.
+ const value12 = await updateSessionData(root, [
+ {
+ method: "add",
+ values: ["text-9"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext1.browserId,
+ },
+ },
+ ]);
+ is(value12.length, 1);
+ is(value12[0].addedData, "text-9");
+ is(value12[0].removedData, "");
+ is(value12[0].sessionData, "text-1, text-4, text-8, text-9");
+ is(value12[0].contextId, browsingContext1.id);
+
+ // Remove the item for one context and add the item for all contexts.
+ const value13 = await updateSessionData(root, [
+ {
+ method: "remove",
+ values: ["text-9"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext1.browserId,
+ },
+ },
+ {
+ method: "add",
+ values: ["text-9"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ },
+ ]);
+ is(value13.length, 2);
+ is(value13[0].addedData, "");
+ // Make sure that nothing is removed.
+ is(value13[0].removedData, "");
+ is(value13[0].sessionData, "text-1, text-4, text-8, text-9");
+ is(value13[0].contextId, browsingContext1.id);
+ is(value13[1].addedData, "text-9");
+ is(value13[1].removedData, "");
+ is(value13[1].sessionData, "text-1, text-4, text-8, text-9");
+ is(value13[1].contextId, browsingContext2.id);
+
+ info(
+ "Remove the event, which has also an individual subscription, for all contexts."
+ );
+ const value14 = await updateSessionData(root, [
+ {
+ method: "add",
+ values: ["text-10"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ },
+ {
+ method: "add",
+ values: ["text-10"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext1.browserId,
+ },
+ },
+ ]);
+ is(value14.length, 2);
+ is(value14[0].addedData, "text-10");
+ is(value14[0].removedData, "");
+ is(value14[0].sessionData, "text-1, text-4, text-8, text-9, text-10");
+ is(value14[0].contextId, browsingContext1.id);
+ is(value14[1].addedData, "text-10");
+ is(value14[1].removedData, "");
+ is(value14[1].sessionData, "text-1, text-4, text-8, text-9, text-10");
+ is(value14[1].contextId, browsingContext2.id);
+
+ const value15 = await updateSessionData(root, [
+ {
+ method: "remove",
+ values: ["text-10"],
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ },
+ ]);
+
+ is(value15.length, 2);
+ is(value15[0].addedData, "");
+ // Make sure that nothing is removed for the first context
+ is(value15[0].removedData, "");
+ is(value15[0].sessionData, "text-1, text-4, text-8, text-9, text-10");
+ is(value15[0].contextId, browsingContext1.id);
+ is(value15[1].addedData, "");
+ is(value15[1].removedData, "text-10");
+ is(value15[1].sessionData, "text-1, text-4, text-8, text-9");
+ is(value15[1].contextId, browsingContext2.id);
+
+ root.destroy();
+
+ gBrowser.removeTab(tab2);
+});
+
+function addSessionData(rootMessageHandler, values) {
+ return rootMessageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "testAddSessionData",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ params: {
+ values,
+ },
+ });
+}
+
+function removeSessionData(rootMessageHandler, values) {
+ return rootMessageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "testRemoveSessionData",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ params: {
+ values,
+ },
+ });
+}
+
+function updateSessionData(rootMessageHandler, params) {
+ return rootMessageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "testUpdateSessionData",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ params,
+ });
+}
diff --git a/remote/shared/messagehandler/test/browser/browser_session_data_browser_element.js b/remote/shared/messagehandler/test/browser/browser_session_data_browser_element.js
new file mode 100644
index 0000000000..62c51b29a8
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_session_data_browser_element.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE = "https://example.com/document-builder.sjs?html=tab";
+
+/**
+ * Check that message handlers are not created for parent process browser
+ * elements, even if they have the type="content" attribute (eg used for the
+ * DevTools toolbox), as well as for webextension contexts.
+ */
+add_task(async function test_session_data_broadcast() {
+ // Prepare:
+ // - one content tab
+ // - one browser type content
+ // - one browser type chrome
+ // - one sidebar webextension
+ // We only expect session data to be applied to the content tab
+ const tab1 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
+ const contentBrowser1 = tab1.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(contentBrowser1);
+ const parentBrowser1 = createParentBrowserElement(tab1, "content");
+ const parentBrowser2 = createParentBrowserElement(tab1, "chrome");
+ const {
+ extension: extension1,
+ sidebarBrowser: extSidebarBrowser1,
+ } = await installSidebarExtension();
+
+ const root = createRootMessageHandler("session-id-event");
+
+ // When the windowglobal command.jsm module applies the session data
+ // browser_session_data_browser_element, it will emit an event.
+ // Collect the events to detect which MessageHandlers have been started.
+ info("Watch events emitted when session data is applied");
+ const sessionDataEvents = [];
+ const onRootEvent = function(evtName, wrappedEvt) {
+ if (wrappedEvt.name === "received-session-data") {
+ sessionDataEvents.push(wrappedEvt.data.contextId);
+ }
+ };
+ root.on("message-handler-event", onRootEvent);
+
+ info("Add a new session data item, expect one return value");
+ await root.addSessionData({
+ moduleName: "command",
+ category: "browser_session_data_browser_element",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ values: [true],
+ });
+
+ function hasSessionData(browsingContext) {
+ return sessionDataEvents.includes(browsingContext.id);
+ }
+
+ info(
+ "Check that only the content tab window global received the session data"
+ );
+ is(hasSessionData(contentBrowser1.browsingContext), true);
+ is(hasSessionData(parentBrowser1.browsingContext), false);
+ is(hasSessionData(parentBrowser2.browsingContext), false);
+ is(hasSessionData(extSidebarBrowser1.browsingContext), false);
+
+ const tab2 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
+ const contentBrowser2 = tab2.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(contentBrowser2);
+ const parentBrowser3 = createParentBrowserElement(contentBrowser2, "content");
+ const parentBrowser4 = createParentBrowserElement(contentBrowser2, "chrome");
+
+ const {
+ extension: extension2,
+ sidebarBrowser: extSidebarBrowser2,
+ } = await installSidebarExtension();
+
+ info("Wait until the session data was applied to the new tab");
+ await TestUtils.waitForCondition(() =>
+ sessionDataEvents.includes(contentBrowser2.browsingContext.id)
+ );
+
+ info("Check that parent browser elements did not apply the session data");
+ is(hasSessionData(parentBrowser3.browsingContext), false);
+ is(hasSessionData(parentBrowser4.browsingContext), false);
+
+ info(
+ "Check that extension did not apply the session data, " +
+ extSidebarBrowser2.browsingContext.id
+ );
+ is(hasSessionData(extSidebarBrowser2.browsingContext), false);
+
+ root.destroy();
+
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ await extension1.unload();
+ await extension2.unload();
+});
diff --git a/remote/shared/messagehandler/test/browser/browser_session_data_constructor_race.js b/remote/shared/messagehandler/test/browser/browser_session_data_constructor_race.js
new file mode 100644
index 0000000000..e71c638ca3
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_session_data_constructor_race.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { WindowGlobalMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs"
+);
+
+const TEST_PAGE = "https://example.com/document-builder.sjs?html=tab";
+
+/**
+ * Check that modules created early for session data are still created with a
+ * fully initialized MessageHandler. See Bug 1743083.
+ */
+add_task(async function() {
+ const tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ const browsingContext = tab.linkedBrowser.browsingContext;
+
+ const root = createRootMessageHandler("session-id-event");
+
+ info("Add some session data for the command module");
+ await root.addSessionData({
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: contextDescriptorAll,
+ values: ["some-value"],
+ });
+
+ info("Reload the current tab to create new message handlers and modules");
+ await BrowserTestUtils.reloadTab(tab);
+
+ info(
+ "Check if the command module was created by the MessageHandler constructor"
+ );
+ const isCreatedByMessageHandlerConstructor = await root.handleCommand({
+ moduleName: "command",
+ commandName: "testIsCreatedByMessageHandlerConstructor",
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContext.id,
+ },
+ });
+
+ is(
+ isCreatedByMessageHandlerConstructor,
+ false,
+ "The command module from session data should not be created by the MessageHandler constructor"
+ );
+ root.destroy();
+
+ gBrowser.removeTab(tab);
+});
diff --git a/remote/shared/messagehandler/test/browser/head.js b/remote/shared/messagehandler/test/browser/head.js
new file mode 100644
index 0000000000..825db7afef
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/head.js
@@ -0,0 +1,186 @@
+/* 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";
+
+var { ContextDescriptorType } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs"
+);
+
+var contextDescriptorAll = {
+ type: ContextDescriptorType.All,
+};
+
+function createRootMessageHandler(sessionId) {
+ const { RootMessageHandlerRegistry } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs"
+ );
+ return RootMessageHandlerRegistry.getOrCreateMessageHandler(sessionId);
+}
+
+/**
+ * Load the provided url in an existing browser.
+ * Returns a promise which will resolve when the page is loaded.
+ *
+ * @param {Browser} browser
+ * The browser element where the URL should be loaded.
+ * @param {String} url
+ * The URL to load in the new tab
+ */
+async function loadURL(browser, url) {
+ const loaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURI(browser, url);
+ return loaded;
+}
+
+/**
+ * Create a new foreground tab loading the provided url.
+ * Returns a promise which will resolve when the page is loaded.
+ *
+ * @param {String} url
+ * The URL to load in the new tab
+ */
+async function addTab(url) {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ registerCleanupFunction(() => {
+ gBrowser.removeTab(tab);
+ });
+ return tab;
+}
+
+/**
+ * Create inline markup for a simple iframe that can be used with
+ * document-builder.sjs. The iframe will be served under the provided domain.
+ *
+ * @param {String} domain
+ * A domain (eg "example.com"), compatible with build/pgo/server-locations.txt
+ */
+function createFrame(domain) {
+ return createFrameForUri(
+ `https://${domain}/document-builder.sjs?html=frame-${domain}`
+ );
+}
+
+function createFrameForUri(uri) {
+ return `<iframe src="${encodeURI(uri)}"></iframe>`;
+}
+
+/**
+ * Create a XUL browser element in the provided XUL tab, with the provided type.
+ *
+ * @param {xul:tab} tab
+ * The XUL tab in which the browser element should be inserted.
+ * @param {String} type
+ * The type attribute of the browser element, "chrome" or "content".
+ * @return {xul:browser}
+ * The created browser element.
+ */
+function createParentBrowserElement(tab, type) {
+ const parentBrowser = gBrowser.ownerDocument.createXULElement("browser");
+ parentBrowser.setAttribute("type", type);
+ const container = gBrowser.getBrowserContainer(tab.linkedBrowser);
+ container.appendChild(parentBrowser);
+
+ return parentBrowser;
+}
+
+// Create a test page with 2 iframes:
+// - one with a different eTLD+1 (example.com)
+// - one with a nested iframe on a different eTLD+1 (example.net)
+//
+// Overall the document structure should look like:
+//
+// html (example.org)
+// iframe (example.org)
+// iframe (example.net)
+// iframe(example.com)
+//
+// Which means we should have 4 browsing contexts in total.
+function createTestMarkupWithFrames() {
+ // Create the markup for an example.net frame nested in an example.com frame.
+ const NESTED_FRAME_MARKUP = createFrameForUri(
+ `https://example.org/document-builder.sjs?html=${createFrame(
+ "example.net"
+ )}`
+ );
+
+ // Combine the nested frame markup created above with an example.com frame.
+ const TEST_URI_MARKUP = `${NESTED_FRAME_MARKUP}${createFrame("example.com")}`;
+
+ // Create the test page URI on example.org.
+ return `https://example.org/document-builder.sjs?html=${encodeURI(
+ TEST_URI_MARKUP
+ )}`;
+}
+
+const hasPromiseResolved = async function(promise) {
+ let resolved = false;
+ promise.finally(() => (resolved = true));
+ // Make sure microtasks have time to run.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+ return resolved;
+};
+
+/**
+ * Install a sidebar extension.
+ *
+ * @return {Object}
+ * Return value with two properties:
+ * - extension: test wrapper as returned by SpecialPowers.loadExtension.
+ * Make sure to explicitly call extension.unload() before the end of the test.
+ * - sidebarBrowser: the browser element containing the extension sidebar.
+ */
+async function installSidebarExtension() {
+ info("Load the test extension");
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+ <!DOCTYPE html>
+ <html>
+ Test extension
+ <script src="sidebar.js"></script>
+ </html>
+ `,
+ "sidebar.js": function() {
+ const { browser } = this;
+ browser.test.sendMessage("sidebar-loaded", {
+ bcId: SpecialPowers.wrap(window).browsingContext.id,
+ });
+ },
+ "tab.html": `
+ <!DOCTYPE html>
+ <html>
+ Test extension (tab)
+ <script src="tab.js"></script>
+ </html>
+ `,
+ "tab.js": function() {
+ const { browser } = this;
+ browser.test.sendMessage("tab-loaded", {
+ bcId: SpecialPowers.wrap(window).browsingContext.id,
+ });
+ },
+ },
+ });
+
+ info("Wait for the extension to start");
+ await extension.startup();
+
+ info("Wait for the extension browsing context");
+ const { bcId } = await extension.awaitMessage("sidebar-loaded");
+ const sidebarBrowser = BrowsingContext.get(bcId).top.embedderElement;
+ ok(sidebarBrowser, "Got a browser element for the extension sidebar");
+
+ return {
+ extension,
+ sidebarBrowser,
+ };
+}
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/ModuleRegistry.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/ModuleRegistry.sys.mjs
new file mode 100644
index 0000000000..e5d9c38719
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/ModuleRegistry.sys.mjs
@@ -0,0 +1,30 @@
+/* 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/. */
+
+/**
+ * Retrieve the WebDriver BiDi module class matching the provided module name
+ * and folder.
+ *
+ * @param {String} moduleName
+ * The name of the module to get the class for.
+ * @param {String} moduleFolder
+ * A valid folder name for modules.
+ * @return {Class=}
+ * The class corresponding to the module name and folder, null if no match
+ * was found.
+ * @throws {Error}
+ * If the provided module folder is unexpected.
+ **/
+export const getModuleClass = function(moduleName, moduleFolder) {
+ const root = `chrome://mochitests/content/browser/remote/shared/messagehandler/test/`;
+ const path = `${root}browser/resources/modules/${moduleFolder}/${moduleName}.sys.mjs`;
+ try {
+ return ChromeUtils.importESModule(path)[moduleName];
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
+ return null;
+ }
+ throw e;
+ }
+};
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/root/command.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/root/command.sys.mjs
new file mode 100644
index 0000000000..a67428ddfa
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/root/command.sys.mjs
@@ -0,0 +1,80 @@
+/* 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/. */
+
+import { ContextDescriptorType } from "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs";
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+import { WindowGlobalMessageHandler } from "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs";
+
+class CommandModule extends Module {
+ destroy() {}
+
+ async #getSessionDataUpdateFromAllContexts() {
+ const updates = await this.messageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "_getSessionDataUpdate",
+ destination: {
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ type: WindowGlobalMessageHandler.type,
+ },
+ });
+
+ // Filter out null values, which indicate that no new session data was
+ // received by the windowglobal module since the last getSessionDataUpdate
+ // command.
+ return updates.filter(update => update != null);
+ }
+
+ /**
+ * Commands
+ */
+
+ async testAddSessionData(params) {
+ await this.messageHandler.addSessionData({
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ values: params.values,
+ });
+
+ return this.#getSessionDataUpdateFromAllContexts();
+ }
+
+ async testRemoveSessionData(params) {
+ await this.messageHandler.removeSessionData({
+ moduleName: "command",
+ category: "testCategory",
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ values: params.values,
+ });
+
+ return this.#getSessionDataUpdateFromAllContexts();
+ }
+
+ async testUpdateSessionData(params) {
+ await this.messageHandler.updateSessionData(params);
+ return this.#getSessionDataUpdateFromAllContexts();
+ }
+
+ testRootModule() {
+ return "root-value";
+ }
+
+ testMissingIntermediaryMethod(params, destination) {
+ // Spawn a new internal command, but with a commandName which doesn't match
+ // any method.
+ return this.messageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "missingMethod",
+ destination,
+ });
+ }
+}
+
+export const command = CommandModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/root/event.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/root/event.sys.mjs
new file mode 100644
index 0000000000..e49437e80d
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/root/event.sys.mjs
@@ -0,0 +1,21 @@
+/* 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/. */
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+class EventModule extends Module {
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ testEmitRootEvent() {
+ this.emitEvent("event-from-root", {
+ text: "event from root",
+ });
+ }
+}
+
+export const event = EventModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/root/invalid.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/root/invalid.sys.mjs
new file mode 100644
index 0000000000..3b74769d06
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/root/invalid.sys.mjs
@@ -0,0 +1,4 @@
+// This module is meant to check error reporting when importing a module fails
+// due to an actual issue (syntax error etc...).
+
+SyntaxError(;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/root/rootOnly.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/root/rootOnly.sys.mjs
new file mode 100644
index 0000000000..0931a7ee8e
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/root/rootOnly.sys.mjs
@@ -0,0 +1,70 @@
+/* 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/. */
+
+import { ContextDescriptorType } from "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs";
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+class RootOnlyModule extends Module {
+ #sessionDataReceived;
+ #subscribedEvents;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+
+ this.#sessionDataReceived = [];
+ this.#subscribedEvents = new Set();
+ }
+
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ getSessionDataReceived() {
+ return this.#sessionDataReceived;
+ }
+
+ testCommand(params = {}) {
+ return params;
+ }
+
+ _applySessionData(params) {
+ const added = [];
+ const removed = [];
+
+ const filteredSessionData = params.sessionData.filter(item =>
+ this.messageHandler.matchesContext(item.contextDescriptor)
+ );
+ for (const event of this.#subscribedEvents.values()) {
+ const hasSessionItem = filteredSessionData.some(
+ item => item.value === event
+ );
+ // If there are no session items for this context, we should unsubscribe from the event.
+ if (!hasSessionItem) {
+ this.#subscribedEvents.delete(event);
+ removed.push(event);
+ }
+ }
+
+ // Subscribe to all events, which have an item in SessionData
+ for (const { value } of filteredSessionData) {
+ if (!this.#subscribedEvents.has(value)) {
+ this.#subscribedEvents.add(value);
+ added.push(value);
+ }
+ }
+
+ this.#sessionDataReceived.push({
+ category: params.category,
+ added,
+ removed,
+ contextDescriptor: {
+ type: ContextDescriptorType.All,
+ },
+ });
+ }
+}
+
+export const rootOnly = RootOnlyModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/windowglobal-in-root/command.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal-in-root/command.sys.mjs
new file mode 100644
index 0000000000..f9a2e5d4eb
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal-in-root/command.sys.mjs
@@ -0,0 +1,28 @@
+/* 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/. */
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+class CommandModule extends Module {
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ testInterceptModule() {
+ return "intercepted-value";
+ }
+
+ async testInterceptAndForwardModule(params, destination) {
+ const windowGlobalValue = await this.messageHandler.handleCommand({
+ moduleName: "command",
+ commandName: "testForwardToWindowGlobal",
+ destination,
+ });
+ return "intercepted-and-forward+" + windowGlobalValue;
+ }
+}
+
+export const command = CommandModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/windowglobal-in-root/event.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal-in-root/event.sys.mjs
new file mode 100644
index 0000000000..2969e0a3e8
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal-in-root/event.sys.mjs
@@ -0,0 +1,31 @@
+/* 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/. */
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+class EventModule extends Module {
+ destroy() {}
+
+ interceptEvent(name, payload) {
+ if (name === "event.testEventWithInterception") {
+ return {
+ ...payload,
+ additionalInformation: "information added through interception",
+ };
+ }
+ return payload;
+ }
+
+ /**
+ * Commands
+ */
+
+ testEmitWindowGlobalInRootEvent(params, destination) {
+ this.emitEvent("event-from-window-global-in-root", {
+ text: `windowglobal-in-root event for ${destination.id}`,
+ });
+ }
+}
+
+export const event = EventModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/command.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/command.sys.mjs
new file mode 100644
index 0000000000..23655a464a
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/command.sys.mjs
@@ -0,0 +1,114 @@
+/* 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/. */
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+class CommandModule extends Module {
+ constructor(messageHandler) {
+ super(messageHandler);
+ this._lastSessionDataUpdate = {};
+ this._subscribedEvents = new Set();
+ this._testCategorySessionData = [];
+
+ this._createdByMessageHandlerConstructor = this._isCreatedByMessageHandlerConstructor();
+ }
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ _applySessionData(params) {
+ if (params.category === "testCategory") {
+ const added = [];
+ const removed = [];
+
+ const filteredSessionData = params.sessionData.filter(item =>
+ this.messageHandler.matchesContext(item.contextDescriptor)
+ );
+ for (const event of this._subscribedEvents.values()) {
+ const hasSessionItem = filteredSessionData.some(
+ item => item.value === event
+ );
+ // If there are no session items for this context, we should unsubscribe from the event.
+ if (!hasSessionItem) {
+ this._subscribedEvents.delete(event);
+ removed.push(event);
+ }
+ }
+
+ // Subscribe to all events, which have an item in SessionData
+ for (const { value } of filteredSessionData) {
+ if (!this._subscribedEvents.has(value)) {
+ this._subscribedEvents.add(value);
+ added.push(value);
+ }
+ }
+
+ this._testCategorySessionData = this._testCategorySessionData
+ .concat(added)
+ .filter(value => !removed.includes(value));
+
+ this._lastSessionDataUpdate = {
+ addedData: added.join(", "),
+ removedData: removed.join(", "),
+ sessionData: this._testCategorySessionData.join(", "),
+ contextId: this.messageHandler.contextId,
+ };
+ }
+
+ if (params.category === "browser_session_data_browser_element") {
+ this.emitEvent("received-session-data", {
+ contextId: this.messageHandler.contextId,
+ });
+ }
+
+ return {};
+ }
+
+ _getSessionDataUpdate(params) {
+ const lastUpdate = this._lastSessionDataUpdate;
+
+ // Each "lastUpdate" should only be returned once, so that the caller can
+ // assert when a SessionData update had no impact.
+ this._lastSessionDataUpdate = null;
+
+ return lastUpdate;
+ }
+
+ testWindowGlobalModule() {
+ return "windowglobal-value";
+ }
+
+ testSetValue(params) {
+ const { value } = params;
+
+ this._testValue = value;
+ }
+
+ testGetValue() {
+ return this._testValue;
+ }
+
+ testForwardToWindowGlobal() {
+ return "forward-to-windowglobal-value";
+ }
+
+ testIsCreatedByMessageHandlerConstructor() {
+ return this._createdByMessageHandlerConstructor;
+ }
+
+ _isCreatedByMessageHandlerConstructor() {
+ let caller = Components.stack.caller;
+ while (caller) {
+ if (caller.name === this.messageHandler.constructor.name) {
+ return true;
+ }
+ caller = caller.caller;
+ }
+ return false;
+ }
+}
+
+export const command = CommandModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/commandwindowglobalonly.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/commandwindowglobalonly.sys.mjs
new file mode 100644
index 0000000000..1e4e6c1574
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/commandwindowglobalonly.sys.mjs
@@ -0,0 +1,41 @@
+/* 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/. */
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+class CommandWindowGlobalOnlyModule extends Module {
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ testOnlyInWindowGlobal() {
+ return "only-in-windowglobal";
+ }
+
+ testBroadcast() {
+ return `broadcast-${this.messageHandler.contextId}`;
+ }
+
+ testBroadcastWithParameter(params) {
+ return `broadcast-${this.messageHandler.contextId}-${params.value}`;
+ }
+
+ testError() {
+ throw new Error("error-from-module");
+ }
+
+ testMissingIntermediaryMethod(params, destination) {
+ // Spawn a new internal command, but with a commandName which doesn't match
+ // any method.
+ return this.messageHandler.handleCommand({
+ moduleName: "commandwindowglobalonly",
+ commandName: "missingMethod",
+ destination,
+ });
+ }
+}
+
+export const commandwindowglobalonly = CommandWindowGlobalOnlyModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/event.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/event.sys.mjs
new file mode 100644
index 0000000000..5bb50cb83d
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/event.sys.mjs
@@ -0,0 +1,26 @@
+/* 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/. */
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+class EventModule extends Module {
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ testEmitEvent() {
+ // Emit a payload including the contextId to check which context emitted
+ // a specific event.
+ const text = `event from ${this.messageHandler.contextId}`;
+ this.emitEvent("event-from-window-global", { text });
+ }
+
+ testEmitEventWithInterception() {
+ this.emitEvent("event.testEventWithInterception", {});
+ }
+}
+
+export const event = EventModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/eventemitter.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/eventemitter.sys.mjs
new file mode 100644
index 0000000000..c86954c5e0
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/eventemitter.sys.mjs
@@ -0,0 +1,81 @@
+/* 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/. */
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+class EventEmitterModule extends Module {
+ #isSubscribed;
+ #subscribedEvents;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+ this.#isSubscribed = false;
+ this.#subscribedEvents = new Set();
+ }
+
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ emitTestEvent() {
+ if (this.#isSubscribed) {
+ const text = `event from ${this.messageHandler.contextId}`;
+ this.emitEvent("eventemitter.testEvent", { text });
+ }
+
+ // Emit another event consistently for monitoring during the test.
+ this.emitEvent("eventemitter.monitoringEvent", {});
+ }
+
+ isSubscribed() {
+ return this.#isSubscribed;
+ }
+
+ _applySessionData(params) {
+ const { category } = params;
+ if (category === "event") {
+ const filteredSessionData = params.sessionData.filter(item =>
+ this.messageHandler.matchesContext(item.contextDescriptor)
+ );
+ for (const event of this.#subscribedEvents.values()) {
+ const hasSessionItem = filteredSessionData.some(
+ item => item.value === event
+ );
+ // If there are no session items for this context, we should unsubscribe from the event.
+ if (!hasSessionItem) {
+ this.#unsubscribeEvent(event);
+ }
+ }
+
+ // Subscribe to all events, which have an item in SessionData
+ for (const { value } of filteredSessionData) {
+ this.#subscribeEvent(value);
+ }
+ }
+ }
+
+ #subscribeEvent(event) {
+ if (event === "eventemitter.testEvent") {
+ if (this.#isSubscribed) {
+ throw new Error("Already subscribed to eventemitter.testEvent");
+ }
+ this.#isSubscribed = true;
+ this.#subscribedEvents.add(event);
+ }
+ }
+
+ #unsubscribeEvent(event) {
+ if (event === "eventemitter.testEvent") {
+ if (!this.#isSubscribed) {
+ throw new Error("Not subscribed to eventemitter.testEvent");
+ }
+ this.#isSubscribed = false;
+ this.#subscribedEvents.delete(event);
+ }
+ }
+}
+
+export const eventemitter = EventEmitterModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/eventnointercept.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/eventnointercept.sys.mjs
new file mode 100644
index 0000000000..48bbfbf951
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/eventnointercept.sys.mjs
@@ -0,0 +1,16 @@
+/* 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/. */
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+class EventNoInterceptModule extends Module {
+ destroy() {}
+
+ testEvent() {
+ const text = `event no interception`;
+ this.emitEvent("eventnointercept.testEvent", { text });
+ }
+}
+
+export const eventnointercept = EventNoInterceptModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/retry.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/retry.sys.mjs
new file mode 100644
index 0000000000..f7b2279018
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/retry.sys.mjs
@@ -0,0 +1,84 @@
+/* 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/. */
+
+import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+// Store counters in the JSM scope to persist them across reloads.
+let callsToBlockedOneTime = 0;
+let callsToBlockedTenTimes = 0;
+let callsToBlockedElevenTimes = 0;
+
+// This module provides various commands which all hang for various reasons.
+// The test is supposed to trigger the command and then destroy the
+// JSWindowActor pair by any mean (eg a navigation) in order to trigger an
+// AbortError and a retry.
+class RetryModule extends Module {
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ // Resolves only if called while on the example.net domain.
+ async blockedOnNetDomain(params) {
+ // Note: we do not store a call counter here, because this is used for a
+ // cross-group navigation test, and the JSM will be loaded in different
+ // processes.
+ const uri = this.messageHandler.window.document.baseURI;
+ if (!uri.includes("example.net")) {
+ await new Promise(r => {});
+ }
+
+ return { ...params };
+ }
+
+ // Resolves only if called more than once.
+ async blockedOneTime(params) {
+ callsToBlockedOneTime++;
+ if (callsToBlockedOneTime < 2) {
+ await new Promise(r => {});
+ }
+
+ // Return:
+ // - params sent to the command to check that retries have correct params
+ // - the call counter
+ return { ...params, callsToCommand: callsToBlockedOneTime };
+ }
+
+ // Resolves only if called more than ten times (which is exactly the maximum
+ // of retry attempts).
+ async blockedTenTimes(params) {
+ callsToBlockedTenTimes++;
+ if (callsToBlockedTenTimes < 11) {
+ await new Promise(r => {});
+ }
+
+ // Return:
+ // - params sent to the command to check that retries have correct params
+ // - the call counter
+ return { ...params, callsToCommand: callsToBlockedTenTimes };
+ }
+
+ // Resolves only if called more than eleven times (which is greater than the
+ // maximum of retry attempts).
+ async blockedElevenTimes(params) {
+ callsToBlockedElevenTimes++;
+ if (callsToBlockedElevenTimes < 12) {
+ await new Promise(r => {});
+ }
+
+ // Return:
+ // - params sent to the command to check that retries have correct params
+ // - the call counter
+ return { ...params, callsToCommand: callsToBlockedElevenTimes };
+ }
+
+ cleanup() {
+ callsToBlockedOneTime = 0;
+ callsToBlockedTenTimes = 0;
+ callsToBlockedElevenTimes = 0;
+ }
+}
+
+export const retry = RetryModule;
diff --git a/remote/shared/messagehandler/test/browser/webdriver/browser.ini b/remote/shared/messagehandler/test/browser/webdriver/browser.ini
new file mode 100644
index 0000000000..a7ece66163
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/webdriver/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = remote
+subsuite = remote
+support-files =
+ !/remote/shared/messagehandler/test/browser/resources/*
+prefs =
+ remote.messagehandler.modulecache.useBrowserTestRoot=true
+
+[browser_session_execute_command_errors.js]
diff --git a/remote/shared/messagehandler/test/browser/webdriver/browser_session_execute_command_errors.js b/remote/shared/messagehandler/test/browser/webdriver/browser_session_execute_command_errors.js
new file mode 100644
index 0000000000..36a510bb29
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/webdriver/browser_session_execute_command_errors.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { WebDriverSession } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Session.sys.mjs"
+);
+
+const { error } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Errors.sys.mjs"
+);
+
+add_task(async function test_execute_missing_command_error() {
+ const session = new WebDriverSession();
+
+ info("Attempt to execute an unknown protocol command");
+ await Assert.rejects(
+ session.execute("command", "missingCommand"),
+ err =>
+ err.name == "UnknownCommandError" &&
+ err.message == `command.missingCommand`
+ );
+});
+
+add_task(async function test_execute_missing_internal_command_error() {
+ const session = new WebDriverSession();
+
+ info(
+ "Attempt to execute a protocol command which relies on an unknown internal method"
+ );
+ await Assert.rejects(
+ session.execute("command", "testMissingIntermediaryMethod"),
+ err =>
+ err.name == "UnsupportedCommandError" &&
+ err.message ==
+ `command.missingMethod not supported for destination ROOT` &&
+ !error.isWebDriverError(err)
+ );
+});
diff --git a/remote/shared/messagehandler/test/xpcshell/test_Errors.js b/remote/shared/messagehandler/test/xpcshell/test_Errors.js
new file mode 100644
index 0000000000..33cfb5cfd7
--- /dev/null
+++ b/remote/shared/messagehandler/test/xpcshell/test_Errors.js
@@ -0,0 +1,99 @@
+/* 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/. */
+
+const { error } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/Errors.sys.mjs"
+);
+
+// Note: this test file is similar to remote/shared/webdriver/test/xpcshell/test_Errors.js
+// because shared/webdriver/Errors.jsm and shared/messagehandler/Errors.jsm share
+// similar helpers.
+
+add_test(function test_toJSON() {
+ let e0 = new error.MessageHandlerError();
+ let e0s = e0.toJSON();
+ equal(e0s.error, "message handler error");
+ equal(e0s.message, "");
+
+ let e1 = new error.MessageHandlerError("a");
+ let e1s = e1.toJSON();
+ equal(e1s.message, e1.message);
+
+ let e2 = new error.UnsupportedCommandError("foo");
+ let e2s = e2.toJSON();
+ equal(e2.status, e2s.error);
+ equal(e2.message, e2s.message);
+
+ run_next_test();
+});
+
+add_test(function test_fromJSON() {
+ Assert.throws(
+ () => error.MessageHandlerError.fromJSON({ error: "foo" }),
+ /Not of MessageHandlerError descent/
+ );
+ Assert.throws(
+ () => error.MessageHandlerError.fromJSON({ error: "Error" }),
+ /Not of MessageHandlerError descent/
+ );
+ Assert.throws(
+ () => error.MessageHandlerError.fromJSON({}),
+ /Undeserialisable error type/
+ );
+ Assert.throws(
+ () => error.MessageHandlerError.fromJSON(undefined),
+ /TypeError/
+ );
+
+ let e1 = new error.MessageHandlerError("1");
+ let e1r = error.MessageHandlerError.fromJSON({
+ error: "message handler error",
+ message: "1",
+ });
+ ok(e1r instanceof error.MessageHandlerError);
+ equal(e1r.name, e1.name);
+ equal(e1r.status, e1.status);
+ equal(e1r.message, e1.message);
+
+ let e2 = new error.UnsupportedCommandError("foo");
+ let e2r = error.MessageHandlerError.fromJSON({
+ error: "unsupported message handler command",
+ message: "foo",
+ });
+ ok(e2r instanceof error.MessageHandlerError);
+ ok(e2r instanceof error.UnsupportedCommandError);
+ equal(e2r.name, e2.name);
+ equal(e2r.status, e2.status);
+ equal(e2r.message, e2.message);
+
+ // parity with toJSON
+ let e3 = new error.UnsupportedCommandError("foo");
+ let e3toJSON = e3.toJSON();
+ let e3fromJSON = error.MessageHandlerError.fromJSON(e3toJSON);
+ equal(e3toJSON.error, e3fromJSON.status);
+ equal(e3toJSON.message, e3fromJSON.message);
+ equal(e3toJSON.stacktrace, e3fromJSON.stack);
+
+ run_next_test();
+});
+
+add_test(function test_MessageHandlerError() {
+ let err = new error.MessageHandlerError("foo");
+ equal("MessageHandlerError", err.name);
+ equal("foo", err.message);
+ equal("message handler error", err.status);
+ ok(err instanceof error.MessageHandlerError);
+
+ run_next_test();
+});
+
+add_test(function test_UnsupportedCommandError() {
+ let e = new error.UnsupportedCommandError("foo");
+ equal("UnsupportedCommandError", e.name);
+ equal("foo", e.message);
+ equal("unsupported message handler command", e.status);
+ ok(e instanceof error.MessageHandlerError);
+
+ run_next_test();
+});
diff --git a/remote/shared/messagehandler/test/xpcshell/test_SessionData.js b/remote/shared/messagehandler/test/xpcshell/test_SessionData.js
new file mode 100644
index 0000000000..ef61ce27d4
--- /dev/null
+++ b/remote/shared/messagehandler/test/xpcshell/test_SessionData.js
@@ -0,0 +1,296 @@
+/* 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/. */
+
+const { ContextDescriptorType } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs"
+);
+const { RootMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs"
+);
+const { SessionData, SessionDataMethod } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs"
+);
+
+add_task(async function test_sessionData() {
+ const sessionData = new SessionData(new RootMessageHandler("session-id-1"));
+ equal(sessionData.getSessionData("mod", "event").length, 0);
+
+ const globalContext = {
+ type: ContextDescriptorType.All,
+ };
+ const otherContext = { type: "other-type", id: "some-id" };
+
+ info("Add a first event for the global context");
+ let updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Add, globalContext, ["first.event"]),
+ ]);
+ equal(updatedItems.length, 1, "One item updated");
+ equal(updatedItems[0].method, SessionDataMethod.Add, "One item added");
+ let updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 1, "One value added");
+ equal(updatedValues[0], "first.event", "Expected value was added");
+ checkEvents(sessionData.getSessionData("mod", "event"), [
+ {
+ value: "first.event",
+ contextDescriptor: globalContext,
+ },
+ ]);
+
+ info("Add the exact same data (same module, type, context, value)");
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Add, globalContext, ["first.event"]),
+ ]);
+ equal(updatedItems.length, 0, "No new item updated");
+ checkEvents(sessionData.getSessionData("mod", "event"), [
+ {
+ value: "first.event",
+ contextDescriptor: globalContext,
+ },
+ ]);
+
+ info("Add another context for the same event");
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Add, otherContext, ["first.event"]),
+ ]);
+ equal(updatedItems.length, 1, "One item updated");
+ equal(updatedItems[0].method, SessionDataMethod.Add, "One item added");
+ updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 1, "One value added");
+ equal(updatedValues[0], "first.event", "Expected value was added");
+ checkEvents(sessionData.getSessionData("mod", "event"), [
+ {
+ value: "first.event",
+ contextDescriptor: globalContext,
+ },
+ {
+ value: "first.event",
+ contextDescriptor: otherContext,
+ },
+ ]);
+
+ info("Add a second event for the global context");
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Add, globalContext, ["second.event"]),
+ ]);
+ equal(updatedItems.length, 1, "One item updated");
+ equal(updatedItems[0].method, SessionDataMethod.Add, "One item added");
+ updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 1, "One value added");
+ equal(updatedValues[0], "second.event", "Expected value was added");
+ checkEvents(sessionData.getSessionData("mod", "event"), [
+ {
+ value: "first.event",
+ contextDescriptor: globalContext,
+ },
+ {
+ value: "first.event",
+ contextDescriptor: otherContext,
+ },
+ {
+ value: "second.event",
+ contextDescriptor: globalContext,
+ },
+ ]);
+
+ info("Add two events for the global context");
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Add, globalContext, [
+ "third.event",
+ "fourth.event",
+ ]),
+ ]);
+ equal(updatedItems.length, 1, "One item updated");
+ equal(updatedItems[0].method, SessionDataMethod.Add, "One item added");
+ updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 2, "Two values added");
+ equal(updatedValues[0], "third.event", "Expected value was added");
+ equal(updatedValues[1], "fourth.event", "Expected value was added");
+ checkEvents(sessionData.getSessionData("mod", "event"), [
+ {
+ value: "first.event",
+ contextDescriptor: globalContext,
+ },
+ {
+ value: "first.event",
+ contextDescriptor: otherContext,
+ },
+ {
+ value: "second.event",
+ contextDescriptor: globalContext,
+ },
+ {
+ value: "third.event",
+ contextDescriptor: globalContext,
+ },
+ {
+ value: "fourth.event",
+ contextDescriptor: globalContext,
+ },
+ ]);
+
+ info("Remove the second, third and fourth events");
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Remove, globalContext, [
+ "second.event",
+ "third.event",
+ "fourth.event",
+ ]),
+ ]);
+ equal(updatedItems.length, 1, "One item updated");
+ equal(updatedItems[0].method, SessionDataMethod.Remove, "One item removed");
+ updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 3, "Three values removed");
+ equal(updatedValues[0], "second.event", "Expected value was removed");
+ equal(updatedValues[1], "third.event", "Expected value was removed");
+ equal(updatedValues[2], "fourth.event", "Expected value was removed");
+ checkEvents(sessionData.getSessionData("mod", "event"), [
+ {
+ value: "first.event",
+ contextDescriptor: globalContext,
+ },
+ {
+ value: "first.event",
+ contextDescriptor: otherContext,
+ },
+ ]);
+
+ info("Remove the global context from the first event");
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Remove, globalContext, ["first.event"]),
+ ]);
+ equal(updatedItems.length, 1, "One item updated");
+ equal(updatedItems[0].method, SessionDataMethod.Remove, "One item removed");
+ updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 1, "One value removed");
+ equal(updatedValues[0], "first.event", "Expected value was removed");
+ checkEvents(sessionData.getSessionData("mod", "event"), [
+ {
+ value: "first.event",
+ contextDescriptor: otherContext,
+ },
+ ]);
+
+ info("Remove the other context from the first event");
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Remove, otherContext, ["first.event"]),
+ ]);
+ equal(updatedItems.length, 1, "One item updated");
+ equal(updatedItems[0].method, SessionDataMethod.Remove, "One item removed");
+ updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 1, "One value removed");
+ equal(updatedValues[0], "first.event", "Expected value was removed");
+ checkEvents(sessionData.getSessionData("mod", "event"), []);
+
+ info("Add two events for different contexts");
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Add, otherContext, ["first.event"]),
+ createUpdate(SessionDataMethod.Add, globalContext, ["second.event"]),
+ ]);
+ equal(updatedItems.length, 2, "Two items updated");
+ equal(updatedItems[0].method, SessionDataMethod.Add, "First item added");
+ updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 1, "One value for first item added");
+ equal(updatedValues[0], "first.event", "Expected value first item was added");
+ equal(updatedItems[1].method, SessionDataMethod.Add, "Second item added");
+ updatedValues = updatedItems[1].values;
+ equal(updatedValues.length, 1, "One value for second item added");
+ equal(
+ updatedValues[0],
+ "second.event",
+ "Expected value second item was added"
+ );
+ checkEvents(sessionData.getSessionData("mod", "event"), [
+ {
+ value: "first.event",
+ contextDescriptor: otherContext,
+ },
+ {
+ value: "second.event",
+ contextDescriptor: globalContext,
+ },
+ ]);
+
+ info("Remove two events for different contexts");
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Remove, otherContext, ["first.event"]),
+ createUpdate(SessionDataMethod.Remove, globalContext, ["second.event"]),
+ ]);
+ equal(updatedItems.length, 2, "Two items updated");
+ equal(updatedItems[0].method, SessionDataMethod.Remove, "First item removed");
+ updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 1, "One value for first item removed");
+ equal(
+ updatedValues[0],
+ "first.event",
+ "Expected value first item was removed"
+ );
+ equal(
+ updatedItems[1].method,
+ SessionDataMethod.Remove,
+ "Second item removed"
+ );
+ updatedValues = updatedItems[1].values;
+ equal(updatedValues.length, 1, "One value for second item removed");
+ equal(
+ updatedValues[0],
+ "second.event",
+ "Expected value second item was removed"
+ );
+ checkEvents(sessionData.getSessionData("mod", "event"), []);
+
+ info("Add and remove event in different order");
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Remove, otherContext, ["first.event"]),
+ createUpdate(SessionDataMethod.Add, otherContext, ["first.event"]),
+ ]);
+ equal(updatedItems.length, 1, "One item updated");
+ equal(updatedItems[0].method, SessionDataMethod.Add, "One item added");
+ updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 1, "One value added");
+ equal(updatedValues[0], "first.event", "Expected value was added");
+ checkEvents(sessionData.getSessionData("mod", "event"), [
+ {
+ value: "first.event",
+ contextDescriptor: otherContext,
+ },
+ ]);
+
+ updatedItems = sessionData.applySessionData([
+ createUpdate(SessionDataMethod.Add, otherContext, ["first.event"]),
+ createUpdate(SessionDataMethod.Remove, otherContext, ["first.event"]),
+ ]);
+ equal(updatedItems.length, 1, "No item update");
+ equal(updatedItems[0].method, SessionDataMethod.Remove, "One item removed");
+ updatedValues = updatedItems[0].values;
+ equal(updatedValues.length, 1, "One value removed");
+ equal(updatedValues[0], "first.event", "Expected value was removed");
+ checkEvents(sessionData.getSessionData("mod", "event"), []);
+});
+
+function checkEvents(events, expectedEvents) {
+ // Check the arrays have the same size.
+ equal(events.length, expectedEvents.length);
+
+ // Check all the expectedEvents can be found in the events array.
+ for (const expected of expectedEvents) {
+ ok(
+ events.some(
+ event =>
+ expected.contextDescriptor.type === event.contextDescriptor.type &&
+ expected.contextDescriptor.id === event.contextDescriptor.id &&
+ expected.value == event.value
+ )
+ );
+ }
+}
+
+function createUpdate(method, contextDescriptor, values) {
+ return {
+ method,
+ moduleName: "mod",
+ category: "event",
+ contextDescriptor,
+ values,
+ };
+}
diff --git a/remote/shared/messagehandler/test/xpcshell/xpcshell.ini b/remote/shared/messagehandler/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..9199459e0a
--- /dev/null
+++ b/remote/shared/messagehandler/test/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+# 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/.
+
+[test_Errors.js]
+[test_SessionData.js]