diff options
Diffstat (limited to '')
46 files changed, 3926 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) + ); +}); |