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.js84
-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.js40
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/doc_messagehandler_broadcasting_xul.xhtml3
-rw-r--r--remote/shared/messagehandler/test/browser/broadcast/head.js48
-rw-r--r--remote/shared/messagehandler/test/browser/browser.ini25
-rw-r--r--remote/shared/messagehandler/test/browser/browser_bfcache.js98
-rw-r--r--remote/shared/messagehandler/test/browser/browser_events_dispatcher.js449
-rw-r--r--remote/shared/messagehandler/test/browser/browser_events_handler.js57
-rw-r--r--remote/shared/messagehandler/test/browser/browser_events_module.js281
-rw-r--r--remote/shared/messagehandler/test/browser/browser_frame_context_utils.js98
-rw-r--r--remote/shared/messagehandler/test/browser/browser_handle_command_errors.js218
-rw-r--r--remote/shared/messagehandler/test/browser/browser_handle_command_retry.js239
-rw-r--r--remote/shared/messagehandler/test/browser/browser_handle_simple_command.js203
-rw-r--r--remote/shared/messagehandler/test/browser/browser_registry.js37
-rw-r--r--remote/shared/messagehandler/test/browser/browser_session_data.js271
-rw-r--r--remote/shared/messagehandler/test/browser/browser_session_data_browser_element.js94
-rw-r--r--remote/shared/messagehandler/test/browser/browser_session_data_constructor_race.js50
-rw-r--r--remote/shared/messagehandler/test/browser/browser_session_data_update.js113
-rw-r--r--remote/shared/messagehandler/test/browser/browser_session_data_update_categories.js91
-rw-r--r--remote/shared/messagehandler/test/browser/browser_session_data_update_contexts.js194
-rw-r--r--remote/shared/messagehandler/test/browser/browser_windowglobal_to_root.js47
-rw-r--r--remote/shared/messagehandler/test/browser/head.js236
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/ModuleRegistry.sys.mjs40
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/root/command.sys.mjs29
-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/root/windowglobaltoroot.sys.mjs29
-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.mjs85
-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/eventonprefchange.sys.mjs33
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal/retry.sys.mjs84
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal/sessiondataupdate.sys.mjs33
-rw-r--r--remote/shared/messagehandler/test/browser/resources/modules/windowglobal/windowglobaltoroot.sys.mjs47
-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.js91
-rw-r--r--remote/shared/messagehandler/test/xpcshell/test_SessionData.js296
-rw-r--r--remote/shared/messagehandler/test/xpcshell/xpcshell.ini6
49 files changed, 4319 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..c140c26fc6
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/browser_filter_top_browsing_context.js
@@ -0,0 +1,84 @@
+/* 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..50326d3885
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/browser_with_frames.js
@@ -0,0 +1,40 @@
+/* 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..eb97549c26
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/broadcast/head.js
@@ -0,0 +1,48 @@
+/* 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";
+
+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.
+ * @returns {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
+) {
+ 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..8d93cdb795
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser.ini
@@ -0,0 +1,25 @@
+[DEFAULT]
+tags = remote
+subsuite = remote
+support-files =
+ head.js
+ resources/*
+prefs =
+ remote.messagehandler.modulecache.useBrowserTestRoot=true
+
+[browser_bfcache.js]
+[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_browser_element.js]
+[browser_session_data_constructor_race.js]
+[browser_session_data_update.js]
+[browser_session_data_update_categories.js]
+[browser_session_data_update_contexts.js]
+[browser_windowglobal_to_root.js]
diff --git a/remote/shared/messagehandler/test/browser/browser_bfcache.js b/remote/shared/messagehandler/test/browser/browser_bfcache.js
new file mode 100644
index 0000000000..f829d8b58d
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_bfcache.js
@@ -0,0 +1,98 @@
+/* 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_PREF = "remote.messagehandler.test.pref";
+
+// Check that pages in bfcache no longer have message handlers attached to them,
+// and that they will not emit unexpected events.
+add_task(async function test_bfcache_broadcast() {
+ const tab = await addTab("https://example.com/document-builder.sjs?html=tab");
+ const rootMessageHandler = createRootMessageHandler("session-id-bfcache");
+
+ try {
+ const browsingContext = tab.linkedBrowser.browsingContext;
+ const contextDescriptor = {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext.browserId,
+ };
+
+ // Whenever a "preference-changed" event from the eventonprefchange module
+ // will be received on the root MessageHandler, increment a counter.
+ let preferenceChangeEventCount = 0;
+ const onEvent = (evtName, wrappedEvt) => {
+ if (wrappedEvt.name === "preference-changed") {
+ preferenceChangeEventCount++;
+ }
+ };
+ rootMessageHandler.on("message-handler-event", onEvent);
+
+ // Initialize the preference, no eventonprefchange module should be created
+ // yet so preferenceChangeEventCount is not expected to be updated.
+ Services.prefs.setIntPref(TEST_PREF, 0);
+ await TestUtils.waitForCondition(() => preferenceChangeEventCount >= 0);
+ is(preferenceChangeEventCount, 0);
+
+ // Broadcast a "ping" command to force the creation of the eventonprefchange
+ // module
+ let values = await sendPingCommand(rootMessageHandler, contextDescriptor);
+ is(values.length, 1, "Broadcast returned a single value");
+
+ Services.prefs.setIntPref(TEST_PREF, 1);
+ await TestUtils.waitForCondition(() => preferenceChangeEventCount >= 1);
+ is(preferenceChangeEventCount, 1);
+
+ info("Navigate to another page");
+ await loadURL(
+ tab.linkedBrowser,
+ "https://example.com/document-builder.sjs?html=othertab"
+ );
+
+ values = await sendPingCommand(rootMessageHandler, contextDescriptor);
+ is(values.length, 1, "Broadcast returned a single value after navigation");
+
+ info("Update the preference and check we only receive 1 event");
+ Services.prefs.setIntPref(TEST_PREF, 2);
+ await TestUtils.waitForCondition(() => preferenceChangeEventCount >= 2);
+ is(preferenceChangeEventCount, 2);
+
+ info("Navigate to another origin");
+ await loadURL(
+ tab.linkedBrowser,
+ "https://example.org/document-builder.sjs?html=otherorigin"
+ );
+
+ values = await sendPingCommand(rootMessageHandler, contextDescriptor);
+ is(
+ values.length,
+ 1,
+ "Broadcast returned a single value after cross origin navigation"
+ );
+
+ info("Update the preference and check again that we only receive 1 event");
+ Services.prefs.setIntPref(TEST_PREF, 3);
+ await TestUtils.waitForCondition(() => preferenceChangeEventCount >= 3);
+ is(preferenceChangeEventCount, 3);
+ } finally {
+ rootMessageHandler.destroy();
+ gBrowser.removeTab(tab);
+ Services.prefs.clearUserPref(TEST_PREF);
+ }
+});
+
+function sendPingCommand(rootMessageHandler, contextDescriptor) {
+ return rootMessageHandler.handleCommand({
+ moduleName: "eventonprefchange",
+ commandName: "ping",
+ params: {},
+ destination: {
+ contextDescriptor,
+ type: WindowGlobalMessageHandler.type,
+ },
+ });
+}
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..ea75167bf2
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_events_dispatcher.js
@@ -0,0 +1,449 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * 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..705c306de3
--- /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.addSessionDataItem({
+ 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..536c1d9754
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_events_module.js
@@ -0,0 +1,281 @@
+/* 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"
+);
+
+/**
+ * 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..cddcba3529
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_frame_context_utils.js
@@ -0,0 +1,98 @@
+/* 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/BrowsingContextUtils.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 BrowsingContextUtils 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 BrowsingContextUtils = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs"
+ );
+ is(
+ BrowsingContextUtils.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..5f0bdb78df
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_handle_command_errors.js
@@ -0,0 +1,218 @@
+/* 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"
+);
+
+// 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..fc70ae8c4e
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_handle_command_retry.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// 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..0a086d6f09
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_handle_simple_command.js
@@ -0,0 +1,203 @@
+/* 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"
+);
+
+// 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..945ac06c19
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_registry.js
@@ -0,0 +1,37 @@
+/* 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..f0fef31883
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_session_data.js
@@ -0,0 +1,271 @@
+/* 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.loadURIString(
+ 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.addSessionDataItem({
+ 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.removeSessionDataItem({
+ 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_browser_element.js b/remote/shared/messagehandler/test/browser/browser_session_data_browser_element.js
new file mode 100644
index 0000000000..9c15974ae6
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_session_data_browser_element.js
@@ -0,0 +1,94 @@
+/* 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.addSessionDataItem({
+ 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..03ed59166f
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_session_data_constructor_race.js
@@ -0,0 +1,50 @@
+/* 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 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.addSessionDataItem({
+ 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/browser_session_data_update.js b/remote/shared/messagehandler/test/browser/browser_session_data_update.js
new file mode 100644
index 0000000000..342a4a6139
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_session_data_update.js
@@ -0,0 +1,113 @@
+/* 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";
+
+const { assertUpdate, createSessionDataUpdate, getUpdates } =
+ SessionDataUpdateHelpers;
+
+// Test various session data update scenarios against a single browsing context.
+add_task(async function test_session_data_update() {
+ const tab1 = gBrowser.selectedTab;
+ await loadURL(tab1.linkedBrowser, TEST_PAGE);
+ const browsingContext1 = tab1.linkedBrowser.browsingContext;
+
+ const root = createRootMessageHandler("session-data-update");
+
+ info("Add a new session data item, expect one return value");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-1"], "add", "category1"),
+ ]);
+ let processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 1);
+ assertUpdate(processedUpdates.at(-1), ["text-1"], "category1");
+
+ info("Add two session data items, expect one return value with both items");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-2"], "add", "category1"),
+ createSessionDataUpdate(["text-3"], "add", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 2);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-1", "text-2", "text-3"],
+ "category1"
+ );
+
+ info("Try to add an existing data item, expect no update broadcast");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-1"], "add", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 2);
+
+ info("Add an existing and a new item");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-2", "text-4"], "add", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 3);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-1", "text-2", "text-3", "text-4"],
+ "category1"
+ );
+
+ info("Remove an item, expect only the new item to return");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-3"], "remove", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 4);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-1", "text-2", "text-4"],
+ "category1"
+ );
+
+ info("Remove a unknown item, expect no return value");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-unknown"], "remove", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 4);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-1", "text-2", "text-4"],
+ "category1"
+ );
+
+ info("Remove an existing and a unknown item");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-2"], "remove", "category1"),
+ createSessionDataUpdate(["text-unknown"], "remove", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 5);
+ assertUpdate(processedUpdates.at(-1), ["text-1", "text-4"], "category1");
+
+ info("Add and remove at once");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-5"], "add", "category1"),
+ createSessionDataUpdate(["text-4"], "remove", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 6);
+ assertUpdate(processedUpdates.at(-1), ["text-1", "text-5"], "category1");
+
+ info("Adding and removing an item does not trigger any update");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-6"], "add", "category1"),
+ createSessionDataUpdate(["text-6"], "remove", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ // TODO: We could detect transactions which can't have any impact and fully
+ // ignore them. See Bug 1810807.
+ todo_is(processedUpdates.length, 6);
+ assertUpdate(processedUpdates.at(-1), ["text-1", "text-5"], "category1");
+
+ root.destroy();
+});
diff --git a/remote/shared/messagehandler/test/browser/browser_session_data_update_categories.js b/remote/shared/messagehandler/test/browser/browser_session_data_update_categories.js
new file mode 100644
index 0000000000..b1cadcf095
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_session_data_update_categories.js
@@ -0,0 +1,91 @@
+/* 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";
+
+const { assertUpdate, createSessionDataUpdate, getUpdates } =
+ SessionDataUpdateHelpers;
+
+// Test session data update scenarios involving different session data item
+// categories.
+add_task(async function test_session_data_update_categories() {
+ const tab1 = gBrowser.selectedTab;
+ await loadURL(tab1.linkedBrowser, TEST_PAGE);
+ const browsingContext1 = tab1.linkedBrowser.browsingContext;
+
+ const root = createRootMessageHandler("session-data-update-categories");
+ await root.updateSessionData([
+ createSessionDataUpdate(["value1-1"], "add", "category1"),
+ createSessionDataUpdate(["value1-2"], "add", "category1"),
+ ]);
+
+ let processedUpdates = await getUpdates(root, browsingContext1);
+
+ is(processedUpdates.length, 1);
+ assertUpdate(processedUpdates.at(-1), ["value1-1", "value1-2"], "category1");
+
+ info("Adding a new item in category1 broadcasts all category1 items");
+ await root.updateSessionData([
+ createSessionDataUpdate(["value1-3"], "add", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 2);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["value1-1", "value1-2", "value1-3"],
+ "category1"
+ );
+
+ info("Removing a new item in category1 broadcasts all category1 items");
+ await root.updateSessionData([
+ createSessionDataUpdate(["value1-1"], "remove", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 3);
+ assertUpdate(processedUpdates.at(-1), ["value1-2", "value1-3"], "category1");
+
+ info("Adding a new category does not broadcast category1 items");
+ await root.updateSessionData([
+ createSessionDataUpdate(["value2-1"], "add", "category2"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 4);
+ assertUpdate(processedUpdates.at(-1), ["value2-1"], "category2");
+
+ info("Adding an item in 2 categories triggers an update for each category");
+ await root.updateSessionData([
+ createSessionDataUpdate(["value1-4"], "add", "category1"),
+ createSessionDataUpdate(["value2-2"], "add", "category2"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 6);
+ assertUpdate(
+ processedUpdates.at(-2),
+ ["value1-2", "value1-3", "value1-4"],
+ "category1"
+ );
+ assertUpdate(processedUpdates.at(-1), ["value2-1", "value2-2"], "category2");
+
+ info("Removing an item in 2 categories triggers an update for each category");
+ await root.updateSessionData([
+ createSessionDataUpdate(["value1-4"], "remove", "category1"),
+ createSessionDataUpdate(["value2-2"], "remove", "category2"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 8);
+ assertUpdate(processedUpdates.at(-2), ["value1-2", "value1-3"], "category1");
+ assertUpdate(processedUpdates.at(-1), ["value2-1"], "category2");
+
+ info("Opening a new tab triggers an update for each category");
+ const tab2 = await addTab(TEST_PAGE);
+ const browsingContext2 = tab2.linkedBrowser.browsingContext;
+ processedUpdates = await getUpdates(root, browsingContext2);
+ is(processedUpdates.length, 2);
+ assertUpdate(processedUpdates.at(-2), ["value1-2", "value1-3"], "category1");
+ assertUpdate(processedUpdates.at(-1), ["value2-1"], "category2");
+
+ root.destroy();
+ gBrowser.removeTab(tab2);
+});
diff --git a/remote/shared/messagehandler/test/browser/browser_session_data_update_contexts.js b/remote/shared/messagehandler/test/browser/browser_session_data_update_contexts.js
new file mode 100644
index 0000000000..711df1fc56
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_session_data_update_contexts.js
@@ -0,0 +1,194 @@
+/* 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";
+
+const { assertUpdate, createSessionDataUpdate, getUpdates } =
+ SessionDataUpdateHelpers;
+
+// Test session data update scenarios involving 2 browsing contexts, and using
+// the TopBrowsingContext ContextDescriptor type.
+add_task(async function test_session_data_update_contexts() {
+ const tab1 = gBrowser.selectedTab;
+ await loadURL(tab1.linkedBrowser, TEST_PAGE);
+ const browsingContext1 = tab1.linkedBrowser.browsingContext;
+
+ const root = createRootMessageHandler("session-data-update-contexts");
+
+ info("Add several items over 2 separate updates for all contexts");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-1"], "add", "category1"),
+ ]);
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-2"], "add", "category1"),
+ createSessionDataUpdate(["text-3"], "add", "category1"),
+ ]);
+
+ info("Check we processed two distinct updates in browsingContext 1");
+ let processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 2);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-1", "text-2", "text-3"],
+ "category1"
+ );
+
+ info("Open a new tab on the same test URL");
+ const tab2 = await addTab(TEST_PAGE);
+ const browsingContext2 = tab2.linkedBrowser.browsingContext;
+
+ processedUpdates = await getUpdates(root, browsingContext2);
+ is(processedUpdates.length, 1);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-1", "text-2", "text-3"],
+ "category1"
+ );
+
+ info("Add two items: one globally and one in a single context");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-4"], "add", "category1"),
+ createSessionDataUpdate(["text-5"], "add", "category1", {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext2.browserId,
+ }),
+ ]);
+
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 3);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-1", "text-2", "text-3", "text-4"],
+ "category1"
+ );
+
+ processedUpdates = await getUpdates(root, browsingContext2);
+ is(processedUpdates.length, 2);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-1", "text-2", "text-3", "text-4", "text-5"],
+ "category1"
+ );
+
+ info("Remove two items: one globally and one in a single context");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-1"], "remove", "category1"),
+ createSessionDataUpdate(["text-5"], "remove", "category1", {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext2.browserId,
+ }),
+ ]);
+
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 4);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-2", "text-3", "text-4"],
+ "category1"
+ );
+
+ processedUpdates = await getUpdates(root, browsingContext2);
+ is(processedUpdates.length, 3);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-2", "text-3", "text-4"],
+ "category1"
+ );
+
+ info(
+ "Add session data item to all contexts and remove this event for one context (2 steps)"
+ );
+
+ info("First step: add an item to browsingContext1");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-6"], "add", "category1", {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext1.browserId,
+ }),
+ ]);
+
+ info(
+ "Second step: remove the item from browsingContext1, and add it globally"
+ );
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-6"], "remove", "category1", {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext1.browserId,
+ }),
+ createSessionDataUpdate(["text-6"], "add", "category1"),
+ ]);
+
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 6);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-2", "text-3", "text-4", "text-6"],
+ "category1"
+ );
+
+ processedUpdates = await getUpdates(root, browsingContext2);
+ is(processedUpdates.length, 4);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-2", "text-3", "text-4", "text-6"],
+ "category1"
+ );
+
+ info(
+ "Remove the event, which has also an individual subscription, for all contexts (2 steps)"
+ );
+
+ info("First step: Add the same item for browsingContext1 and globally");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-7"], "add", "category1", {
+ type: ContextDescriptorType.TopBrowsingContext,
+ id: browsingContext1.browserId,
+ }),
+ createSessionDataUpdate(["text-7"], "add", "category1"),
+ ]);
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 7);
+ // We will find text-7 twice here, the module is responsible for not applying
+ // the same session data item twice. Each item corresponds to a different
+ // descriptor which matched browsingContext1.
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-2", "text-3", "text-4", "text-6", "text-7", "text-7"],
+ "category1"
+ );
+
+ processedUpdates = await getUpdates(root, browsingContext2);
+ is(processedUpdates.length, 5);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-2", "text-3", "text-4", "text-6", "text-7"],
+ "category1"
+ );
+
+ info("Second step: Remove the item globally");
+ await root.updateSessionData([
+ createSessionDataUpdate(["text-7"], "remove", "category1"),
+ ]);
+
+ processedUpdates = await getUpdates(root, browsingContext1);
+ is(processedUpdates.length, 8);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-2", "text-3", "text-4", "text-6", "text-7"],
+ "category1"
+ );
+
+ processedUpdates = await getUpdates(root, browsingContext2);
+ is(processedUpdates.length, 6);
+ assertUpdate(
+ processedUpdates.at(-1),
+ ["text-2", "text-3", "text-4", "text-6"],
+ "category1"
+ );
+
+ root.destroy();
+
+ gBrowser.removeTab(tab2);
+});
diff --git a/remote/shared/messagehandler/test/browser/browser_windowglobal_to_root.js b/remote/shared/messagehandler/test/browser/browser_windowglobal_to_root.js
new file mode 100644
index 0000000000..57629e5485
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/browser_windowglobal_to_root.js
@@ -0,0 +1,47 @@
+/* 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"
+);
+
+add_task(async function test_windowGlobal_to_root_command() {
+ // Navigate to a page to make sure that the windowglobal modules run in a
+ // different process than the root module.
+ 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 rootMessageHandler = createRootMessageHandler(
+ "session-id-windowglobal-to-rootModule"
+ );
+
+ for (const commandName of [
+ "testHandleCommandToRoot",
+ "testSendRootCommand",
+ ]) {
+ const valueFromRoot = await rootMessageHandler.handleCommand({
+ moduleName: "windowglobaltoroot",
+ commandName,
+ destination: {
+ type: WindowGlobalMessageHandler.type,
+ id: browsingContextId,
+ },
+ });
+
+ is(
+ valueFromRoot,
+ "root-value-called-from-windowglobal",
+ "Retrieved the expected value from windowglobaltoroot using " +
+ commandName
+ );
+ }
+
+ rootMessageHandler.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..5397fc0761
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/head.js
@@ -0,0 +1,236 @@
+/* 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 { WindowGlobalMessageHandler } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.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.loadURIString(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 {XULTab} 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".
+ * @returns {XULBrowser}
+ * 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.
+ *
+ * @returns {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,
+ };
+}
+
+const SessionDataUpdateHelpers = {
+ getUpdates(rootMessageHandler, browsingContext) {
+ return rootMessageHandler.handleCommand({
+ moduleName: "sessiondataupdate",
+ commandName: "getSessionDataUpdates",
+ destination: {
+ id: browsingContext.id,
+ type: WindowGlobalMessageHandler.type,
+ },
+ });
+ },
+
+ createSessionDataUpdate(
+ values,
+ method,
+ category,
+ descriptor = { type: ContextDescriptorType.All }
+ ) {
+ return {
+ method,
+ values,
+ moduleName: "sessiondataupdate",
+ category,
+ contextDescriptor: descriptor,
+ };
+ },
+
+ assertUpdate(update, expectedValues, expectedCategory) {
+ is(
+ update.length,
+ expectedValues.length,
+ "Update has the expected number of values"
+ );
+
+ for (const item of update) {
+ info(`Check session data update item '${item.value}'`);
+ is(item.category, expectedCategory, "Item has the expected category");
+ is(
+ expectedValues[update.indexOf(item)],
+ item.value,
+ "Item has the expected value"
+ );
+ }
+ },
+};
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..7d93f45b33
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/ModuleRegistry.sys.mjs
@@ -0,0 +1,40 @@
+/* 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/. */
+
+export const modules = {
+ root: {},
+ "windowglobal-in-root": {},
+ windowglobal: {},
+};
+
+const BASE_FOLDER =
+ "chrome://mochitests/content/browser/remote/shared/messagehandler/test/browser/resources/modules";
+
+// eslint-disable-next-line mozilla/lazy-getter-object-name
+ChromeUtils.defineESModuleGetters(modules.root, {
+ command: `${BASE_FOLDER}/root/command.sys.mjs`,
+ event: `${BASE_FOLDER}/root/event.sys.mjs`,
+ invalid: `${BASE_FOLDER}/root/invalid.sys.mjs`,
+ rootOnly: `${BASE_FOLDER}/root/rootOnly.sys.mjs`,
+ windowglobaltoroot: `${BASE_FOLDER}/root/windowglobaltoroot.sys.mjs`,
+});
+
+// eslint-disable-next-line mozilla/lazy-getter-object-name
+ChromeUtils.defineESModuleGetters(modules["windowglobal-in-root"], {
+ command: `${BASE_FOLDER}/windowglobal-in-root/command.sys.mjs`,
+ event: `${BASE_FOLDER}/windowglobal-in-root/event.sys.mjs`,
+});
+
+// eslint-disable-next-line mozilla/lazy-getter-object-name
+ChromeUtils.defineESModuleGetters(modules.windowglobal, {
+ command: `${BASE_FOLDER}/windowglobal/command.sys.mjs`,
+ commandwindowglobalonly: `${BASE_FOLDER}/windowglobal/commandwindowglobalonly.sys.mjs`,
+ event: `${BASE_FOLDER}/windowglobal/event.sys.mjs`,
+ eventemitter: `${BASE_FOLDER}/windowglobal/eventemitter.sys.mjs`,
+ eventnointercept: `${BASE_FOLDER}/windowglobal/eventnointercept.sys.mjs`,
+ eventonprefchange: `${BASE_FOLDER}/windowglobal/eventonprefchange.sys.mjs`,
+ retry: `${BASE_FOLDER}/windowglobal/retry.sys.mjs`,
+ sessiondataupdate: `${BASE_FOLDER}/windowglobal/sessiondataupdate.sys.mjs`,
+ windowglobaltoroot: `${BASE_FOLDER}/windowglobal/windowglobaltoroot.sys.mjs`,
+});
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..29e4a75828
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/root/command.sys.mjs
@@ -0,0 +1,29 @@
+/* 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
+ */
+
+ 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/root/windowglobaltoroot.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/root/windowglobaltoroot.sys.mjs
new file mode 100644
index 0000000000..0975c4abd5
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/root/windowglobaltoroot.sys.mjs
@@ -0,0 +1,29 @@
+/* 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 WindowGlobalToRootModule extends Module {
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ getValueFromRoot() {
+ this.#assertParentProcess();
+ return "root-value-called-from-windowglobal";
+ }
+
+ #assertParentProcess() {
+ const isParent =
+ Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT;
+
+ if (!isParent) {
+ throw new Error("Can only run in the parent process");
+ }
+ }
+}
+
+export const windowglobaltoroot = WindowGlobalToRootModule;
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..99ee76a4b8
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/command.sys.mjs
@@ -0,0 +1,85 @@
+/* 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._subscribedEvents = new Set();
+
+ this._createdByMessageHandlerConstructor =
+ this._isCreatedByMessageHandlerConstructor();
+ }
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ _applySessionData(params) {
+ if (params.category === "testCategory") {
+ 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);
+ }
+ }
+
+ // Subscribe to all events, which have an item in SessionData
+ for (const { value } of filteredSessionData) {
+ if (!this._subscribedEvents.has(value)) {
+ this._subscribedEvents.add(value);
+ }
+ }
+ }
+
+ if (params.category === "browser_session_data_browser_element") {
+ this.emitEvent("received-session-data", {
+ contextId: this.messageHandler.contextId,
+ });
+ }
+ }
+
+ 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/eventonprefchange.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/eventonprefchange.sys.mjs
new file mode 100644
index 0000000000..33cb25d10b
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/eventonprefchange.sys.mjs
@@ -0,0 +1,33 @@
+/* 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";
+
+const TEST_PREF = "remote.messagehandler.test.pref";
+
+class EventOnPrefChangeModule extends Module {
+ constructor(messageHandler) {
+ super(messageHandler);
+ Services.prefs.addObserver(TEST_PREF, this.#onPreferenceUpdated);
+ }
+
+ destroy() {
+ Services.prefs.removeObserver(TEST_PREF, this.#onPreferenceUpdated);
+ }
+
+ #onPreferenceUpdated = () => {
+ this.emitEvent("preference-changed");
+ };
+
+ /**
+ * Commands
+ */
+
+ ping() {
+ // We only use this command to force creating the module.
+ return 1;
+ }
+}
+
+export const eventonprefchange = EventOnPrefChangeModule;
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/resources/modules/windowglobal/sessiondataupdate.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/sessiondataupdate.sys.mjs
new file mode 100644
index 0000000000..5e9ce00b46
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/sessiondataupdate.sys.mjs
@@ -0,0 +1,33 @@
+/* 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 SessionDataUpdateModule extends Module {
+ #sessionDataUpdates;
+
+ constructor(messageHandler) {
+ super(messageHandler);
+ this.#sessionDataUpdates = [];
+ }
+
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ _applySessionData(params) {
+ const filteredSessionData = params.sessionData.filter(item =>
+ this.messageHandler.matchesContext(item.contextDescriptor)
+ );
+ this.#sessionDataUpdates.push(filteredSessionData);
+ }
+
+ getSessionDataUpdates() {
+ return this.#sessionDataUpdates;
+ }
+}
+
+export const sessiondataupdate = SessionDataUpdateModule;
diff --git a/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/windowglobaltoroot.sys.mjs b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/windowglobaltoroot.sys.mjs
new file mode 100644
index 0000000000..815a836d9c
--- /dev/null
+++ b/remote/shared/messagehandler/test/browser/resources/modules/windowglobal/windowglobaltoroot.sys.mjs
@@ -0,0 +1,47 @@
+/* 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";
+import { RootMessageHandler } from "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs";
+
+class WindowGlobalToRootModule extends Module {
+ constructor(messageHandler) {
+ super(messageHandler);
+ this.#assertContentProcess();
+ }
+
+ destroy() {}
+
+ /**
+ * Commands
+ */
+
+ testHandleCommandToRoot(params, destination) {
+ return this.messageHandler.handleCommand({
+ moduleName: "windowglobaltoroot",
+ commandName: "getValueFromRoot",
+ destination: {
+ type: RootMessageHandler.type,
+ },
+ });
+ }
+
+ testSendRootCommand(params, destination) {
+ return this.messageHandler.sendRootCommand({
+ moduleName: "windowglobaltoroot",
+ commandName: "getValueFromRoot",
+ });
+ }
+
+ #assertContentProcess() {
+ const isContent =
+ Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+
+ if (!isContent) {
+ throw new Error("Can only run in a content process");
+ }
+ }
+}
+
+export const windowglobaltoroot = WindowGlobalToRootModule;
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..26187dac11
--- /dev/null
+++ b/remote/shared/messagehandler/test/xpcshell/test_Errors.js
@@ -0,0 +1,91 @@
+/* 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_task(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);
+});
+
+add_task(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);
+});
+
+add_task(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);
+});
+
+add_task(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);
+});
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]