summaryrefslogtreecommitdiffstats
path: root/devtools/shared/resources/tests
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/resources/tests')
-rw-r--r--devtools/shared/resources/tests/.eslintrc.js6
-rw-r--r--devtools/shared/resources/tests/browser.ini64
-rw-r--r--devtools/shared/resources/tests/browser_browser_resources_console_messages.js90
-rw-r--r--devtools/shared/resources/tests/browser_resources_client_caching.js362
-rw-r--r--devtools/shared/resources/tests/browser_resources_console_messages.js460
-rw-r--r--devtools/shared/resources/tests/browser_resources_console_messages_workers.js246
-rw-r--r--devtools/shared/resources/tests/browser_resources_css_changes.js134
-rw-r--r--devtools/shared/resources/tests/browser_resources_css_messages.js202
-rw-r--r--devtools/shared/resources/tests/browser_resources_document_events.js143
-rw-r--r--devtools/shared/resources/tests/browser_resources_error_messages.js614
-rw-r--r--devtools/shared/resources/tests/browser_resources_getAllResources.js102
-rw-r--r--devtools/shared/resources/tests/browser_resources_network_event_stacktraces.js104
-rw-r--r--devtools/shared/resources/tests/browser_resources_network_events.js258
-rw-r--r--devtools/shared/resources/tests/browser_resources_platform_messages.js149
-rw-r--r--devtools/shared/resources/tests/browser_resources_root_node.js129
-rw-r--r--devtools/shared/resources/tests/browser_resources_several_resources.js120
-rw-r--r--devtools/shared/resources/tests/browser_resources_sources.js198
-rw-r--r--devtools/shared/resources/tests/browser_resources_stylesheets.js506
-rw-r--r--devtools/shared/resources/tests/browser_resources_target_destroy.js92
-rw-r--r--devtools/shared/resources/tests/browser_resources_target_resources_race.js77
-rw-r--r--devtools/shared/resources/tests/browser_resources_target_switching.js101
-rw-r--r--devtools/shared/resources/tests/browser_resources_websocket.js142
-rw-r--r--devtools/shared/resources/tests/browser_target_list_browser_workers.js195
-rw-r--r--devtools/shared/resources/tests/browser_target_list_frames.js168
-rw-r--r--devtools/shared/resources/tests/browser_target_list_getAllTargets.js108
-rw-r--r--devtools/shared/resources/tests/browser_target_list_preffedoff.js91
-rw-r--r--devtools/shared/resources/tests/browser_target_list_processes.js195
-rw-r--r--devtools/shared/resources/tests/browser_target_list_service_workers.js79
-rw-r--r--devtools/shared/resources/tests/browser_target_list_service_workers_navigation.js393
-rw-r--r--devtools/shared/resources/tests/browser_target_list_switchToTarget.js147
-rw-r--r--devtools/shared/resources/tests/browser_target_list_tab_workers.js330
-rw-r--r--devtools/shared/resources/tests/browser_target_list_watchTargets.js256
-rw-r--r--devtools/shared/resources/tests/early_console_document.html14
-rw-r--r--devtools/shared/resources/tests/fission_document.html47
-rw-r--r--devtools/shared/resources/tests/fission_iframe.html29
-rw-r--r--devtools/shared/resources/tests/head.js150
-rw-r--r--devtools/shared/resources/tests/network_document.html13
-rw-r--r--devtools/shared/resources/tests/service-worker-sources.js2
-rw-r--r--devtools/shared/resources/tests/sources.html22
-rw-r--r--devtools/shared/resources/tests/sources.js2
-rw-r--r--devtools/shared/resources/tests/style_document.css1
-rw-r--r--devtools/shared/resources/tests/style_document.html16
-rw-r--r--devtools/shared/resources/tests/style_iframe.css1
-rw-r--r--devtools/shared/resources/tests/style_iframe.html15
-rw-r--r--devtools/shared/resources/tests/test_service_worker.js11
-rw-r--r--devtools/shared/resources/tests/test_sw_page.html19
-rw-r--r--devtools/shared/resources/tests/test_sw_page_worker.js5
-rw-r--r--devtools/shared/resources/tests/test_worker.js15
-rw-r--r--devtools/shared/resources/tests/websocket_backend_wsh.py21
-rw-r--r--devtools/shared/resources/tests/websocket_frontend.html39
-rw-r--r--devtools/shared/resources/tests/worker-sources.js2
51 files changed, 6685 insertions, 0 deletions
diff --git a/devtools/shared/resources/tests/.eslintrc.js b/devtools/shared/resources/tests/.eslintrc.js
new file mode 100644
index 0000000000..3d0bd99e1b
--- /dev/null
+++ b/devtools/shared/resources/tests/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/shared/resources/tests/browser.ini b/devtools/shared/resources/tests/browser.ini
new file mode 100644
index 0000000000..afbc008130
--- /dev/null
+++ b/devtools/shared/resources/tests/browser.ini
@@ -0,0 +1,64 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+ !/devtools/client/shared/test/test-actor.js
+ head.js
+ network_document.html
+ early_console_document.html
+ fission_document.html
+ fission_iframe.html
+ service-worker-sources.js
+ sources.html
+ sources.js
+ style_document.css
+ style_document.html
+ style_iframe.css
+ style_iframe.html
+ test_service_worker.js
+ test_sw_page.html
+ test_sw_page_worker.js
+ test_worker.js
+ websocket_backend_wsh.py
+ websocket_frontend.html
+ worker-sources.js
+
+[browser_browser_resources_console_messages.js]
+[browser_resources_client_caching.js]
+[browser_resources_console_messages.js]
+[browser_resources_console_messages_workers.js]
+[browser_resources_css_changes.js]
+[browser_resources_css_messages.js]
+[browser_resources_document_events.js]
+[browser_resources_error_messages.js]
+[browser_resources_getAllResources.js]
+[browser_resources_network_event_stacktraces.js]
+[browser_resources_network_events.js]
+[browser_resources_platform_messages.js]
+[browser_resources_root_node.js]
+[browser_resources_several_resources.js]
+[browser_resources_sources.js]
+[browser_resources_stylesheets.js]
+skip-if = fission # Disable frequent fission intermittents Bug 1675020
+[browser_resources_target_destroy.js]
+[browser_resources_target_resources_race.js]
+[browser_resources_target_switching.js]
+[browser_resources_websocket.js]
+[browser_target_list_browser_workers.js]
+[browser_target_list_frames.js]
+[browser_target_list_getAllTargets.js]
+[browser_target_list_preffedoff.js]
+[browser_target_list_processes.js]
+[browser_target_list_service_workers.js]
+[browser_target_list_service_workers_navigation.js]
+skip-if = fission
+# There are several issues to test TargetList navigation scenarios with fission.
+# Without a toolbox linked to the target-list, the target list cannot switch
+# targets. The legacy worker watchers are also not designed to support target
+# switching, since they set this.target = targetList.targetFront just once in
+# their constructor.
+[browser_target_list_switchToTarget.js]
+[browser_target_list_tab_workers.js]
+[browser_target_list_watchTargets.js]
diff --git a/devtools/shared/resources/tests/browser_browser_resources_console_messages.js b/devtools/shared/resources/tests/browser_browser_resources_console_messages.js
new file mode 100644
index 0000000000..67bd5af635
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_browser_resources_console_messages.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around CONSOLE_MESSAGE for the whole browser
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const TEST_URL = URL_ROOT_SSL + "early_console_document.html";
+
+add_task(async function() {
+ // Enable Multiprocess Browser Toolbox (it's still disabled for non-Nightly builds).
+ await pushPref("devtools.browsertoolbox.fission", true);
+
+ const {
+ client,
+ resourceWatcher,
+ targetList,
+ } = await initMultiProcessResourceWatcher();
+
+ info(
+ "Log some messages *before* calling ResourceWatcher.watchResources in order to " +
+ "assert the behavior of already existing messages."
+ );
+ console.log("foobar");
+
+ info("Wait for existing browser mochitest log");
+ await waitForNextResource(
+ resourceWatcher,
+ ResourceWatcher.TYPES.CONSOLE_MESSAGE,
+ {
+ ignoreExistingResources: false,
+ predicate({ message }) {
+ return message.arguments[0] === "foobar";
+ },
+ }
+ );
+ ok(true, "The existing log was retrieved");
+
+ // We can't use waitForNextResource here as we have to ensure
+ // waiting for watchResource resolution before doing the console log.
+ let resolveMochitestRuntimeLog;
+ const onMochitestRuntimeLog = new Promise(resolve => {
+ resolveMochitestRuntimeLog = resolve;
+ });
+ const onAvailable = resources => {
+ if (
+ resources.some(resource => resource.message.arguments[0] == "foobar2")
+ ) {
+ resourceWatcher.unwatchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ { onAvailable }
+ );
+ resolveMochitestRuntimeLog();
+ }
+ };
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ ignoreExistingResources: true,
+ onAvailable,
+ }
+ );
+ console.log("foobar2");
+
+ info("Wait for runtime browser mochitest log");
+ await onMochitestRuntimeLog;
+ ok(true, "The runtime log was retrieved");
+
+ const onEarlyLog = waitForNextResource(
+ resourceWatcher,
+ ResourceWatcher.TYPES.CONSOLE_MESSAGE,
+ {
+ ignoreExistingResources: true,
+ predicate({ message }) {
+ return message.arguments[0] === "early-page-log";
+ },
+ }
+ );
+ await addTab(TEST_URL);
+ info("Wait for early page log");
+ await onEarlyLog;
+ ok(true, "The early page log was retrieved");
+
+ targetList.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/resources/tests/browser_resources_client_caching.js b/devtools/shared/resources/tests/browser_resources_client_caching.js
new file mode 100644
index 0000000000..b758318a13
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_client_caching.js
@@ -0,0 +1,362 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the cache mechanism of the ResourceWatcher.
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const TEST_URI = "data:text/html;charset=utf-8,Cache Test";
+
+add_task(async function() {
+ info("Test whether multiple listener can get same cached resources");
+
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Add messages as existing resources");
+ const messages = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, messages);
+
+ info("Register first listener");
+ const cachedResources1 = [];
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => cachedResources1.push(...resources),
+ }
+ );
+
+ info("Register second listener");
+ const cachedResources2 = [];
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => cachedResources2.push(...resources),
+ }
+ );
+
+ assertContents(cachedResources1, messages);
+ assertResources(cachedResources2, cachedResources1);
+
+ await targetList.destroy();
+ await client.close();
+});
+
+add_task(async function() {
+ info(
+ "Test whether the cache is reflecting existing resources and additional resources"
+ );
+
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Add messages as existing resources");
+ const existingMessages = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, existingMessages);
+
+ info("Register first listener to get all available resources");
+ const availableResources = [];
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => availableResources.push(...resources),
+ }
+ );
+
+ info("Add messages as additional resources");
+ const additionalMessages = ["d", "e"];
+ await logMessages(tab.linkedBrowser, additionalMessages);
+
+ info("Wait until onAvailable is called expected times");
+ const allMessages = [...existingMessages, ...additionalMessages];
+ await waitUntil(() => availableResources.length === allMessages.length);
+
+ info("Register second listener to get the cached resources");
+ const cachedResources = [];
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => cachedResources.push(...resources),
+ }
+ );
+
+ assertContents(availableResources, allMessages);
+ assertResources(cachedResources, availableResources);
+
+ await targetList.destroy();
+ await client.close();
+});
+
+add_task(async function() {
+ info("Test whether the cache is cleared when navigation");
+
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Add messages as existing resources");
+ const existingMessages = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, existingMessages);
+
+ info("Register first listener");
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: () => {},
+ }
+ );
+
+ info("Reload the page");
+ const onReloaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.reloadTab(tab);
+ await onReloaded;
+
+ info("Register second listener");
+ const cachedResources = [];
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => cachedResources.push(...resources),
+ }
+ );
+
+ is(cachedResources.length, 0, "The cache in ResourceWatcher is cleared");
+
+ await targetList.destroy();
+ await client.close();
+});
+
+add_task(async function() {
+ info("Test with multiple resource types");
+
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Register first listener to get all available resources");
+ const availableResources = [];
+ await resourceWatcher.watchResources(
+ [
+ ResourceWatcher.TYPES.CONSOLE_MESSAGE,
+ ResourceWatcher.TYPES.ERROR_MESSAGE,
+ ],
+ {
+ onAvailable: resources => availableResources.push(...resources),
+ }
+ );
+
+ info("Add messages as console message");
+ const consoleMessages1 = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, consoleMessages1);
+
+ info("Add message as error message");
+ const errorMessages = ["document.doTheImpossible();"];
+ await triggerErrors(tab.linkedBrowser, errorMessages);
+
+ info("Add messages as console message again");
+ const consoleMessages2 = ["d", "e"];
+ await logMessages(tab.linkedBrowser, consoleMessages2);
+
+ info("Wait until the getting all available resources");
+ const totalResourceCount =
+ consoleMessages1.length + errorMessages.length + consoleMessages2.length;
+ await waitUntil(() => {
+ return availableResources.length === totalResourceCount;
+ });
+
+ info("Register listener to get the cached resources");
+ const cachedResources = [];
+ await resourceWatcher.watchResources(
+ [
+ ResourceWatcher.TYPES.CONSOLE_MESSAGE,
+ ResourceWatcher.TYPES.ERROR_MESSAGE,
+ ],
+ {
+ onAvailable: resources => cachedResources.push(...resources),
+ }
+ );
+
+ assertResources(cachedResources, availableResources);
+
+ await targetList.destroy();
+ await client.close();
+});
+
+add_task(async function() {
+ info("Test multiple listeners with/without ignoreExistingResources");
+ await testIgnoreExistingResources(true);
+ await testIgnoreExistingResources(false);
+});
+
+async function testIgnoreExistingResources(isFirstListenerIgnoreExisting) {
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Add messages as existing resources");
+ const existingMessages = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, existingMessages);
+
+ info("Register first listener");
+ const cachedResources1 = [];
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => cachedResources1.push(...resources),
+ ignoreExistingResources: isFirstListenerIgnoreExisting,
+ }
+ );
+
+ info("Register second listener");
+ const cachedResources2 = [];
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => cachedResources2.push(...resources),
+ ignoreExistingResources: !isFirstListenerIgnoreExisting,
+ }
+ );
+
+ const cachedResourcesWithFlag = isFirstListenerIgnoreExisting
+ ? cachedResources1
+ : cachedResources2;
+ const cachedResourcesWithoutFlag = isFirstListenerIgnoreExisting
+ ? cachedResources2
+ : cachedResources1;
+
+ info("Check the existing resources both listeners got");
+ assertContents(cachedResourcesWithFlag, []);
+ assertContents(cachedResourcesWithoutFlag, existingMessages);
+
+ info("Add messages as additional resources");
+ const additionalMessages = ["d", "e"];
+ await logMessages(tab.linkedBrowser, additionalMessages);
+
+ info("Wait until onAvailable is called expected times");
+ await waitUntil(
+ () => cachedResourcesWithFlag.length === additionalMessages.length
+ );
+ const allMessages = [...existingMessages, ...additionalMessages];
+ await waitUntil(
+ () => cachedResourcesWithoutFlag.length === allMessages.length
+ );
+
+ info("Check the resources after adding messages");
+ assertContents(cachedResourcesWithFlag, additionalMessages);
+ assertContents(cachedResourcesWithoutFlag, allMessages);
+
+ await targetList.destroy();
+ await client.close();
+}
+
+add_task(async function() {
+ info("Test that onAvailable is not called with an empty resources array");
+
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Register first listener to get all available resources");
+ const availableResources = [];
+ let onAvailableCallCount = 0;
+ const onAvailable = resources => {
+ ok(
+ resources.length > 0,
+ "onAvailable is called with a non empty resources array"
+ );
+ availableResources.push(...resources);
+ onAvailableCallCount++;
+ };
+
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ { onAvailable }
+ );
+ is(availableResources.length, 0, "availableResources array is empty");
+ is(onAvailableCallCount, 0, "onAvailable was never called");
+
+ info("Add messages as console message");
+ await logMessages(tab.linkedBrowser, ["expected message"]);
+
+ await waitUntil(() => availableResources.length === 1);
+ is(availableResources.length, 1, "availableResources array has one item");
+ is(onAvailableCallCount, 1, "onAvailable was called only once");
+ is(
+ availableResources[0].message.arguments[0],
+ "expected message",
+ "onAvailable was called with the expected resource"
+ );
+
+ resourceWatcher.unwatchResources([ResourceWatcher.TYPES.CONSOLE_MESSAGE], {
+ onAvailable,
+ });
+ await targetList.destroy();
+ await client.close();
+});
+
+function assertContents(resources, expectedMessages) {
+ is(
+ resources.length,
+ expectedMessages.length,
+ "Number of the resources is correct"
+ );
+
+ for (let i = 0; i < expectedMessages.length; i++) {
+ const resource = resources[i];
+ const message = resource.message.arguments[0];
+ const expectedMessage = expectedMessages[i];
+ is(message, expectedMessage, `The ${i}th content is correct`);
+ }
+}
+
+function assertResources(resources, expectedResources) {
+ is(
+ resources.length,
+ expectedResources.length,
+ "Number of the resources is correct"
+ );
+
+ for (let i = 0; i < resources.length; i++) {
+ const resource = resources[i];
+ const expectedResource = expectedResources[i];
+ ok(resource === expectedResource, `The ${i}th resource is correct`);
+ }
+}
+
+function logMessages(browser, messages) {
+ return ContentTask.spawn(browser, { messages }, args => {
+ for (const message of args.messages) {
+ content.console.log(message);
+ }
+ });
+}
+
+async function triggerErrors(browser, errorScripts) {
+ for (const errorScript of errorScripts) {
+ await ContentTask.spawn(browser, errorScript, expr => {
+ const document = content.document;
+ const container = document.createElement("script");
+ document.body.appendChild(container);
+ container.textContent = expr;
+ container.remove();
+ });
+ }
+}
diff --git a/devtools/shared/resources/tests/browser_resources_console_messages.js b/devtools/shared/resources/tests/browser_resources_console_messages.js
new file mode 100644
index 0000000000..1be72427b1
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_console_messages.js
@@ -0,0 +1,460 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around CONSOLE_MESSAGE
+//
+// Reproduces assertions from: devtools/shared/webconsole/test/chrome/test_cached_messages.html
+// And now more. Once we remove the console actor's startListeners in favor of watcher class
+// We could remove that other old test.
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const IFRAME_URL = URL_ROOT_ORG_SSL + "fission_iframe.html";
+
+add_task(async function() {
+ info("Execute test in top level document");
+ await testTabConsoleMessagesResources(false);
+ await testTabConsoleMessagesResourcesWithIgnoreExistingResources(false);
+
+ info("Execute test in an iframe document, possibly remote with fission");
+ await testTabConsoleMessagesResources(true);
+ await testTabConsoleMessagesResourcesWithIgnoreExistingResources(true);
+});
+
+async function testTabConsoleMessagesResources(executeInIframe) {
+ const tab = await addTab(FISSION_TEST_URL);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info(
+ "Log some messages *before* calling ResourceWatcher.watchResources in order to " +
+ "assert the behavior of already existing messages."
+ );
+ await logExistingMessages(tab.linkedBrowser, executeInIframe);
+
+ const targetDocumentUrl = executeInIframe ? IFRAME_URL : FISSION_TEST_URL;
+
+ let runtimeDoneResolve;
+ const expectedExistingCalls = getExpectedExistingConsoleCalls(
+ targetDocumentUrl
+ );
+ const expectedRuntimeCalls = getExpectedRuntimeConsoleCalls(
+ targetDocumentUrl
+ );
+ const onRuntimeDone = new Promise(resolve => (runtimeDoneResolve = resolve));
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ if (resource.message.arguments?.[0] === "[WORKER] started") {
+ // XXX Ignore message from workers as we can't know when they're logged, and we
+ // have a dedicated test for them (browser_resources_console_messages_workers.js).
+ continue;
+ }
+
+ is(
+ resource.resourceType,
+ ResourceWatcher.TYPES.CONSOLE_MESSAGE,
+ "Received a message"
+ );
+ ok(resource.message, "message is wrapped into a message attribute");
+ const expected = (expectedExistingCalls.length > 0
+ ? expectedExistingCalls
+ : expectedRuntimeCalls
+ ).shift();
+ checkConsoleAPICall(resource.message, expected);
+ if (expectedRuntimeCalls.length == 0) {
+ runtimeDoneResolve();
+ }
+ }
+ };
+
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable,
+ }
+ );
+ is(
+ expectedExistingCalls.length,
+ 0,
+ "Got the expected number of existing messages"
+ );
+
+ info(
+ "Now log messages *after* the call to ResourceWatcher.watchResources and after having received all existing messages"
+ );
+ await logRuntimeMessages(tab.linkedBrowser, executeInIframe);
+
+ info("Waiting for all runtime messages");
+ await onRuntimeDone;
+
+ is(
+ expectedRuntimeCalls.length,
+ 0,
+ "Got the expected number of runtime messages"
+ );
+
+ targetList.destroy();
+ await client.close();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+}
+
+async function testTabConsoleMessagesResourcesWithIgnoreExistingResources(
+ executeInIframe
+) {
+ info("Test ignoreExistingResources option for console messages");
+ const tab = await addTab(FISSION_TEST_URL);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info(
+ "Check whether onAvailable will not be called with existing console messages"
+ );
+ await logExistingMessages(tab.linkedBrowser, executeInIframe);
+
+ const availableResources = [];
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: resources => availableResources.push(...resources),
+ ignoreExistingResources: true,
+ }
+ );
+ is(
+ availableResources.length,
+ 0,
+ "onAvailable wasn't called for existing console messages"
+ );
+
+ info(
+ "Check whether onAvailable will be called with the future console messages"
+ );
+ await logRuntimeMessages(tab.linkedBrowser, executeInIframe);
+ const targetDocumentUrl = executeInIframe ? IFRAME_URL : FISSION_TEST_URL;
+ const expectedRuntimeConsoleCalls = getExpectedRuntimeConsoleCalls(
+ targetDocumentUrl
+ );
+ await waitUntil(
+ () => availableResources.length === expectedRuntimeConsoleCalls.length
+ );
+ const expectedTargetFront =
+ executeInIframe && isFissionEnabled()
+ ? targetList
+ .getAllTargets([targetList.TYPES.FRAME])
+ .find(target => target.url == IFRAME_URL)
+ : targetList.targetFront;
+ for (let i = 0; i < expectedRuntimeConsoleCalls.length; i++) {
+ const { message, targetFront } = availableResources[i];
+ is(
+ targetFront,
+ expectedTargetFront,
+ "The targetFront property is the expected one"
+ );
+ const expected = expectedRuntimeConsoleCalls[i];
+ checkConsoleAPICall(message, expected);
+ }
+
+ await targetList.destroy();
+ await client.close();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+}
+
+async function logExistingMessages(browser, executeInIframe) {
+ let browsingContext = browser.browsingContext;
+ if (executeInIframe) {
+ browsingContext = await SpecialPowers.spawn(
+ browser,
+ [],
+ function frameScript() {
+ return content.document.querySelector("iframe").browsingContext;
+ }
+ );
+ }
+ return evalInBrowsingContext(browsingContext, function pageScript() {
+ console.log("foobarBaz-log", undefined);
+ console.info("foobarBaz-info", null);
+ console.warn("foobarBaz-warn", document.body);
+ });
+}
+
+/**
+ * Helper function similar to spawn, but instead of executing the script
+ * as a Frame Script, with privileges and including test harness in stacktraces,
+ * execute the script as a regular page script, without privileges and without any
+ * preceding stack.
+ *
+ * @param {BrowsingContext} The browsing context into which the script should be evaluated
+ * @param {Function|String} The JS to execute in the browsing context
+ *
+ * @return {Promise} Which resolves once the JS is done executing in the page
+ */
+function evalInBrowsingContext(browsingContext, script) {
+ return SpecialPowers.spawn(browsingContext, [String(script)], expr => {
+ const document = content.document;
+ const scriptEl = document.createElement("script");
+ document.body.appendChild(scriptEl);
+ // Force the immediate execution of the stringified JS function passed in `expr`
+ scriptEl.textContent = "new " + expr;
+ scriptEl.remove();
+ });
+}
+
+// For both existing and runtime messages, we execute console API
+// from a page script evaluated via evalInBrowsingContext.
+// Records here the function used to execute the script in the page.
+const EXPECTED_FUNCTION_NAME = "pageScript";
+
+const NUMBER_REGEX = /^\d+$/;
+
+function getExpectedExistingConsoleCalls(documentFilename) {
+ return [
+ {
+ level: "log",
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ timeStamp: NUMBER_REGEX,
+ arguments: ["foobarBaz-log", { type: "undefined" }],
+ },
+ {
+ level: "info",
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ timeStamp: NUMBER_REGEX,
+ arguments: ["foobarBaz-info", { type: "null" }],
+ },
+ {
+ level: "warn",
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ timeStamp: NUMBER_REGEX,
+ arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }],
+ },
+ ];
+}
+
+const longString = new Array(DevToolsServer.LONG_STRING_LENGTH + 2).join("a");
+function getExpectedRuntimeConsoleCalls(documentFilename) {
+ const defaultStackFrames = [
+ // This is the usage of "new " + expr from `evalInBrowsingContext`
+ {
+ filename: documentFilename,
+ lineNumber: 1,
+ columnNumber: NUMBER_REGEX,
+ },
+ ];
+
+ return [
+ {
+ level: "log",
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ timeStamp: NUMBER_REGEX,
+ arguments: ["foobarBaz-log", { type: "undefined" }],
+ },
+ {
+ level: "log",
+ arguments: ["Float from not a number: NaN"],
+ },
+ {
+ level: "log",
+ arguments: ["Float from string: 1.200000"],
+ },
+ {
+ level: "log",
+ arguments: ["Float from number: 1.300000"],
+ },
+ {
+ level: "info",
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ timeStamp: NUMBER_REGEX,
+ arguments: ["foobarBaz-info", { type: "null" }],
+ },
+ {
+ level: "warn",
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ timeStamp: NUMBER_REGEX,
+ arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }],
+ },
+ {
+ level: "debug",
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ timeStamp: NUMBER_REGEX,
+ arguments: [{ type: "null" }],
+ },
+ {
+ level: "trace",
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ timeStamp: NUMBER_REGEX,
+ stacktrace: [
+ {
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ },
+ ...defaultStackFrames,
+ ],
+ },
+ {
+ level: "dir",
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ timeStamp: NUMBER_REGEX,
+ arguments: [
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "HTMLDocument",
+ },
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "Location",
+ },
+ ],
+ },
+ {
+ level: "log",
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ timeStamp: NUMBER_REGEX,
+ arguments: [
+ "foo",
+ {
+ type: "longString",
+ initial: longString.substring(
+ 0,
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH
+ ),
+ length: longString.length,
+ actor: /[a-z]/,
+ },
+ ],
+ },
+ {
+ level: "error",
+ filename: documentFilename,
+ functionName: "fromAsmJS",
+ timeStamp: NUMBER_REGEX,
+ arguments: ["foobarBaz-asmjs-error", { type: "undefined" }],
+
+ stacktrace: [
+ {
+ filename: documentFilename,
+ functionName: "fromAsmJS",
+ },
+ {
+ filename: documentFilename,
+ functionName: "inAsmJS2",
+ },
+ {
+ filename: documentFilename,
+ functionName: "inAsmJS1",
+ },
+ {
+ filename: documentFilename,
+ functionName: EXPECTED_FUNCTION_NAME,
+ },
+ ...defaultStackFrames,
+ ],
+ },
+ {
+ level: "log",
+ filename: gTestPath,
+ functionName: "frameScript",
+ timeStamp: NUMBER_REGEX,
+ arguments: [
+ {
+ type: "object",
+ actor: /[a-z]/,
+ class: "Restricted",
+ },
+ ],
+ },
+ ];
+}
+
+async function logRuntimeMessages(browser, executeInIframe) {
+ let browsingContext = browser.browsingContext;
+ if (executeInIframe) {
+ browsingContext = await SpecialPowers.spawn(
+ browser,
+ [],
+ function frameScript() {
+ return content.document.querySelector("iframe").browsingContext;
+ }
+ );
+ }
+ // First inject LONG_STRING_LENGTH in global scope it order to easily use it after
+ await evalInBrowsingContext(
+ browsingContext,
+ `function () {window.LONG_STRING_LENGTH = ${DevToolsServer.LONG_STRING_LENGTH};}`
+ );
+ await evalInBrowsingContext(browsingContext, function pageScript() {
+ const _longString = new Array(window.LONG_STRING_LENGTH + 2).join("a");
+
+ console.log("foobarBaz-log", undefined);
+
+ console.log("Float from not a number: %f", "foo");
+ console.log("Float from string: %f", "1.2");
+ console.log("Float from number: %f", 1.3);
+
+ console.info("foobarBaz-info", null);
+ console.warn("foobarBaz-warn", document.documentElement);
+ console.debug(null);
+ console.trace();
+ console.dir(document, location);
+ console.log("foo", _longString);
+
+ function fromAsmJS() {
+ console.error("foobarBaz-asmjs-error", undefined);
+ }
+
+ (function(global, foreign) {
+ "use asm";
+ function inAsmJS2() {
+ foreign.fromAsmJS();
+ }
+ function inAsmJS1() {
+ inAsmJS2();
+ }
+ return inAsmJS1;
+ })(null, { fromAsmJS: fromAsmJS })();
+ });
+ await SpecialPowers.spawn(browsingContext, [], function frameScript() {
+ const sandbox = new Cu.Sandbox(null, { invisibleToDebugger: true });
+ const sandboxObj = sandbox.eval("new Object");
+ content.console.log(sandboxObj);
+ });
+}
+
+// Copied from devtools/shared/webconsole/test/chrome/common.js
+function checkConsoleAPICall(call, expected) {
+ is(
+ call.arguments?.length || 0,
+ expected.arguments?.length || 0,
+ "number of arguments"
+ );
+
+ checkObject(call, expected);
+}
diff --git a/devtools/shared/resources/tests/browser_resources_console_messages_workers.js b/devtools/shared/resources/tests/browser_resources_console_messages_workers.js
new file mode 100644
index 0000000000..a31388bc32
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_console_messages_workers.js
@@ -0,0 +1,246 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around CONSOLE_MESSAGE in workers
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const WORKER_FILE = "test_worker.js";
+const IFRAME_FILE = `${URL_ROOT_ORG_SSL}fission_iframe.html`;
+
+add_task(async function() {
+ // Set the following pref to false as it's the one that enables direct connection
+ // to the worker targets
+ await pushPref("dom.worker.console.dispatch_events_to_main_thread", false);
+
+ const tab = await addTab(FISSION_TEST_URL);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab,
+ { listenForWorkers: true }
+ );
+
+ info("Wait for the workers (from the main page and the iframe) to be ready");
+ const targets = [];
+ await new Promise(resolve => {
+ const onAvailable = async ({ targetFront }) => {
+ targets.push(targetFront);
+ if (targets.length === 2) {
+ resolve();
+ }
+ };
+ targetList.watchTargets([targetList.TYPES.WORKER], onAvailable);
+ });
+
+ // The worker logs a message right when it starts, containing its location, so we can
+ // assert that we get the logs from the worker spawned in the content page and from the
+ // worker spawned in the iframe.
+ info("Check that we receive the cached messages");
+
+ const resources = [];
+ const onAvailable = innerResources => {
+ for (const resource of innerResources) {
+ // Ignore resources from non worker targets
+ if (!resource.targetFront.isWorkerTarget) {
+ continue;
+ }
+
+ resources.push(resource);
+ }
+ };
+
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable,
+ }
+ );
+
+ is(resources.length, 2, "Got the expected number of existing messages");
+ const startLogFromWorkerInMainPage = resources.find(
+ ({ message }) =>
+ message.arguments[1] === `${URL_ROOT_SSL}${WORKER_FILE}#simple-worker`
+ );
+ const startLogFromWorkerInIframe = resources.find(
+ ({ message }) =>
+ message.arguments[1] ===
+ `${URL_ROOT_ORG_SSL}${WORKER_FILE}#simple-worker-in-iframe`
+ );
+
+ checkStartWorkerLogMessage(
+ startLogFromWorkerInMainPage,
+ `${URL_ROOT_SSL}${WORKER_FILE}#simple-worker`
+ );
+ checkStartWorkerLogMessage(
+ startLogFromWorkerInIframe,
+ `${URL_ROOT_ORG_SSL}${WORKER_FILE}#simple-worker-in-iframe`
+ );
+ let messageCount = resources.length;
+
+ info(
+ "Now log messages *after* the call to ResourceWatcher.watchResources and after having received all existing messages"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.wrappedJSObject.logMessageInWorker("live message from main page");
+
+ const iframe = content.document.querySelector("iframe");
+ SpecialPowers.spawn(iframe, [], () => {
+ content.wrappedJSObject.logMessageInWorker("live message from iframe");
+ });
+ });
+
+ // Wait until the 2 new logs are available
+ await waitUntil(() => resources.length === messageCount + 2);
+ const liveMessageFromWorkerInMainPage = resources.find(
+ ({ message }) => message.arguments[1] === "live message from main page"
+ );
+ const liveMessageFromWorkerInIframe = resources.find(
+ ({ message }) => message.arguments[1] === "live message from iframe"
+ );
+
+ checkLogInWorkerMessage(
+ liveMessageFromWorkerInMainPage,
+ "live message from main page"
+ );
+
+ checkLogInWorkerMessage(
+ liveMessageFromWorkerInIframe,
+ "live message from iframe"
+ );
+
+ // update the current number of resources received
+ messageCount = resources.length;
+
+ info("Now spawn new workers and log messages in main page and iframe");
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [WORKER_FILE],
+ async workerUrl => {
+ const spawnedWorker = new content.Worker(`${workerUrl}#spawned-worker`);
+ spawnedWorker.postMessage({
+ type: "log-in-worker",
+ message: "live message in spawned worker from main page",
+ });
+
+ const iframe = content.document.querySelector("iframe");
+ SpecialPowers.spawn(iframe, [workerUrl], async innerWorkerUrl => {
+ const spawnedWorkerInIframe = new content.Worker(
+ `${innerWorkerUrl}#spawned-worker-in-iframe`
+ );
+ spawnedWorkerInIframe.postMessage({
+ type: "log-in-worker",
+ message: "live message in spawned worker from iframe",
+ });
+ });
+ }
+ );
+
+ info(
+ "Wait until the 4 new logs are available (the ones logged at worker creation + the ones from postMessage"
+ );
+ await waitUntil(
+ () => resources.length === messageCount + 4,
+ `Couldn't get the expected number of resources (expected ${messageCount +
+ 4}, got ${resources.length})`
+ );
+ const startLogFromSpawnedWorkerInMainPage = resources.find(
+ ({ message }) =>
+ message.arguments[1] === `${URL_ROOT_SSL}${WORKER_FILE}#spawned-worker`
+ );
+ const startLogFromSpawnedWorkerInIframe = resources.find(
+ ({ message }) =>
+ message.arguments[1] ===
+ `${URL_ROOT_ORG_SSL}${WORKER_FILE}#spawned-worker-in-iframe`
+ );
+ const liveMessageFromSpawnedWorkerInMainPage = resources.find(
+ ({ message }) =>
+ message.arguments[1] === "live message in spawned worker from main page"
+ );
+ const liveMessageFromSpawnedWorkerInIframe = resources.find(
+ ({ message }) =>
+ message.arguments[1] === "live message in spawned worker from iframe"
+ );
+
+ checkStartWorkerLogMessage(
+ startLogFromSpawnedWorkerInMainPage,
+ `${URL_ROOT_SSL}${WORKER_FILE}#spawned-worker`
+ );
+ checkStartWorkerLogMessage(
+ startLogFromSpawnedWorkerInIframe,
+ `${URL_ROOT_ORG_SSL}${WORKER_FILE}#spawned-worker-in-iframe`
+ );
+ checkLogInWorkerMessage(
+ liveMessageFromSpawnedWorkerInMainPage,
+ "live message in spawned worker from main page"
+ );
+ checkLogInWorkerMessage(
+ liveMessageFromSpawnedWorkerInIframe,
+ "live message in spawned worker from iframe"
+ );
+ // update the current number of resources received
+ messageCount = resources.length;
+
+ info(
+ "Add a remote iframe on the same origin we already have an iframe and check we get the messages"
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [IFRAME_FILE],
+ async iframeUrl => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = `${iframeUrl}?hashSuffix=in-second-iframe`;
+ content.document.body.append(iframe);
+ }
+ );
+
+ info("Wait until the new log is available");
+ await waitUntil(
+ () => resources.length === messageCount + 1,
+ `Couldn't get the expected number of resources (expected ${messageCount +
+ 1}, got ${resources.length})`
+ );
+ const startLogFromWorkerInSecondIframe = resources[resources.length - 1];
+ checkStartWorkerLogMessage(
+ startLogFromWorkerInSecondIframe,
+ `${URL_ROOT_ORG_SSL}${WORKER_FILE}#simple-worker-in-second-iframe`
+ );
+
+ targetList.destroy();
+ await client.close();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+});
+
+function checkStartWorkerLogMessage(resource, expectedUrl) {
+ const { message } = resource;
+ const [firstArg, secondArg, thirdArg] = message.arguments;
+ is(firstArg, "[WORKER] started", "Got the expected first argument");
+ is(secondArg, expectedUrl, "expected url was logged");
+ is(
+ thirdArg?._grip?.class,
+ "DedicatedWorkerGlobalScope",
+ "the global scope was logged as expected"
+ );
+}
+
+function checkLogInWorkerMessage(resource, expectedMessage) {
+ const { message } = resource;
+ const [firstArg, secondArg, thirdArg] = message.arguments;
+ is(firstArg, "[WORKER]", "Got the expected first argument");
+ is(secondArg, expectedMessage, "expected message was logged");
+ is(
+ thirdArg?._grip?.class,
+ "MessageEvent",
+ "the message event object was logged as expected"
+ );
+}
diff --git a/devtools/shared/resources/tests/browser_resources_css_changes.js b/devtools/shared/resources/tests/browser_resources_css_changes.js
new file mode 100644
index 0000000000..d81953e75e
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_css_changes.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around CSS_CHANGE.
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+add_task(async function() {
+ // Open a test tab
+ const tab = await addTab(
+ "data:text/html,<body style='color: lime;'>CSS Changes</body>"
+ );
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ // CSS_CHANGE watcher doesn't record modification made before watching,
+ // so we have to start watching before doing any DOM mutation.
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.CSS_CHANGE], {
+ onAvailable: () => {},
+ });
+
+ const { walker } = await targetList.targetFront.getFront("inspector");
+ const nodeList = await walker.querySelectorAll(walker.rootNode, "body");
+ const body = (await nodeList.items())[0];
+ const style = (
+ await body.inspectorFront.pageStyle.getApplied(body, {
+ skipPseudo: false,
+ })
+ )[0];
+
+ info(
+ "Check whether ResourceWatcher catches CSS change that fired before starting to watch"
+ );
+ await setProperty(style.rule, 0, "color", "black");
+
+ const availableResources = [];
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.CSS_CHANGE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+ assertResource(
+ availableResources[0],
+ { index: 0, property: "color", value: "black" },
+ { index: 0, property: "color", value: "lime" }
+ );
+
+ info(
+ "Check whether ResourceWatcher catches CSS change after the property changed"
+ );
+ await setProperty(style.rule, 0, "background-color", "pink");
+ await waitUntil(() => availableResources.length === 2);
+ assertResource(
+ availableResources[1],
+ { index: 0, property: "background-color", value: "pink" },
+ { index: 0, property: "color", value: "black" }
+ );
+
+ info("Check whether ResourceWatcher catches CSS change of disabling");
+ await setPropertyEnabled(style.rule, 0, "background-color", false);
+ await waitUntil(() => availableResources.length === 3);
+ assertResource(availableResources[2], null, {
+ index: 0,
+ property: "background-color",
+ value: "pink",
+ });
+
+ info("Check whether ResourceWatcher catches CSS change of new property");
+ await createProperty(style.rule, 1, "font-size", "100px");
+ await waitUntil(() => availableResources.length === 4);
+ assertResource(
+ availableResources[3],
+ { index: 1, property: "font-size", value: "100px" },
+ null
+ );
+
+ info("Check whether ResourceWatcher sends all resources added in this test");
+ const existingResources = [];
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.CSS_CHANGE], {
+ onAvailable: resources => existingResources.push(...resources),
+ });
+ await waitUntil(() => existingResources.length === 4);
+ is(availableResources[0], existingResources[0], "1st resource is correct");
+ is(availableResources[1], existingResources[1], "2nd resource is correct");
+ is(availableResources[2], existingResources[2], "3rd resource is correct");
+ is(availableResources[3], existingResources[3], "4th resource is correct");
+
+ await targetList.destroy();
+ await client.close();
+});
+
+function assertResource(resource, expectedAddedChange, expectedRemovedChange) {
+ if (expectedAddedChange) {
+ is(resource.add.length, 1, "The number of added changes is correct");
+ assertChange(resource.add[0], expectedAddedChange);
+ } else {
+ is(resource.add, null, "There is no added changes");
+ }
+
+ if (expectedRemovedChange) {
+ is(resource.remove.length, 1, "The number of removed changes is correct");
+ assertChange(resource.remove[0], expectedRemovedChange);
+ } else {
+ is(resource.remove, null, "There is no removed changes");
+ }
+}
+
+function assertChange(change, expected) {
+ is(change.index, expected.index, "The index of change is correct");
+ is(change.property, expected.property, "The property of change is correct");
+ is(change.value, expected.value, "The value of change is correct");
+}
+
+async function setProperty(rule, index, property, value) {
+ const modifications = rule.startModifyingProperties({ isKnown: true });
+ modifications.setProperty(index, property, value, "");
+ await modifications.apply();
+}
+
+async function createProperty(rule, index, property, value) {
+ const modifications = rule.startModifyingProperties({ isKnown: true });
+ modifications.createProperty(index, property, value, "", true);
+ await modifications.apply();
+}
+
+async function setPropertyEnabled(rule, index, property, isEnabled) {
+ const modifications = rule.startModifyingProperties({ isKnown: true });
+ modifications.setPropertyEnabled(index, property, isEnabled);
+ await modifications.apply();
+}
diff --git a/devtools/shared/resources/tests/browser_resources_css_messages.js b/devtools/shared/resources/tests/browser_resources_css_messages.js
new file mode 100644
index 0000000000..0e82bacd57
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_css_messages.js
@@ -0,0 +1,202 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around CSS_MESSAGE
+// Reproduces the CSS message assertions from devtools/shared/webconsole/test/chrome/test_page_errors.html
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+const { MESSAGE_CATEGORY } = require("devtools/shared/constants");
+
+// Create a simple server so we have a nice sourceName in the resources packets.
+const httpServer = createTestHTTPServer();
+httpServer.registerPathHandler(`/test_css_messages.html`, (req, res) => {
+ res.setStatusLine(req.httpVersion, 200, "OK");
+ res.write(`<meta charset=utf8>
+ <style>
+ html {
+ color: bloup;
+ }
+ </style>Test CSS Messages`);
+});
+
+const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/test_css_messages.html`;
+
+add_task(async function() {
+ await testWatchingCssMessages();
+ await testWatchingCachedCssMessages();
+});
+
+async function testWatchingCssMessages() {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ // Open a test tab
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ const receivedMessages = [];
+ const { onAvailable, onAllMessagesReceived } = setupOnAvailableFunction(
+ targetList,
+ receivedMessages
+ );
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.CSS_MESSAGE], {
+ onAvailable,
+ });
+
+ info(
+ "Now log CSS warning *after* the call to ResourceWatcher.watchResources and after " +
+ "having received the existing message"
+ );
+ // We need to wait for the first CSS Warning as it is not a cached message; when we
+ // start watching, the `cssErrorReportingEnabled` is checked on the target docShell, and
+ // if it is false, we re-parse the stylesheets to get the messages.
+ await BrowserTestUtils.waitForCondition(() => receivedMessages.length === 1);
+
+ info("Trigger a CSS Warning");
+ triggerCSSWarning(tab);
+
+ info("Waiting for all expected CSS messages to be received");
+ await onAllMessagesReceived;
+ ok(true, "All the expected CSS messages were received");
+
+ Services.console.reset();
+ targetList.destroy();
+ await client.close();
+}
+
+async function testWatchingCachedCssMessages() {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ // Open a test tab
+ const tab = await addTab(TEST_URI);
+
+ // By default, the CSS Parser does not emit warnings at all, for performance matter.
+ // Since we actually want the Parser to emit those messages _before_ we start listening
+ // for CSS messages, we need to set the cssErrorReportingEnabled flag on the docShell.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() {
+ content.docShell.cssErrorReportingEnabled = true;
+ });
+
+ // Setting the docShell flag only indicates to the Parser that from now on, it should
+ // emit warnings. But it does not automatically emit warnings for the existing CSS
+ // errors in the stylesheets. So here we reload the tab, which will make the Parser
+ // parse the stylesheets again, this time emitting warnings.
+ const loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ tab.linkedBrowser.reload();
+ // wait for the tab to be fully loaded
+ await loaded;
+ // and trigger more CSS warnings
+ await triggerCSSWarning(tab);
+
+ // At this point, all messages should be in the ConsoleService cache, and we can begin
+ // to watch and check that we do retrieve those messages.
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ const receivedMessages = [];
+ const { onAvailable } = setupOnAvailableFunction(
+ targetList,
+ receivedMessages
+ );
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.CSS_MESSAGE], {
+ onAvailable,
+ });
+ is(receivedMessages.length, 3, "Cached messages were retrieved as expected");
+
+ Services.console.reset();
+ targetList.destroy();
+ await client.close();
+}
+
+function setupOnAvailableFunction(targetList, receivedMessages) {
+ // The expected messages are the CSS warnings:
+ // - one for the rule in the style element
+ // - two for the JS modified style we're doing in the test.
+ const expectedMessages = [
+ {
+ pageError: {
+ errorMessage: /Expected color but found ‘bloup’/,
+ sourceName: /test_css_messages/,
+ category: MESSAGE_CATEGORY.CSS_PARSER,
+ timeStamp: /^\d+$/,
+ error: false,
+ warning: true,
+ },
+ cssSelectors: "html",
+ },
+ {
+ pageError: {
+ errorMessage: /Error in parsing value for ‘width’/,
+ sourceName: /test_css_messages/,
+ category: MESSAGE_CATEGORY.CSS_PARSER,
+ timeStamp: /^\d+$/,
+ error: false,
+ warning: true,
+ },
+ },
+ {
+ pageError: {
+ errorMessage: /Error in parsing value for ‘height’/,
+ sourceName: /test_css_messages/,
+ category: MESSAGE_CATEGORY.CSS_PARSER,
+ timeStamp: /^\d+$/,
+ error: false,
+ warning: true,
+ },
+ },
+ ];
+
+ let done;
+ const onAllMessagesReceived = new Promise(resolve => (done = resolve));
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ const { pageError } = resource;
+
+ is(
+ resource.targetFront,
+ targetList.targetFront,
+ "The targetFront property is the expected one"
+ );
+
+ if (!pageError.sourceName.includes("test_css_messages")) {
+ info(`Ignore error from unknown source: "${pageError.sourceName}"`);
+ continue;
+ }
+
+ const index = receivedMessages.length;
+ receivedMessages.push(pageError);
+
+ info(
+ `checking received css message #${index}: ${pageError.errorMessage}`
+ );
+ ok(pageError, "The resource has a pageError attribute");
+ checkObject(resource, expectedMessages[index]);
+
+ if (receivedMessages.length == expectedMessages.length) {
+ done();
+ }
+ }
+ };
+ return { onAvailable, onAllMessagesReceived };
+}
+
+/**
+ * Sets invalid values for width and height on the document's body style attribute.
+ */
+function triggerCSSWarning(tab) {
+ return ContentTask.spawn(tab.linkedBrowser, null, function frameScript() {
+ content.document.body.style.width = "red";
+ content.document.body.style.height = "blue";
+ });
+}
diff --git a/devtools/shared/resources/tests/browser_resources_document_events.js b/devtools/shared/resources/tests/browser_resources_document_events.js
new file mode 100644
index 0000000000..bf6a66ed40
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_document_events.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around DOCUMENT_EVENT
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+add_task(async function() {
+ await testDocumentEventResources();
+ await testDocumentEventResourcesWithIgnoreExistingResources();
+});
+
+async function testDocumentEventResources() {
+ info("Test ResourceWatcher for DOCUMENT_EVENT");
+
+ // Open a test tab
+ const tab = await addTab("data:text/html,Document Events");
+
+ // Create a TargetList for the test tab
+ const client = await createLocalClient();
+ const descriptor = await client.mainRoot.getTab({ tab });
+ const target = await descriptor.getTarget();
+ const targetList = new TargetList(client.mainRoot, target);
+ await targetList.startListening();
+
+ // Activate ResourceWatcher
+ const listener = new ResourceListener();
+ const resourceWatcher = new ResourceWatcher(targetList);
+
+ info(
+ "Check whether the document events are fired correctly even when the document was already loaded"
+ );
+ const onLoadingAtInit = listener.once("dom-loading");
+ const onInteractiveAtInit = listener.once("dom-interactive");
+ const onCompleteAtInit = listener.once("dom-complete");
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.DOCUMENT_EVENT], {
+ onAvailable: parameters => listener.dispatch(parameters),
+ });
+ await assertPromises(onLoadingAtInit, onInteractiveAtInit, onCompleteAtInit);
+ ok(
+ true,
+ "Document events are fired even when the document was already loaded"
+ );
+
+ info("Check whether the document events are fired correctly when reloading");
+ const onLoadingAtReloaded = listener.once("dom-loading");
+ const onInteractiveAtReloaded = listener.once("dom-interactive");
+ const onCompleteAtReloaded = listener.once("dom-complete");
+ gBrowser.reloadTab(tab);
+ await assertPromises(
+ onLoadingAtReloaded,
+ onInteractiveAtReloaded,
+ onCompleteAtReloaded
+ );
+ ok(true, "Document events are fired after reloading");
+
+ await targetList.destroy();
+ await client.close();
+}
+
+async function testDocumentEventResourcesWithIgnoreExistingResources() {
+ info("Test ignoreExistingResources option for DOCUMENT_EVENT");
+
+ const tab = await addTab("data:text/html,Document Events");
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Check whether the existing document events will not be fired");
+ const documentEvents = [];
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.DOCUMENT_EVENT], {
+ onAvailable: resources => documentEvents.push(...resources),
+ ignoreExistingResources: true,
+ });
+ is(documentEvents.length, 0, "Existing document events are not fired");
+
+ info("Check whether the future document events are fired");
+ gBrowser.reloadTab(tab);
+ info("Wait for dom-loading, dom-interactive and dom-complete events");
+ await waitUntil(() => documentEvents.length === 3);
+ assertEvents(...documentEvents);
+
+ await targetList.destroy();
+ await client.close();
+}
+
+async function assertPromises(onLoading, onInteractive, onComplete) {
+ const loadingEvent = await onLoading;
+ const interactiveEvent = await onInteractive;
+ const completeEvent = await onComplete;
+ assertEvents(loadingEvent, interactiveEvent, completeEvent);
+}
+
+function assertEvents(loadingEvent, interactiveEvent, completeEvent) {
+ is(
+ typeof loadingEvent.time,
+ "number",
+ "Type of time attribute for loading event is correct"
+ );
+ is(
+ typeof interactiveEvent.time,
+ "number",
+ "Type of time attribute for interactive event is correct"
+ );
+ is(
+ typeof completeEvent.time,
+ "number",
+ "Type of time attribute for complete event is correct"
+ );
+
+ ok(
+ loadingEvent.time < interactiveEvent.time,
+ "Timestamp for interactive event is greater than loading event"
+ );
+ ok(
+ interactiveEvent.time < completeEvent.time,
+ "Timestamp for complete event is greater than interactive event"
+ );
+}
+
+class ResourceListener {
+ _listeners = new Map();
+
+ dispatch(resources) {
+ for (const resource of resources) {
+ const resolve = this._listeners.get(resource.name);
+ if (resolve) {
+ resolve(resource);
+ this._listeners.delete(resource.name);
+ }
+ }
+ }
+
+ once(resourceName) {
+ return new Promise(r => this._listeners.set(resourceName, r));
+ }
+}
diff --git a/devtools/shared/resources/tests/browser_resources_error_messages.js b/devtools/shared/resources/tests/browser_resources_error_messages.js
new file mode 100644
index 0000000000..959a97e87e
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_error_messages.js
@@ -0,0 +1,614 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around ERROR_MESSAGE
+// Reproduces assertions from devtools/shared/webconsole/test/chrome/test_page_errors.html
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+// Create a simple server so we have a nice sourceName in the resources packets.
+const httpServer = createTestHTTPServer();
+httpServer.registerPathHandler(`/test_page_errors.html`, (req, res) => {
+ res.setStatusLine(req.httpVersion, 200, "OK");
+ res.write(`<meta charset=utf8>Test Error Messages`);
+});
+
+const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/test_page_errors.html`;
+
+add_task(async function() {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ await testErrorMessagesResources();
+ await testErrorMessagesResourcesWithIgnoreExistingResources();
+});
+
+async function testErrorMessagesResources() {
+ // Open a test tab
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ const receivedMessages = [];
+ // The expected messages are the errors, twice (once for cached messages, once for live messages)
+ const expectedMessages = Array.from(expectedPageErrors.values()).concat(
+ Array.from(expectedPageErrors.values())
+ );
+
+ info(
+ "Log some errors *before* calling ResourceWatcher.watchResources in order to assert" +
+ " the behavior of already existing messages."
+ );
+ await triggerErrors(tab);
+
+ let done;
+ const onAllErrorReceived = new Promise(resolve => (done = resolve));
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ const { pageError } = resource;
+
+ is(
+ resource.targetFront,
+ targetList.targetFront,
+ "The targetFront property is the expected one"
+ );
+
+ if (!pageError.sourceName.includes("test_page_errors")) {
+ info(`Ignore error from unknown source: "${pageError.sourceName}"`);
+ continue;
+ }
+
+ const index = receivedMessages.length;
+ receivedMessages.push(pageError);
+
+ info(`checking received page error #${index}: ${pageError.errorMessage}`);
+ ok(pageError, "The resource has a pageError attribute");
+ checkPageErrorResource(pageError, expectedMessages[index]);
+
+ if (receivedMessages.length == expectedMessages.length) {
+ done();
+ }
+ }
+ };
+
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.ERROR_MESSAGE], {
+ onAvailable,
+ });
+
+ info(
+ "Now log errors *after* the call to ResourceWatcher.watchResources and after having" +
+ " received all existing messages"
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => receivedMessages.length === expectedPageErrors.size
+ );
+ await triggerErrors(tab);
+
+ info("Waiting for all expected errors to be received");
+ await onAllErrorReceived;
+ ok(true, "All the expected errors were received");
+
+ Services.console.reset();
+ targetList.destroy();
+ await client.close();
+}
+
+async function testErrorMessagesResourcesWithIgnoreExistingResources() {
+ info("Test ignoreExistingResources option for ERROR_MESSAGE");
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info(
+ "Check whether onAvailable will not be called with existing error messages"
+ );
+ await triggerErrors(tab);
+
+ const availableResources = [];
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.ERROR_MESSAGE], {
+ onAvailable: resources => availableResources.push(...resources),
+ ignoreExistingResources: true,
+ });
+ is(
+ availableResources.length,
+ 0,
+ "onAvailable wasn't called for existing error messages"
+ );
+
+ info(
+ "Check whether onAvailable will be called with the future error messages"
+ );
+ await triggerErrors(tab);
+
+ const expectedMessages = Array.from(expectedPageErrors.values());
+ await waitUntil(() => availableResources.length === expectedMessages.length);
+ for (let i = 0; i < expectedMessages.length; i++) {
+ const { pageError } = availableResources[i];
+ const expected = expectedMessages[i];
+ checkPageErrorResource(pageError, expected);
+ }
+
+ Services.console.reset();
+ await targetList.destroy();
+ await client.close();
+}
+
+/**
+ * Triggers all the errors in the content page.
+ */
+async function triggerErrors(tab) {
+ for (const [expression, expected] of expectedPageErrors.entries()) {
+ if (
+ !expected[noUncaughtException] &&
+ !Services.appinfo.browserTabsRemoteAutostart
+ ) {
+ expectUncaughtException();
+ }
+
+ await ContentTask.spawn(tab.linkedBrowser, expression, function frameScript(
+ expr
+ ) {
+ const document = content.document;
+ const scriptEl = document.createElement("script");
+ scriptEl.textContent = expr;
+ document.body.appendChild(scriptEl);
+ });
+
+ if (expected.isPromiseRejection) {
+ // Wait a bit after an uncaught promise rejection error, as they are not emitted
+ // right away.
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(res => setTimeout(res, 10));
+ }
+ }
+}
+
+function checkPageErrorResource(pageErrorResource, expected) {
+ // Let's remove test harness related frames in stacktrace
+ const clonedPageErrorResource = { ...pageErrorResource };
+ if (clonedPageErrorResource.stacktrace) {
+ const index = clonedPageErrorResource.stacktrace.findIndex(frame =>
+ frame.filename.startsWith("resource://testing-common/content-task.js")
+ );
+ if (index > -1) {
+ clonedPageErrorResource.stacktrace = clonedPageErrorResource.stacktrace.slice(
+ 0,
+ index
+ );
+ }
+ }
+ checkObject(clonedPageErrorResource, expected);
+}
+
+const noUncaughtException = Symbol();
+const NUMBER_REGEX = /^\d+$/;
+
+const mdnUrl = path =>
+ `https://developer.mozilla.org/${path}?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default`;
+
+const expectedPageErrors = new Map([
+ [
+ "document.doTheImpossible();",
+ {
+ errorMessage: /doTheImpossible/,
+ errorMessageName: undefined,
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Not_a_function"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 10,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "(42).toString(0);",
+ {
+ errorMessage: /radix/,
+ errorMessageName: "JSMSG_BAD_RADIX",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl("docs/Web/JavaScript/Reference/Errors/Bad_radix"),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 6,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "'use strict'; (Object.freeze({name: 'Elsa', score: 157})).score = 0;",
+ {
+ errorMessage: /read.only/,
+ errorMessageName: "JSMSG_READ_ONLY",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl("docs/Web/JavaScript/Reference/Errors/Read-only"),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 23,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "([]).length = -1",
+ {
+ errorMessage: /array length/,
+ errorMessageName: "JSMSG_BAD_ARRAY_LENGTH",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Invalid_array_length"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 2,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "'abc'.repeat(-1);",
+ {
+ errorMessage: /repeat count.*non-negative/,
+ errorMessageName: "JSMSG_NEGATIVE_REPETITION_COUNT",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Negative_repetition_count"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: "self-hosted",
+ sourceId: null,
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ functionName: "repeat",
+ },
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 7,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "'a'.repeat(2e28);",
+ {
+ errorMessage: /repeat count.*less than infinity/,
+ errorMessageName: "JSMSG_RESULTING_STRING_TOO_LARGE",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Resulting_string_too_large"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: "self-hosted",
+ sourceId: null,
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ functionName: "repeat",
+ },
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 5,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "77.1234.toExponential(-1);",
+ {
+ errorMessage: /out of range/,
+ errorMessageName: "JSMSG_PRECISION_RANGE",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Precision_range"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 9,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "function a() { return; 1 + 1; }",
+ {
+ errorMessage: /unreachable code/,
+ errorMessageName: "JSMSG_STMT_AFTER_RETURN",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: false,
+ warning: true,
+ info: false,
+ sourceId: null,
+ lineText: "function a() { return; 1 + 1; }",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Stmt_after_return"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: null,
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "{let a, a;}",
+ {
+ errorMessage: /redeclaration of/,
+ errorMessageName: "JSMSG_REDECLARED_VAR",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ sourceId: null,
+ lineText: "{let a, a;}",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl(
+ "docs/Web/JavaScript/Reference/Errors/Redeclared_parameter"
+ ),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [],
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ notes: [
+ {
+ messageBody: /Previously declared at line/,
+ frame: {
+ source: /test_page_errors/,
+ },
+ },
+ ],
+ },
+ ],
+ [
+ `var error = new TypeError("abc");
+ error.name = "MyError";
+ error.message = "here";
+ throw error`,
+ {
+ errorMessage: /MyError: here/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl("docs/Web/JavaScript/Reference/Errors/Read-only"),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 13,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ "DOMTokenList.prototype.contains.call([])",
+ {
+ errorMessage: /does not implement interface/,
+ errorMessageName: "MSG_METHOD_THIS_DOES_NOT_IMPLEMENT_INTERFACE",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl("docs/Web/JavaScript/Reference/Errors/Read-only"),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ lineNumber: 1,
+ columnNumber: 33,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: false,
+ isForwardedFromContentProcess: false,
+ },
+ ],
+ [
+ `
+ function promiseThrow() {
+ var error2 = new TypeError("abc");
+ error2.name = "MyPromiseError";
+ error2.message = "here2";
+ return Promise.reject(error2);
+ }
+ promiseThrow()`,
+ {
+ errorMessage: /MyPromiseError: here2/,
+ errorMessageName: "",
+ sourceName: /test_page_errors/,
+ category: "content javascript",
+ timeStamp: NUMBER_REGEX,
+ error: true,
+ warning: false,
+ info: false,
+ lineText: "",
+ lineNumber: NUMBER_REGEX,
+ columnNumber: NUMBER_REGEX,
+ exceptionDocURL: mdnUrl("docs/Web/JavaScript/Reference/Errors/Read-only"),
+ innerWindowID: NUMBER_REGEX,
+ private: false,
+ stacktrace: [
+ {
+ filename: /test_page_errors\.html/,
+ sourceId: null,
+ lineNumber: 6,
+ columnNumber: 24,
+ functionName: "promiseThrow",
+ },
+ {
+ filename: /test_page_errors\.html/,
+ sourceId: null,
+ lineNumber: 8,
+ columnNumber: 7,
+ functionName: null,
+ },
+ ],
+ notes: null,
+ chromeContext: false,
+ isPromiseRejection: true,
+ isForwardedFromContentProcess: false,
+ [noUncaughtException]: true,
+ },
+ ],
+]);
diff --git a/devtools/shared/resources/tests/browser_resources_getAllResources.js b/devtools/shared/resources/tests/browser_resources_getAllResources.js
new file mode 100644
index 0000000000..c22f6e0b52
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_getAllResources.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test getAllResources function of the ResourceWatcher.
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const TEST_URI = "data:text/html;charset=utf-8,getAllResources test";
+
+add_task(async function() {
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Check the resources gotten from getAllResources at initial");
+ is(
+ resourceWatcher.getAllResources(ResourceWatcher.TYPES.CONSOLE_MESSAGE)
+ .length,
+ 0,
+ "There is no resources at initial"
+ );
+
+ info(
+ "Start to watch the available resources in order to compare with resources gotten from getAllResources"
+ );
+ const availableResources = [];
+ const onAvailable = resources => availableResources.push(...resources);
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ { onAvailable }
+ );
+
+ info("Check the resources after some resources are available");
+ const messages = ["a", "b", "c"];
+ await logMessages(tab.linkedBrowser, messages);
+ await waitUntil(() => availableResources.length >= messages.length);
+ assertResources(
+ resourceWatcher.getAllResources(ResourceWatcher.TYPES.CONSOLE_MESSAGE),
+ availableResources
+ );
+ assertResources(
+ resourceWatcher.getAllResources(ResourceWatcher.TYPES.STYLESHEET),
+ []
+ );
+
+ info("Check the resources after reloading");
+ const onReloaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.reloadTab(tab);
+ await onReloaded;
+ assertResources(
+ resourceWatcher.getAllResources(ResourceWatcher.TYPES.CONSOLE_MESSAGE),
+ []
+ );
+
+ info("Append some resources again to test unwatching");
+ await logMessages(tab.linkedBrowser, messages);
+ await waitUntil(
+ () =>
+ resourceWatcher.getAllResources(ResourceWatcher.TYPES.CONSOLE_MESSAGE)
+ .length === messages.length
+ );
+
+ info("Check the resources after unwatching");
+ resourceWatcher.unwatchResources([ResourceWatcher.TYPES.CONSOLE_MESSAGE], {
+ onAvailable,
+ });
+ assertResources(
+ resourceWatcher.getAllResources(ResourceWatcher.TYPES.CONSOLE_MESSAGE),
+ []
+ );
+
+ await targetList.destroy();
+ await client.close();
+});
+
+function assertResources(resources, expectedResources) {
+ is(
+ resources.length,
+ expectedResources.length,
+ "Number of the resources is correct"
+ );
+
+ for (let i = 0; i < resources.length; i++) {
+ const resource = resources[i];
+ const expectedResource = expectedResources[i];
+ ok(resource === expectedResource, `The ${i}th resource is correct`);
+ }
+}
+
+function logMessages(browser, messages) {
+ return ContentTask.spawn(browser, { messages }, args => {
+ for (const message of args.messages) {
+ content.console.log(message);
+ }
+ });
+}
diff --git a/devtools/shared/resources/tests/browser_resources_network_event_stacktraces.js b/devtools/shared/resources/tests/browser_resources_network_event_stacktraces.js
new file mode 100644
index 0000000000..23500b231c
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_network_event_stacktraces.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around NETWORK_EVENT_STACKTRACE
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const TEST_URI = `${URL_ROOT_SSL}network_document.html`;
+
+const REQUEST_STUB = {
+ code: `await fetch("/request_post_0.html", { method: "POST" });`,
+ expected: {
+ stacktraceAvailable: true,
+ lastFrame: {
+ filename:
+ "https://example.com/browser/devtools/shared/resources/tests/network_document.html",
+ lineNumber: 1,
+ columnNumber: 40,
+ functionName: "triggerRequest",
+ asyncCause: null,
+ },
+ },
+};
+
+add_task(async function() {
+ info("Test network stacktraces events");
+ const tab = await addTab(TEST_URI);
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ const networkEvents = new Map();
+ const stackTraces = new Map();
+
+ function onResourceAvailable(resources) {
+ for (const resource of resources) {
+ if (
+ resource.resourceType === ResourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE
+ ) {
+ ok(
+ !networkEvents.has(resource.resourceId),
+ "The network event does not exist"
+ );
+
+ is(
+ resource.stacktraceAvailable,
+ REQUEST_STUB.expected.stacktraceAvailable,
+ "The stacktrace is available"
+ );
+ is(
+ JSON.stringify(resource.lastFrame),
+ JSON.stringify(REQUEST_STUB.expected.lastFrame),
+ "The last frame of the stacktrace is available"
+ );
+
+ stackTraces.set(resource.resourceId, true);
+ return;
+ }
+
+ if (resource.resourceType === ResourceWatcher.TYPES.NETWORK_EVENT) {
+ ok(
+ stackTraces.has(resource.stacktraceResourceId),
+ "The stack trace does exists"
+ );
+
+ networkEvents.set(resource.resourceId, true);
+ }
+ }
+ }
+
+ function onResourceUpdated() {}
+
+ await resourceWatcher.watchResources(
+ [
+ ResourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE,
+ ResourceWatcher.TYPES.NETWORK_EVENT,
+ ],
+ {
+ onAvailable: onResourceAvailable,
+ onUpdated: onResourceUpdated,
+ }
+ );
+
+ await triggerNetworkRequests(tab.linkedBrowser, [REQUEST_STUB.code]);
+
+ resourceWatcher.unwatchResources(
+ [
+ ResourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE,
+ ResourceWatcher.TYPES.NETWORK_EVENT,
+ ],
+ {
+ onAvailable: onResourceAvailable,
+ onUpdated: onResourceUpdated,
+ }
+ );
+
+ await targetList.destroy();
+ await client.close();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/shared/resources/tests/browser_resources_network_events.js b/devtools/shared/resources/tests/browser_resources_network_events.js
new file mode 100644
index 0000000000..81113779bf
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_network_events.js
@@ -0,0 +1,258 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around NETWORK_EVENT
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const EXAMPLE_DOMAIN = "https://example.com/";
+const TEST_URI = `${URL_ROOT_SSL}network_document.html`;
+
+add_task(async function() {
+ info("Test network events");
+ await testNetworkEventResourcesWithExistingResources();
+ await testNetworkEventResourcesWithoutExistingResources();
+});
+
+async function testNetworkEventResourcesWithExistingResources() {
+ info(`Tests for network event resources with the existing resources`);
+ await testNetworkEventResources({
+ ignoreExistingResources: false,
+ // 1 available event fired, for the existing resource in the cache.
+ // 1 available event fired, when live request is created.
+ totalExpectedOnAvailableCounts: 2,
+ // 1 update events fired, when live request is updated.
+ totalExpectedOnUpdatedCounts: 1,
+ expectedResourcesOnAvailable: {
+ [`${EXAMPLE_DOMAIN}cached_post.html`]: {
+ resourceType: ResourceWatcher.TYPES.NETWORK_EVENT,
+ method: "POST",
+ },
+ [`${EXAMPLE_DOMAIN}live_get.html`]: {
+ resourceType: ResourceWatcher.TYPES.NETWORK_EVENT,
+ method: "GET",
+ },
+ },
+ expectedResourcesOnUpdated: {
+ [`${EXAMPLE_DOMAIN}live_get.html`]: {
+ resourceType: ResourceWatcher.TYPES.NETWORK_EVENT,
+ method: "GET",
+ },
+ },
+ });
+}
+
+async function testNetworkEventResourcesWithoutExistingResources() {
+ info(`Tests for network event resources without the existing resources`);
+ await testNetworkEventResources({
+ ignoreExistingResources: true,
+ // 1 available event fired, when live request is created.
+ totalExpectedOnAvailableCounts: 1,
+ // 1 update events fired, when live request is updated.
+ totalExpectedOnUpdatedCounts: 1,
+ expectedResourcesOnAvailable: {
+ [`${EXAMPLE_DOMAIN}live_get.html`]: {
+ resourceType: ResourceWatcher.TYPES.NETWORK_EVENT,
+ method: "GET",
+ },
+ },
+ expectedResourcesOnUpdated: {
+ [`${EXAMPLE_DOMAIN}live_get.html`]: {
+ resourceType: ResourceWatcher.TYPES.NETWORK_EVENT,
+ method: "GET",
+ },
+ },
+ });
+}
+
+async function testNetworkEventResources(options) {
+ const tab = await addTab(TEST_URI);
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info(
+ `Trigger some network requests *before* calling ResourceWatcher.watchResources
+ in order to assert the behavior of already existing network events.`
+ );
+
+ let onResourceAvailable = () => {};
+ let onResourceUpdated = () => {};
+
+ // Lets make sure there is already a network event resource in the cache.
+ const waitOnRequestForResourceWatcherCache = new Promise(resolve => {
+ onResourceAvailable = resources => {
+ for (const resource of resources) {
+ is(
+ resource.resourceType,
+ ResourceWatcher.TYPES.NETWORK_EVENT,
+ "Received a network event resource"
+ );
+ }
+ };
+
+ onResourceUpdated = updates => {
+ for (const { resource } of updates) {
+ is(
+ resource.resourceType,
+ ResourceWatcher.TYPES.NETWORK_EVENT,
+ "Received a network update event resource"
+ );
+ resolve();
+ }
+ };
+
+ resourceWatcher
+ .watchResources([ResourceWatcher.TYPES.NETWORK_EVENT], {
+ onAvailable: onResourceAvailable,
+ onUpdated: onResourceUpdated,
+ })
+ .then(() => {
+ // We can only trigger the requests once `watchResources` settles, otherwise the
+ // thread might be paused.
+ triggerNetworkRequests(tab.linkedBrowser, [cachedRequest]);
+ });
+ });
+
+ await waitOnRequestForResourceWatcherCache;
+
+ const actualResourcesOnAvailable = {};
+ const actualResourcesOnUpdated = {};
+
+ let {
+ totalExpectedOnAvailableCounts,
+ totalExpectedOnUpdatedCounts,
+ expectedResourcesOnAvailable,
+ expectedResourcesOnUpdated,
+
+ ignoreExistingResources,
+ } = options;
+
+ const waitForAllExpectedOnAvailableEvents = waitUntil(
+ () => totalExpectedOnAvailableCounts == 0
+ );
+ const waitForAllExpectedOnUpdatedEvents = waitUntil(
+ () => totalExpectedOnUpdatedCounts == 0
+ );
+
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ is(
+ resource.resourceType,
+ ResourceWatcher.TYPES.NETWORK_EVENT,
+ "Received a network event resource"
+ );
+ actualResourcesOnAvailable[resource.url] = {
+ resourceId: resource.resourceId,
+ resourceType: resource.resourceType,
+ method: resource.method,
+ };
+ totalExpectedOnAvailableCounts--;
+ }
+ };
+
+ const onUpdated = updates => {
+ for (const { resource } of updates) {
+ is(
+ resource.resourceType,
+ ResourceWatcher.TYPES.NETWORK_EVENT,
+ "Received a network update event resource"
+ );
+ actualResourcesOnUpdated[resource.url] = {
+ resourceId: resource.resourceId,
+ resourceType: resource.resourceType,
+ method: resource.method,
+ };
+ totalExpectedOnUpdatedCounts--;
+ }
+ };
+
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.NETWORK_EVENT], {
+ onAvailable,
+ onUpdated,
+ ignoreExistingResources,
+ });
+
+ info(
+ `Trigger the rest of the requests *after* calling ResourceWatcher.watchResources
+ in order to assert the behavior of live network events.`
+ );
+ await triggerNetworkRequests(tab.linkedBrowser, [liveRequest]);
+
+ await Promise.all([
+ waitForAllExpectedOnAvailableEvents,
+ waitForAllExpectedOnUpdatedEvents,
+ ]);
+
+ info("Check the resources on available");
+ is(
+ Object.keys(actualResourcesOnAvailable).length,
+ Object.keys(expectedResourcesOnAvailable).length,
+ "Got the expected number of network events fired onAvailable"
+ );
+
+ // assert that the resourceId for the the available and updated events match
+ is(
+ actualResourcesOnAvailable[`${EXAMPLE_DOMAIN}live_get.html`].resourceId,
+ actualResourcesOnUpdated[`${EXAMPLE_DOMAIN}live_get.html`].resourceId,
+ "The resource id's are the same"
+ );
+
+ // assert the resources emitted when the network event is created
+ for (const key in expectedResourcesOnAvailable) {
+ const expected = expectedResourcesOnAvailable[key];
+ const actual = actualResourcesOnAvailable[key];
+ assertResources(actual, expected);
+ }
+
+ info("Check the resources on updated");
+
+ is(
+ Object.keys(actualResourcesOnUpdated).length,
+ Object.keys(expectedResourcesOnUpdated).length,
+ "Got the expected number of network events fired onUpdated"
+ );
+
+ // assert the resources emitted when the network event is updated
+ for (const key in expectedResourcesOnUpdated) {
+ const expected = expectedResourcesOnUpdated[key];
+ const actual = actualResourcesOnUpdated[key];
+ assertResources(actual, expected);
+ }
+
+ await resourceWatcher.unwatchResources(
+ [ResourceWatcher.TYPES.NETWORK_EVENT],
+ {
+ onAvailable,
+ onUpdated,
+ ignoreExistingResources,
+ }
+ );
+
+ await resourceWatcher.unwatchResources(
+ [ResourceWatcher.TYPES.NETWORK_EVENT],
+ {
+ onAvailable: onResourceAvailable,
+ onUpdated: onResourceUpdated,
+ }
+ );
+ await targetList.destroy();
+ await client.close();
+ BrowserTestUtils.removeTab(tab);
+}
+
+function assertResources(actual, expected) {
+ is(
+ actual.resourceType,
+ expected.resourceType,
+ "The resource type is correct"
+ );
+ is(actual.method, expected.method, "The method is correct");
+}
+
+const cachedRequest = `await fetch("/cached_post.html", { method: "POST" });`;
+const liveRequest = `await fetch("/live_get.html", { method: "GET" });`;
diff --git a/devtools/shared/resources/tests/browser_resources_platform_messages.js b/devtools/shared/resources/tests/browser_resources_platform_messages.js
new file mode 100644
index 0000000000..324b92f771
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_platform_messages.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around PLATFORM_MESSAGE
+// Reproduces assertions from: devtools/shared/webconsole/test/chrome/test_nsiconsolemessage.html
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+add_task(async function() {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ await testPlatformMessagesResources();
+ await testPlatformMessagesResourcesWithIgnoreExistingResources();
+});
+
+async function testPlatformMessagesResources() {
+ const {
+ client,
+ resourceWatcher,
+ targetList,
+ } = await initMultiProcessResourceWatcher();
+
+ const expectedMessages = [
+ "This is a cached message",
+ "This is another cached message",
+ "This is a live message",
+ "This is another live message",
+ ];
+ const receivedMessages = [];
+
+ info(
+ "Log some messages *before* calling ResourceWatcher.watchResources in order to assert the behavior of already existing messages."
+ );
+ Services.console.logStringMessage(expectedMessages[0]);
+ Services.console.logStringMessage(expectedMessages[1]);
+
+ let done;
+ const onAllMessagesReceived = new Promise(resolve => (done = resolve));
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ if (!expectedMessages.includes(resource.message)) {
+ continue;
+ }
+
+ is(
+ resource.targetFront,
+ targetList.targetFront,
+ "The targetFront property is the expected one"
+ );
+
+ receivedMessages.push(resource.message);
+ is(
+ resource.message,
+ expectedMessages[receivedMessages.length - 1],
+ `Received the expected «${resource.message}» message, in the expected order`
+ );
+
+ ok(
+ resource.timeStamp.toString().match(/^\d+$/),
+ "The resource has a timeStamp property"
+ );
+
+ if (receivedMessages.length == expectedMessages.length) {
+ done();
+ }
+ }
+ };
+
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.PLATFORM_MESSAGE],
+ {
+ onAvailable,
+ }
+ );
+
+ info(
+ "Now log messages *after* the call to ResourceWatcher.watchResources and after having received all existing messages"
+ );
+ Services.console.logStringMessage(expectedMessages[2]);
+ Services.console.logStringMessage(expectedMessages[3]);
+
+ info("Waiting for all expected messages to be received");
+ await onAllMessagesReceived;
+ ok(true, "All the expected messages were received");
+
+ Services.console.reset();
+ targetList.destroy();
+ await client.close();
+}
+
+async function testPlatformMessagesResourcesWithIgnoreExistingResources() {
+ const {
+ client,
+ resourceWatcher,
+ targetList,
+ } = await initMultiProcessResourceWatcher();
+
+ info(
+ "Check whether onAvailable will not be called with existing platform messages"
+ );
+ const expectedMessages = ["This is 1st message", "This is 2nd message"];
+ Services.console.logStringMessage(expectedMessages[0]);
+ Services.console.logStringMessage(expectedMessages[1]);
+
+ const availableResources = [];
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.PLATFORM_MESSAGE],
+ {
+ onAvailable: resources => {
+ for (const resource of resources) {
+ if (!expectedMessages.includes(resource.message)) {
+ continue;
+ }
+
+ availableResources.push(resource);
+ }
+ },
+ ignoreExistingResources: true,
+ }
+ );
+ is(
+ availableResources.length,
+ 0,
+ "onAvailable wasn't called for existing platform messages"
+ );
+
+ info(
+ "Check whether onAvailable will be called with the future platform messages"
+ );
+ Services.console.logStringMessage(expectedMessages[0]);
+ Services.console.logStringMessage(expectedMessages[1]);
+
+ await waitUntil(() => availableResources.length === expectedMessages.length);
+ for (let i = 0; i < expectedMessages.length; i++) {
+ const { message } = availableResources[i];
+ const expected = expectedMessages[i];
+ is(message, expected, `Message[${i}] is correct`);
+ }
+
+ Services.console.reset();
+ await targetList.destroy();
+ await client.close();
+}
diff --git a/devtools/shared/resources/tests/browser_resources_root_node.js b/devtools/shared/resources/tests/browser_resources_root_node.js
new file mode 100644
index 0000000000..44c9b7347f
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_root_node.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around ROOT_NODE
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+/**
+ * The original test still asserts some scenarios using several watchRootNode
+ * call sites, which is not something we intend to support at the moment in the
+ * resource watcher.
+ *
+ * Otherwise this test checks the basic behavior of the resource when reloading
+ * an empty page.
+ */
+add_task(async function() {
+ // Open a test tab
+ const tab = await addTab("data:text/html,Root Node tests");
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ const browser = gBrowser.selectedBrowser;
+
+ info("Call watchResources([ROOT_NODE], ...)");
+ let onAvailableCounter = 0;
+ const onAvailable = resources => (onAvailableCounter += resources.length);
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.ROOT_NODE], {
+ onAvailable,
+ });
+
+ info("Wait until onAvailable has been called");
+ await waitUntil(() => onAvailableCounter === 1);
+ is(onAvailableCounter, 1, "onAvailable has been called 1 time");
+
+ info("Reload the selected browser");
+ browser.reload();
+
+ info(
+ "Wait until the watchResources([ROOT_NODE], ...) callback has been called"
+ );
+ await waitUntil(() => onAvailableCounter === 2);
+
+ is(onAvailableCounter, 2, "onAvailable has been called 2 times");
+
+ info("Call unwatchResources([ROOT_NODE], ...) for the onAvailable callback");
+ resourceWatcher.unwatchResources([ResourceWatcher.TYPES.ROOT_NODE], {
+ onAvailable,
+ });
+
+ info("Reload the selected browser");
+ const reloaded = BrowserTestUtils.browserLoaded(browser);
+ browser.reload();
+ await reloaded;
+
+ is(
+ onAvailableCounter,
+ 2,
+ "onAvailable was not called after calling unwatchResources"
+ );
+
+ // Cleanup
+ targetList.destroy();
+ await client.close();
+});
+
+/**
+ * Test that the watchRootNode API provides the expected node fronts.
+ */
+add_task(async function testRootNodeFrontIsCorrect() {
+ const tab = await addTab("data:text/html,<div id=div1>");
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+ const browser = gBrowser.selectedBrowser;
+
+ info("Call watchResources([ROOT_NODE], ...)");
+
+ let rootNodeResolve;
+ let rootNodePromise = new Promise(r => (rootNodeResolve = r));
+ const onAvailable = ([rootNodeFront]) => rootNodeResolve(rootNodeFront);
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.ROOT_NODE], {
+ onAvailable,
+ });
+
+ info("Wait until onAvailable has been called");
+ const root1 = await rootNodePromise;
+ ok(!!root1, "onAvailable has been called with a valid argument");
+ is(
+ root1.resourceType,
+ ResourceWatcher.TYPES.ROOT_NODE,
+ "The resource has the expected type"
+ );
+
+ info("Check we can query an expected node under the retrieved root");
+ const div1 = await root1.walkerFront.querySelector(root1, "div");
+ is(div1.getAttribute("id"), "div1", "Correct root node retrieved");
+
+ info("Reload the selected browser");
+ rootNodePromise = new Promise(r => (rootNodeResolve = r));
+ browser.reload();
+
+ const root2 = await rootNodePromise;
+ ok(
+ root1 !== root2,
+ "onAvailable has been called with a different node front after reload"
+ );
+
+ info("Navigate to another URL");
+ rootNodePromise = new Promise(r => (rootNodeResolve = r));
+ BrowserTestUtils.loadURI(browser, `data:text/html,<div id=div3>`);
+ const root3 = await rootNodePromise;
+ info("Check we can query an expected node under the retrieved root");
+ const div3 = await root3.walkerFront.querySelector(root3, "div");
+ is(div3.getAttribute("id"), "div3", "Correct root node retrieved");
+
+ // Cleanup
+ resourceWatcher.unwatchResources([ResourceWatcher.TYPES.ROOT_NODE], {
+ onAvailable,
+ });
+ targetList.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/resources/tests/browser_resources_several_resources.js b/devtools/shared/resources/tests/browser_resources_several_resources.js
new file mode 100644
index 0000000000..b197eddf54
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_several_resources.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+/**
+ * Check that the resource watcher is still properly watching for new targets
+ * after unwatching one resource, if there is still another watched resource.
+ */
+add_task(async function() {
+ // We will create a main process target list here in order to monitor
+ // resources from new tabs as they get created.
+ // devtools.browsertoolbox.fission should be true to monitor resources from
+ // remote browsers & frames.
+ await pushPref("devtools.browsertoolbox.fission", true);
+
+ // Open a test tab
+ const tab = await addTab("data:text/html,Root Node tests");
+
+ const {
+ client,
+ resourceWatcher,
+ targetList,
+ } = await initMultiProcessResourceWatcher();
+
+ const { CONSOLE_MESSAGE, ROOT_NODE } = ResourceWatcher.TYPES;
+
+ // We are only interested in console messages as a resource, the ROOT_NODE one
+ // is here to test the ResourceWatcher::unwatchResources API with several resources.
+ const receivedMessages = [];
+ const onAvailable = resources => {
+ for (const resource of resources) {
+ if (resource.resourceType === CONSOLE_MESSAGE) {
+ receivedMessages.push(resource);
+ }
+ }
+ };
+
+ info("Call watchResources([CONSOLE_MESSAGE, ROOT_NODE], ...)");
+ await resourceWatcher.watchResources([CONSOLE_MESSAGE, ROOT_NODE], {
+ onAvailable,
+ });
+
+ info("Use console.log in the content page");
+ logInTab(tab, "test from data-url");
+ info(
+ "Wait until onAvailable received the CONSOLE_MESSAGE resource emitted from the data-url tab"
+ );
+ await waitUntil(() =>
+ receivedMessages.find(
+ resource => resource.message.arguments[0] === "test from data-url"
+ )
+ );
+
+ // Check that the resource watcher captures resources from new targets.
+ info("Open a first tab on the example.com domain");
+ const comTab = await addTab(
+ "http://example.com/document-builder.sjs?html=com"
+ );
+ info("Use console.log in the example.com page");
+ logInTab(comTab, "test-from-example-com");
+ info(
+ "Wait until onAvailable received the CONSOLE_MESSAGE resource emitted from the example.com tab"
+ );
+ await waitUntil(() =>
+ receivedMessages.find(
+ resource => resource.message.arguments[0] === "test-from-example-com"
+ )
+ );
+
+ info("Stop watching ROOT_NODE resources");
+ await resourceWatcher.unwatchResources([ROOT_NODE], { onAvailable });
+
+ // Check that messages from new targets are still captured after calling
+ // unwatch for another resource.
+ info("Open a second tab on the example.net domain");
+ const netTab = await addTab(
+ "http://example.net/document-builder.sjs?html=net"
+ );
+ info("Use console.log in the example.net page");
+ logInTab(netTab, "test-from-example-net");
+ info(
+ "Wait until onAvailable received the CONSOLE_MESSAGE resource emitted from the example.net tab"
+ );
+ await waitUntil(() =>
+ receivedMessages.find(
+ resource => resource.message.arguments[0] === "test-from-example-net"
+ )
+ );
+
+ info("Stop watching CONSOLE_MESSAGE resources");
+ await resourceWatcher.unwatchResources([CONSOLE_MESSAGE], { onAvailable });
+ await logInTab(tab, "test-again");
+
+ // We don't have a specific event to wait for here, so allow some time for
+ // the message to be received.
+ await wait(1000);
+
+ is(
+ receivedMessages.find(
+ resource => resource.message.arguments[0] === "test-again"
+ ),
+ undefined,
+ "The resource watcher should not watch CONSOLE_MESSAGE anymore"
+ );
+
+ // Cleanup
+ targetList.destroy();
+ await client.close();
+});
+
+function logInTab(tab, message) {
+ return ContentTask.spawn(tab.linkedBrowser, message, function(_message) {
+ content.console.log(_message);
+ });
+}
diff --git a/devtools/shared/resources/tests/browser_resources_sources.js b/devtools/shared/resources/tests/browser_resources_sources.js
new file mode 100644
index 0000000000..68f533d713
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_sources.js
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around SOURCE.
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const TEST_URL = URL_ROOT_SSL + "sources.html";
+
+add_task(async function() {
+ const tab = await addTab(TEST_URL);
+
+ const htmlRequest = await fetch(TEST_URL);
+ const htmlContent = await htmlRequest.text();
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ // Force the target list to cover workers
+ targetList.listenForWorkers = true;
+ targetList.listenForServiceWorkers = true;
+ await targetList.startListening();
+
+ const targets = [];
+ await targetList.watchTargets(targetList.ALL_TYPES, async function({
+ targetFront,
+ }) {
+ targets.push(targetFront);
+ });
+ is(targets.length, 3, "Got expected number of targets");
+
+ info("Check already available resources");
+ const availableResources = [];
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.SOURCE], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ const expectedExistingResources = [
+ {
+ description: "independent js file",
+ sourceForm: {
+ introductionType: "scriptElement",
+ sourceMapBaseURL:
+ "https://example.com/browser/devtools/shared/resources/tests/sources.js",
+ url:
+ "https://example.com/browser/devtools/shared/resources/tests/sources.js",
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "/* eslint-disable */\nfunction scriptSource() {}\n",
+ },
+ },
+ {
+ description: "eval",
+ sourceForm: {
+ introductionType: "eval",
+ sourceMapBaseURL:
+ "https://example.com/browser/devtools/shared/resources/tests/sources.html",
+ url: null,
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "this.global = function evalFunction() {}",
+ },
+ },
+ {
+ description: "inline JS",
+ sourceForm: {
+ introductionType: "scriptElement",
+ sourceMapBaseURL:
+ "https://example.com/browser/devtools/shared/resources/tests/sources.html",
+ url:
+ "https://example.com/browser/devtools/shared/resources/tests/sources.html",
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ },
+ sourceContent: {
+ contentType: "text/html",
+ source: htmlContent,
+ },
+ },
+ {
+ description: "worker script",
+ sourceForm: {
+ introductionType: undefined,
+ sourceMapBaseURL:
+ "https://example.com/browser/devtools/shared/resources/tests/worker-sources.js",
+ url:
+ "https://example.com/browser/devtools/shared/resources/tests/worker-sources.js",
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "/* eslint-disable */\nfunction workerSource() {}\n",
+ },
+ },
+ {
+ description: "service worker script",
+ sourceForm: {
+ introductionType: undefined,
+ sourceMapBaseURL:
+ "https://example.com/browser/devtools/shared/resources/tests/service-worker-sources.js",
+ url:
+ "https://example.com/browser/devtools/shared/resources/tests/service-worker-sources.js",
+ isBlackBoxed: false,
+ sourceMapURL: null,
+ extensionName: null,
+ },
+ sourceContent: {
+ contentType: "text/javascript",
+ source: "/* eslint-disable */\nfunction serviceWorkerSource() {}\n",
+ },
+ },
+ ];
+ await assertResources(availableResources, expectedExistingResources);
+
+ await targetList.stopListening();
+ await client.close();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+});
+
+async function assertResources(resources, expected) {
+ is(
+ resources.length,
+ expected.length,
+ "Length of existing resources is correct at initial"
+ );
+ for (let i = 0; i < resources.length; i++) {
+ await assertResource(resources[i], expected);
+ }
+}
+
+async function assertResource(source, expected) {
+ info(`Checking resource "#${expected.description}"`);
+
+ is(
+ source.resourceType,
+ ResourceWatcher.TYPES.SOURCE,
+ "Resource type is correct"
+ );
+
+ const threadFront = await source.targetFront.getFront("thread");
+ // `source` is SourceActor's form()
+ // so try to instantiate the related SourceFront:
+ const sourceFront = threadFront.source(source);
+ // then fetch source content
+ const sourceContent = await sourceFront.source();
+
+ // Order of sources is random, so we have to find the best expected resource.
+ // The only unique attribute is the JS Source text content.
+ const matchingExpected = expected.find(res => {
+ return res.sourceContent.source == sourceContent.source;
+ });
+ ok(
+ matchingExpected,
+ `This source was expected with source content being "${sourceContent.source}"`
+ );
+ assertObject(
+ sourceContent,
+ matchingExpected.sourceContent,
+ matchingExpected.description
+ );
+
+ assertObject(
+ source,
+ matchingExpected.sourceForm,
+ matchingExpected.description
+ );
+}
+
+function assertObject(object, expected, description) {
+ for (const field in expected) {
+ is(
+ object[field],
+ expected[field],
+ `The value of ${field} is correct for "#${description}"`
+ );
+ }
+}
diff --git a/devtools/shared/resources/tests/browser_resources_stylesheets.js b/devtools/shared/resources/tests/browser_resources_stylesheets.js
new file mode 100644
index 0000000000..79735cae0e
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_stylesheets.js
@@ -0,0 +1,506 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around STYLESHEET.
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const STYLE_TEST_URL = URL_ROOT_SSL + "style_document.html";
+
+const EXISTING_RESOURCES = [
+ {
+ styleText: "body { color: lime; }",
+ href: null,
+ nodeHref:
+ "https://example.com/browser/devtools/shared/resources/tests/style_document.html",
+ isNew: false,
+ disabled: false,
+ ruleCount: 1,
+ mediaRules: [],
+ },
+ {
+ styleText: "body { margin: 1px; }",
+ href:
+ "https://example.com/browser/devtools/shared/resources/tests/style_document.css",
+ nodeHref:
+ "https://example.com/browser/devtools/shared/resources/tests/style_document.html",
+ isNew: false,
+ disabled: false,
+ ruleCount: 1,
+ mediaRules: [],
+ },
+ {
+ styleText: "body { background-color: pink; }",
+ href: null,
+ nodeHref:
+ "https://example.org/browser/devtools/shared/resources/tests/style_iframe.html",
+ isNew: false,
+ disabled: false,
+ ruleCount: 1,
+ mediaRules: [],
+ },
+ {
+ styleText: "body { padding: 1px; }",
+ href:
+ "https://example.org/browser/devtools/shared/resources/tests/style_iframe.css",
+ nodeHref:
+ "https://example.org/browser/devtools/shared/resources/tests/style_iframe.html",
+ isNew: false,
+ disabled: false,
+ ruleCount: 1,
+ mediaRules: [],
+ },
+];
+
+const ADDITIONAL_RESOURCE = {
+ styleText:
+ "@media all { body { color: red; } } @media print { body { color: cyan; } } body { font-size: 10px; }",
+ href: null,
+ nodeHref:
+ "https://example.com/browser/devtools/shared/resources/tests/style_document.html",
+ isNew: false,
+ disabled: false,
+ ruleCount: 3,
+ mediaRules: [
+ {
+ conditionText: "all",
+ mediaText: "all",
+ matches: true,
+ line: 1,
+ column: 1,
+ },
+ {
+ conditionText: "print",
+ mediaText: "print",
+ matches: false,
+ line: 1,
+ column: 37,
+ },
+ ],
+};
+
+const ADDITIONAL_FROM_ACTOR_RESOURCE = {
+ styleText: "body { font-size: 10px; }",
+ href: null,
+ nodeHref:
+ "https://example.com/browser/devtools/shared/resources/tests/style_document.html",
+ isNew: true,
+ disabled: false,
+ ruleCount: 1,
+ mediaRules: [],
+};
+
+add_task(async function() {
+ await pushPref("devtools.testing.enableServerWatcherSupport", false);
+ await testResourceAvailableFeature();
+ await testResourceUpdateFeature();
+ await testNestedResourceUpdateFeature();
+
+ await pushPref("devtools.testing.enableServerWatcherSupport", true);
+ await testResourceAvailableFeature();
+ await testResourceUpdateFeature();
+ await testNestedResourceUpdateFeature();
+});
+
+async function testResourceAvailableFeature() {
+ info("Check resource available feature of the ResourceWatcher");
+
+ const tab = await addTab(STYLE_TEST_URL);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Check whether ResourceWatcher gets existing stylesheet");
+ const availableResources = [];
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.STYLESHEET], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+
+ is(
+ availableResources.length,
+ EXISTING_RESOURCES.length,
+ "Length of existing resources is correct"
+ );
+ for (let i = 0; i < availableResources.length; i++) {
+ const availableResource = availableResources[i];
+ // We can not expect the resources to always be forwarded in the same order.
+ // See intermittent Bug 1655016.
+ const expectedResource = findMatchingExpectedResource(availableResource);
+ ok(expectedResource, "Found a matching expected resource for the resource");
+ await assertResource(availableResource, expectedResource);
+ }
+
+ info("Check whether ResourceWatcher gets additonal stylesheet");
+ await ContentTask.spawn(
+ tab.linkedBrowser,
+ ADDITIONAL_RESOURCE.styleText,
+ text => {
+ const document = content.document;
+ const stylesheet = document.createElement("style");
+ stylesheet.textContent = text;
+ document.body.appendChild(stylesheet);
+ }
+ );
+ await waitUntil(
+ () => availableResources.length === EXISTING_RESOURCES.length + 1
+ );
+ await assertResource(
+ availableResources[availableResources.length - 1],
+ ADDITIONAL_RESOURCE
+ );
+
+ info(
+ "Check whether ResourceWatcher gets additonal stylesheet which is added by DevTool"
+ );
+ const styleSheetsFront = await targetList.targetFront.getFront("stylesheets");
+ await styleSheetsFront.addStyleSheet(
+ ADDITIONAL_FROM_ACTOR_RESOURCE.styleText
+ );
+ await waitUntil(
+ () => availableResources.length === EXISTING_RESOURCES.length + 2
+ );
+ await assertResource(
+ availableResources[availableResources.length - 1],
+ ADDITIONAL_FROM_ACTOR_RESOURCE
+ );
+
+ await targetList.destroy();
+ await client.close();
+}
+
+async function testResourceUpdateFeature() {
+ info("Check resource update feature of the ResourceWatcher");
+
+ const tab = await addTab(STYLE_TEST_URL);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Setup the watcher");
+ const availableResources = [];
+ const updates = [];
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.STYLESHEET], {
+ onAvailable: resources => availableResources.push(...resources),
+ onUpdated: newUpdates => updates.push(...newUpdates),
+ });
+ is(
+ availableResources.length,
+ EXISTING_RESOURCES.length,
+ "Length of existing resources is correct"
+ );
+
+ info("Check toggleDisabled function");
+ const resource = availableResources[0];
+ const styleSheetsFront = await resource.targetFront.getFront("stylesheets");
+ await styleSheetsFront.toggleDisabled(resource.resourceId);
+ await waitUntil(() => updates.length === 1);
+
+ // Check the content of the update object.
+ assertUpdate(updates[0].update, {
+ resourceId: resource.resourceId,
+ updateType: "property-change",
+ });
+ is(
+ updates[0].update.resourceUpdates.disabled,
+ true,
+ "resourceUpdates is correct"
+ );
+
+ // Check whether the cached resource is updated correctly.
+ is(
+ updates[0].resource.disabled,
+ true,
+ "cached resource is updated correctly"
+ );
+
+ // Check whether the actual stylesheet is updated correctly.
+ const styleSheetDisabled = await ContentTask.spawn(
+ tab.linkedBrowser,
+ null,
+ () => {
+ const document = content.document;
+ const stylesheet = document.styleSheets[0];
+ return stylesheet.disabled;
+ }
+ );
+ is(styleSheetDisabled, true, "actual stylesheet is is updated correctly");
+
+ info("Check update function");
+ const expectedMediaRules = [
+ {
+ conditionText: "screen",
+ mediaText: "screen",
+ matches: true,
+ },
+ {
+ conditionText: "print",
+ mediaText: "print",
+ matches: false,
+ },
+ ];
+
+ const updateCause = "updated-by-test";
+ await styleSheetsFront.update(
+ resource.resourceId,
+ "@media screen { color: red; } @media print { color: green; } body { color: cyan; }",
+ false,
+ updateCause
+ );
+ await waitUntil(() => updates.length === 4);
+
+ assertUpdate(updates[1].update, {
+ resourceId: resource.resourceId,
+ updateType: "property-change",
+ });
+ is(
+ updates[1].update.resourceUpdates.ruleCount,
+ 3,
+ "resourceUpdates is correct"
+ );
+ is(updates[1].resource.ruleCount, 3, "cached resource is updated correctly");
+
+ assertUpdate(updates[2].update, {
+ resourceId: resource.resourceId,
+ updateType: "style-applied",
+ event: {
+ cause: updateCause,
+ },
+ });
+ is(
+ updates[2].update.resourceUpdates,
+ undefined,
+ "resourceUpdates is correct"
+ );
+
+ assertUpdate(updates[3].update, {
+ resourceId: resource.resourceId,
+ updateType: "media-rules-changed",
+ });
+ assertMediaRules(
+ updates[3].update.resourceUpdates.mediaRules,
+ expectedMediaRules
+ );
+
+ // Check the actual page.
+ const styleSheetResult = await getStyleSheetResult(tab);
+
+ is(
+ styleSheetResult.ruleCount,
+ 3,
+ "ruleCount of actual stylesheet is updated correctly"
+ );
+ assertMediaRules(styleSheetResult.mediaRules, expectedMediaRules);
+
+ await targetList.destroy();
+ await client.close();
+}
+
+async function testNestedResourceUpdateFeature() {
+ info("Check nested resource update feature of the ResourceWatcher");
+
+ const tab = await addTab(STYLE_TEST_URL);
+
+ const {
+ outerWidth: originalWindowWidth,
+ outerHeight: originalWindowHeight,
+ } = tab.ownerGlobal;
+
+ registerCleanupFunction(() => {
+ tab.ownerGlobal.resizeTo(originalWindowWidth, originalWindowHeight);
+ });
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Setup the watcher");
+ const availableResources = [];
+ const updates = [];
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.STYLESHEET], {
+ onAvailable: resources => availableResources.push(...resources),
+ onUpdated: newUpdates => updates.push(...newUpdates),
+ });
+ is(
+ availableResources.length,
+ EXISTING_RESOURCES.length,
+ "Length of existing resources is correct"
+ );
+
+ info("Apply new media query");
+ // In order to avoid applying the media query (min-height: 400px).
+ tab.ownerGlobal.resizeTo(originalWindowWidth, 300);
+
+ const resource = availableResources[0];
+ const styleSheetsFront = await resource.targetFront.getFront("stylesheets");
+ await styleSheetsFront.update(
+ resource.resourceId,
+ "@media (min-height: 400px) { color: red; }",
+ false
+ );
+ await waitUntil(() => updates.length === 3);
+ is(resource.mediaRules[0].matches, false, "Media query is not matched yet");
+
+ info("Change window size to fire matches-change event");
+ tab.ownerGlobal.resizeTo(originalWindowWidth, 500);
+ await waitUntil(() => updates.length === 4);
+
+ // Check the update content.
+ const isServerWatcher = Services.prefs.getBoolPref(
+ "devtools.testing.enableServerWatcherSupport"
+ );
+ const targetUpdate = updates[3];
+ assertUpdate(targetUpdate.update, {
+ resourceId: resource.resourceId,
+ updateType: "matches-change",
+ });
+ ok(resource === targetUpdate.resource, "Update object has the same resource");
+
+ if (isServerWatcher) {
+ is(
+ JSON.stringify(targetUpdate.update.nestedResourceUpdates[0].path),
+ JSON.stringify(["mediaRules", 0, "matches"]),
+ "path of nestedResourceUpdates is correct"
+ );
+ is(
+ targetUpdate.update.nestedResourceUpdates[0].value,
+ true,
+ "value of nestedResourceUpdates is correct"
+ );
+ } else {
+ is(
+ JSON.stringify(targetUpdate.update.nestedResourceUpdates[0].path),
+ JSON.stringify(["mediaRules", 0]),
+ "path of nestedResourceUpdates is correct"
+ );
+ is(
+ targetUpdate.update.nestedResourceUpdates[0].value.matches,
+ true,
+ "value of nestedResourceUpdates is correct"
+ );
+ }
+
+ // Check the resource.
+ const expectedMediaRules = [
+ {
+ conditionText: "(min-height: 400px)",
+ mediaText: "(min-height: 400px)",
+ matches: true,
+ },
+ ];
+
+ assertMediaRules(targetUpdate.resource.mediaRules, expectedMediaRules);
+
+ // Check the actual page.
+ const styleSheetResult = await getStyleSheetResult(tab);
+ is(
+ styleSheetResult.ruleCount,
+ 1,
+ "ruleCount of actual stylesheet is updated correctly"
+ );
+ assertMediaRules(styleSheetResult.mediaRules, expectedMediaRules);
+
+ tab.ownerGlobal.resizeTo(originalWindowWidth, originalWindowHeight);
+
+ await targetList.destroy();
+ await client.close();
+}
+
+function findMatchingExpectedResource(resource) {
+ return EXISTING_RESOURCES.find(
+ expected =>
+ resource.href === expected.href && resource.nodeHref === expected.nodeHref
+ );
+}
+
+async function getStyleSheetResult(tab) {
+ const result = await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ const document = content.document;
+ const stylesheet = document.styleSheets[0];
+ const ruleCount = stylesheet.cssRules.length;
+
+ const mediaRules = [];
+ for (const rule of stylesheet.cssRules) {
+ if (!rule.media) {
+ continue;
+ }
+
+ let matches = false;
+ try {
+ const mql = content.matchMedia(rule.media.mediaText);
+ matches = mql.matches;
+ } catch (e) {
+ // Ignored
+ }
+
+ mediaRules.push({
+ mediaText: rule.media.mediaText,
+ conditionText: rule.conditionText,
+ matches,
+ });
+ }
+
+ return { ruleCount, mediaRules };
+ });
+
+ return result;
+}
+
+function assertMediaRules(mediaRules, expected) {
+ is(mediaRules.length, expected.length, "Length of the mediaRules is correct");
+
+ for (let i = 0; i < mediaRules.length; i++) {
+ is(
+ mediaRules[i].conditionText,
+ expected[i].conditionText,
+ "conditionText is correct"
+ );
+ is(mediaRules[i].mediaText, expected[i].mediaText, "mediaText is correct");
+ is(mediaRules[i].matches, expected[i].matches, "matches is correct");
+
+ if (expected[i].line !== undefined) {
+ is(mediaRules[i].line, expected[i].line, "line is correct");
+ }
+
+ if (expected[i].column !== undefined) {
+ is(mediaRules[i].column, expected[i].column, "column is correct");
+ }
+ }
+}
+
+async function assertResource(resource, expected) {
+ is(
+ resource.resourceType,
+ ResourceWatcher.TYPES.STYLESHEET,
+ "Resource type is correct"
+ );
+ const styleSheetsFront = await resource.targetFront.getFront("stylesheets");
+ const styleText = (
+ await styleSheetsFront.getText(resource.resourceId)
+ ).str.trim();
+ is(styleText, expected.styleText, "Style text is correct");
+ is(resource.href, expected.href, "href is correct");
+ is(resource.nodeHref, expected.nodeHref, "nodeHref is correct");
+ is(resource.isNew, expected.isNew, "isNew is correct");
+ is(resource.disabled, expected.disabled, "disabled is correct");
+ is(resource.ruleCount, expected.ruleCount, "ruleCount is correct");
+ assertMediaRules(resource.mediaRules, expected.mediaRules);
+}
+
+function assertUpdate(update, expected) {
+ is(
+ update.resourceType,
+ ResourceWatcher.TYPES.STYLESHEET,
+ "Resource type is correct"
+ );
+ is(update.resourceId, expected.resourceId, "resourceId is correct");
+ is(update.updateType, expected.updateType, "updateType is correct");
+ if (expected.event?.cause) {
+ is(update.event?.cause, expected.event.cause, "cause is correct");
+ }
+}
diff --git a/devtools/shared/resources/tests/browser_resources_target_destroy.js b/devtools/shared/resources/tests/browser_resources_target_destroy.js
new file mode 100644
index 0000000000..9e993ef8cc
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_target_destroy.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the server ResourceWatcher are destroyed when the associated target actors
+// are destroyed.
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+add_task(async function() {
+ const tab = await addTab("data:text/html,Test");
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ // Start watching for console messages. We don't care about messages here, only the
+ // registration/destroy mechanism, so we make onAvailable a no-op function.
+ await resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CONSOLE_MESSAGE],
+ {
+ onAvailable: () => {},
+ }
+ );
+
+ info(
+ "Spawn a content task in order to be able to manipulate actors and resource watchers directly"
+ );
+ await ContentTask.spawn(tab.linkedBrowser, [], function() {
+ const { require } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+ const {
+ TargetActorRegistry,
+ } = require("devtools/server/actors/targets/target-actor-registry.jsm");
+ const {
+ getResourceWatcher,
+ TYPES,
+ } = require("devtools/server/actors/resources/index");
+
+ // Retrieve the target actor instance and its watcher for console messages
+ const targetActor = TargetActorRegistry.getTargetActor(
+ content.browsingContext.browserId
+ );
+ const watcher = getResourceWatcher(targetActor, TYPES.CONSOLE_MESSAGE);
+
+ // Storing the target actor in the global so we can retrieve it later, even if it
+ // was destroyed
+ content._testTargetActor = targetActor;
+
+ is(!!watcher, true, "The console message resource watcher was created");
+ });
+
+ info("Close the client, which will destroy the target");
+ targetList.destroy();
+ await client.close();
+
+ info(
+ "Spawn a content task in order to run some assertions on actors and resource watchers directly"
+ );
+ await ContentTask.spawn(tab.linkedBrowser, [], function() {
+ const { require } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+ const {
+ getResourceWatcher,
+ TYPES,
+ } = require("devtools/server/actors/resources/index");
+
+ ok(
+ content._testTargetActor && !content._testTargetActor.actorID,
+ "The target was destroyed when the client was closed"
+ );
+
+ // Retrieve the console message resource watcher
+ const watcher = getResourceWatcher(
+ content._testTargetActor,
+ TYPES.CONSOLE_MESSAGE
+ );
+
+ is(
+ !!watcher,
+ false,
+ "The console message resource watcher isn't registered anymore after the target was destroyed"
+ );
+
+ // Cleanup work variable
+ delete content._testTargetActor;
+ });
+});
diff --git a/devtools/shared/resources/tests/browser_resources_target_resources_race.js b/devtools/shared/resources/tests/browser_resources_target_resources_race.js
new file mode 100644
index 0000000000..c7e18f8ce1
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_target_resources_race.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+/**
+ * Test initial target resources are correctly retrieved even when several calls
+ * to watchResources are made simultaneously.
+ *
+ * This checks a race condition which occurred when calling watchResources
+ * simultaneously. This made the "second" call to watchResources miss existing
+ * resources (in case those are emitted from the target instead of the watcher).
+ * See Bug 1663896.
+ */
+add_task(async function() {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ const {
+ client,
+ resourceWatcher,
+ targetList,
+ } = await initMultiProcessResourceWatcher();
+
+ const expectedPlatformMessage = "expectedMessage";
+
+ info("Log a message *before* calling ResourceWatcher.watchResources");
+ Services.console.logStringMessage(expectedPlatformMessage);
+
+ info("Call watchResources from 2 separate call sites consecutively");
+
+ // Empty onAvailable callback for CSS MESSAGES, we only want to check that
+ // the second resource we watch correctly provides existing resources.
+ const onCssMessageAvailable = resources => {};
+
+ // First call to watchResources.
+ // We do not await on `watchPromise1` here, in order to simulate simultaneous
+ // calls to watchResources (which could come from 2 separate modules in a real
+ // scenario).
+ const initialWatchPromise = resourceWatcher.watchResources(
+ [ResourceWatcher.TYPES.CSS_MESSAGE],
+ {
+ onAvailable: onCssMessageAvailable,
+ }
+ );
+
+ // `waitForNextResource` will trigger another call to `watchResources`.
+ const onMessageReceived = waitForNextResource(
+ resourceWatcher,
+ ResourceWatcher.TYPES.PLATFORM_MESSAGE,
+ {
+ ignoreExistingResources: false,
+ predicate: r => r.message === expectedPlatformMessage,
+ }
+ );
+
+ info("Waiting for the expected message to be received");
+ await onMessageReceived;
+ ok(true, "All the expected messages were received");
+
+ info("Wait for the other watchResources promise to finish");
+ await initialWatchPromise;
+
+ // Unwatch all resources.
+ resourceWatcher.unwatchResources([ResourceWatcher.TYPES.CSS_MESSAGE], {
+ onAvailable: onCssMessageAvailable,
+ });
+
+ Services.console.reset();
+ targetList.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/resources/tests/browser_resources_target_switching.js b/devtools/shared/resources/tests/browser_resources_target_switching.js
new file mode 100644
index 0000000000..1388970dcd
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_target_switching.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the behavior of ResourceWatcher when the top level target changes
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+const { CONSOLE_MESSAGE, SOURCE } = ResourceWatcher.TYPES;
+
+const TEST_URI =
+ "data:text/html;charset=utf-8,<script>console.log('foo');</script>";
+
+add_task(async function() {
+ const tab = await addTab(TEST_URI);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Check the resources gotten from getAllResources at initial");
+ is(
+ resourceWatcher.getAllResources(CONSOLE_MESSAGE).length,
+ 0,
+ "There is no resources before calling watchResources"
+ );
+
+ info(
+ "Start to watch the available resources in order to compare with resources gotten from getAllResources"
+ );
+ const availableResources = [];
+ const onAvailable = resources => {
+ // Ignore message coming from shared worker started by previous tests and
+ // logging late a console message
+ resources
+ .filter(r => {
+ return !r.message.arguments[0].startsWith("[WORKER] started");
+ })
+ .map(r => availableResources.push(r));
+ };
+ await resourceWatcher.watchResources([CONSOLE_MESSAGE], { onAvailable });
+
+ is(availableResources.length, 1, "Got the page message");
+ is(
+ availableResources[0].message.arguments[0],
+ "foo",
+ "Got the expected page message"
+ );
+
+ // Register another listener before unregistering the console listener
+ // otherwise the resource watcher stop watching for targets
+ const onSourceAvailable = () => {};
+ await resourceWatcher.watchResources([SOURCE], {
+ onAvailable: onSourceAvailable,
+ });
+
+ info(
+ "Unregister the console listener and check that we no longer listen for console messages"
+ );
+ resourceWatcher.unwatchResources([CONSOLE_MESSAGE], {
+ onAvailable,
+ });
+
+ let onSwitched = targetList.once("switched-target");
+ info("Navigate to another process");
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:robots");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await onSwitched;
+
+ is(
+ availableResources.length,
+ 1,
+ "about:robots doesn't fire any new message, so we should have a new one"
+ );
+
+ info("Navigate back to data: URI");
+ onSwitched = targetList.once("switched-target");
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await onSwitched;
+
+ is(
+ availableResources.length,
+ 1,
+ "the data:URI fired a message, but we are no longer listening to it, so no new one should be notified"
+ );
+ is(
+ resourceWatcher.getAllResources(CONSOLE_MESSAGE).length,
+ 0,
+ "As we are no longer listening to CONSOLE message, we should not collect any"
+ );
+
+ resourceWatcher.unwatchResources([SOURCE], {
+ onAvailable: onSourceAvailable,
+ });
+
+ await targetList.destroy();
+ await client.close();
+});
diff --git a/devtools/shared/resources/tests/browser_resources_websocket.js b/devtools/shared/resources/tests/browser_resources_websocket.js
new file mode 100644
index 0000000000..2437836d79
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_resources_websocket.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the ResourceWatcher API around WEBSOCKET.
+
+const {
+ ResourceWatcher,
+} = require("devtools/shared/resources/resource-watcher");
+
+const TEST_URL = URL_ROOT + "websocket_frontend.html";
+const IS_NUMBER = "IS_NUMBER";
+
+add_task(async function() {
+ const tab = await addTab(TEST_URL);
+
+ const { client, resourceWatcher, targetList } = await initResourceWatcher(
+ tab
+ );
+
+ info("Check available resources at initial");
+ const availableResources = [];
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.WEBSOCKET], {
+ onAvailable: resources => availableResources.push(...resources),
+ });
+ is(
+ availableResources.length,
+ 0,
+ "Length of existing resources is correct at initial"
+ );
+
+ info("Check resource of opening websocket");
+ await ContentTask.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.openConnection();
+ });
+ await waitUntil(() => availableResources.length === 1);
+ const httpChannelId = availableResources[0].httpChannelId;
+ ok(httpChannelId, "httpChannelId is present in the resource");
+ assertResource(availableResources[0], {
+ wsMessageType: "webSocketOpened",
+ effectiveURI:
+ "ws://mochi.test:8888/browser/devtools/shared/resources/tests/websocket_backend",
+ extensions: "permessage-deflate",
+ protocols: "",
+ });
+
+ info("Check resource of sending/receiving the data via websocket");
+ await ContentTask.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.sendData("test");
+ });
+ await waitUntil(() => availableResources.length === 3);
+ assertResource(availableResources[1], {
+ wsMessageType: "frameSent",
+ httpChannelId,
+ data: {
+ type: "sent",
+ payload: "test",
+ },
+ });
+ assertResource(availableResources[2], {
+ wsMessageType: "frameReceived",
+ httpChannelId,
+ data: {
+ type: "received",
+ payload: "test",
+ },
+ });
+
+ info("Check resource of closing websocket");
+ await ContentTask.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.closeConnection();
+ });
+ await waitUntil(() => availableResources.length === 6);
+ assertResource(availableResources[3], {
+ wsMessageType: "frameSent",
+ httpChannelId,
+ data: {
+ type: "sent",
+ payload: "",
+ },
+ });
+ assertResource(availableResources[4], {
+ wsMessageType: "frameReceived",
+ httpChannelId,
+ data: {
+ type: "received",
+ payload: "",
+ },
+ });
+ assertResource(availableResources[5], {
+ wsMessageType: "webSocketClosed",
+ httpChannelId,
+ code: IS_NUMBER,
+ reason: "",
+ wasClean: true,
+ });
+
+ info("Check existing resources");
+ const existingResources = [];
+ await resourceWatcher.watchResources([ResourceWatcher.TYPES.WEBSOCKET], {
+ onAvailable: resources => existingResources.push(...resources),
+ });
+ is(
+ availableResources.length,
+ existingResources.length,
+ "Length of existing resources is correct"
+ );
+ for (let i = 0; i < availableResources.length; i++) {
+ const availableResource = availableResources[i];
+ const existingResource = existingResources[i];
+ ok(
+ availableResource === existingResource,
+ `The ${i}th resource is correct`
+ );
+ }
+
+ await targetList.destroy();
+ await client.close();
+});
+
+function assertResource(resource, expected) {
+ is(
+ resource.resourceType,
+ ResourceWatcher.TYPES.WEBSOCKET,
+ "Resource type is correct"
+ );
+
+ assertObject(resource, expected);
+}
+
+function assertObject(object, expected) {
+ for (const field in expected) {
+ if (typeof expected[field] === "object") {
+ assertObject(object[field], expected[field]);
+ } else if (expected[field] === IS_NUMBER) {
+ ok(!isNaN(object[field]), `The value of ${field} is number`);
+ } else {
+ is(object[field], expected[field], `The value of ${field} is correct`);
+ }
+ }
+}
diff --git a/devtools/shared/resources/tests/browser_target_list_browser_workers.js b/devtools/shared/resources/tests/browser_target_list_browser_workers.js
new file mode 100644
index 0000000000..fa85708165
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_target_list_browser_workers.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetList API around workers
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+const { TYPES } = TargetList;
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const WORKER_FILE = "test_worker.js";
+const CHROME_WORKER_URL = CHROME_URL_ROOT + WORKER_FILE;
+const SERVICE_WORKER_URL = URL_ROOT_SSL + "test_service_worker.js";
+
+add_task(async function() {
+ // Enabled fission's pref as the TargetList is almost disabled without it
+ await pushPref("devtools.browsertoolbox.fission", true);
+
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ const client = await createLocalClient();
+ const mainRoot = client.mainRoot;
+ const tab = await addTab(FISSION_TEST_URL);
+
+ info("Test TargetList against workers via the parent process target");
+
+ // Instantiate a worker in the parent process
+ // eslint-disable-next-line no-unused-vars
+ const worker = new Worker(CHROME_WORKER_URL + "#simple-worker");
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker(CHROME_WORKER_URL + "#shared-worker");
+
+ const targetDescriptor = await mainRoot.getMainProcess();
+ const target = await targetDescriptor.getTarget();
+ const targetList = new TargetList(mainRoot, target);
+ await targetList.startListening();
+
+ // Very naive sanity check against getAllTargets([workerType])
+ info("Check that getAllTargets returned the expected targets");
+ const workers = await targetList.getAllTargets([TYPES.WORKER]);
+ const hasWorker = workers.find(workerTarget => {
+ return workerTarget.url == CHROME_WORKER_URL + "#simple-worker";
+ });
+ ok(hasWorker, "retrieve the target for the worker");
+
+ const sharedWorkers = await targetList.getAllTargets([TYPES.SHARED_WORKER]);
+ const hasSharedWorker = sharedWorkers.find(workerTarget => {
+ return workerTarget.url == CHROME_WORKER_URL + "#shared-worker";
+ });
+ ok(hasSharedWorker, "retrieve the target for the shared worker");
+
+ const serviceWorkers = await targetList.getAllTargets([TYPES.SERVICE_WORKER]);
+ const hasServiceWorker = serviceWorkers.find(workerTarget => {
+ return workerTarget.url == SERVICE_WORKER_URL;
+ });
+ ok(hasServiceWorker, "retrieve the target for the service worker");
+
+ info(
+ "Check that calling getAllTargets again return the same target instances"
+ );
+ const workers2 = await targetList.getAllTargets([TYPES.WORKER]);
+ const sharedWorkers2 = await targetList.getAllTargets([TYPES.SHARED_WORKER]);
+ const serviceWorkers2 = await targetList.getAllTargets([
+ TYPES.SERVICE_WORKER,
+ ]);
+ is(workers2.length, workers.length, "retrieved the same number of workers");
+ is(
+ sharedWorkers2.length,
+ sharedWorkers.length,
+ "retrieved the same number of shared workers"
+ );
+ is(
+ serviceWorkers2.length,
+ serviceWorkers.length,
+ "retrieved the same number of service workers"
+ );
+
+ workers.sort(sortFronts);
+ workers2.sort(sortFronts);
+ sharedWorkers.sort(sortFronts);
+ sharedWorkers2.sort(sortFronts);
+ serviceWorkers.sort(sortFronts);
+ serviceWorkers2.sort(sortFronts);
+
+ for (let i = 0; i < workers.length; i++) {
+ is(workers[i], workers2[i], `worker ${i} targets are the same`);
+ }
+ for (let i = 0; i < sharedWorkers2.length; i++) {
+ is(
+ sharedWorkers[i],
+ sharedWorkers2[i],
+ `shared worker ${i} targets are the same`
+ );
+ }
+ for (let i = 0; i < serviceWorkers2.length; i++) {
+ is(
+ serviceWorkers[i],
+ serviceWorkers2[i],
+ `service worker ${i} targets are the same`
+ );
+ }
+
+ info(
+ "Check that watchTargets will call the create callback for all existing workers"
+ );
+ const targets = [];
+ const onAvailable = async ({ targetFront }) => {
+ ok(
+ targetFront.targetType === TYPES.WORKER ||
+ targetFront.targetType === TYPES.SHARED_WORKER ||
+ targetFront.targetType === TYPES.SERVICE_WORKER,
+ "We are only notified about worker targets"
+ );
+ ok(
+ targetFront == target ? targetFront.isTopLevel : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ targets.push(targetFront);
+ };
+ await targetList.watchTargets(
+ [TYPES.WORKER, TYPES.SHARED_WORKER, TYPES.SERVICE_WORKER],
+ onAvailable
+ );
+ is(
+ targets.length,
+ workers.length + sharedWorkers.length + serviceWorkers.length,
+ "retrieved the same number of workers via watchTargets"
+ );
+
+ targets.sort(sortFronts);
+ const allWorkers = workers
+ .concat(sharedWorkers, serviceWorkers)
+ .sort(sortFronts);
+
+ for (let i = 0; i < allWorkers.length; i++) {
+ is(
+ allWorkers[i],
+ targets[i],
+ `worker ${i} targets are the same via watchTargets`
+ );
+ }
+
+ targetList.unwatchTargets(
+ [TYPES.WORKER, TYPES.SHARED_WORKER, TYPES.SERVICE_WORKER],
+ onAvailable
+ );
+
+ // Create a new worker and see if the worker target is reported
+ const onWorkerCreated = new Promise(resolve => {
+ const onAvailable2 = async ({ targetFront }) => {
+ if (targets.includes(targetFront)) {
+ return;
+ }
+ targetList.unwatchTargets([TYPES.WORKER], onAvailable2);
+ resolve(targetFront);
+ };
+ targetList.watchTargets([TYPES.WORKER], onAvailable2);
+ });
+ // eslint-disable-next-line no-unused-vars
+ const worker2 = new Worker(CHROME_WORKER_URL + "#second");
+ info("Wait for the second worker to be created");
+ const workerTarget = await onWorkerCreated;
+
+ is(
+ workerTarget.url,
+ CHROME_WORKER_URL + "#second",
+ "This worker target is about the new worker"
+ );
+
+ const workers3 = await targetList.getAllTargets([TYPES.WORKER]);
+ const hasWorker2 = workers3.find(
+ ({ url }) => url == `${CHROME_WORKER_URL}#second`
+ );
+ ok(hasWorker2, "retrieve the target for tab via getAllTargets");
+
+ targetList.destroy();
+
+ info("Unregister service workers so they don't appear in other tests.");
+ await unregisterAllServiceWorkers(client);
+
+ await client.close();
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+});
+
+function sortFronts(f1, f2) {
+ return f1.actorID < f2.actorID;
+}
diff --git a/devtools/shared/resources/tests/browser_target_list_frames.js b/devtools/shared/resources/tests/browser_target_list_frames.js
new file mode 100644
index 0000000000..0549c67842
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_target_list_frames.js
@@ -0,0 +1,168 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetList API around frames
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "/fission_document.html";
+
+add_task(async function() {
+ // Enabled fission prefs
+ await pushPref("devtools.browsertoolbox.fission", true);
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ const client = await createLocalClient();
+ const mainRoot = client.mainRoot;
+
+ // Test fetching the frames from the main process target
+ await testBrowserFrames(mainRoot);
+
+ // Test fetching the frames from a tab target
+ await testTabFrames(mainRoot);
+
+ await client.close();
+});
+
+async function testBrowserFrames(mainRoot) {
+ info("Test TargetList against frames via the parent process target");
+
+ const targetDescriptor = await mainRoot.getMainProcess();
+ const target = await targetDescriptor.getTarget();
+ const targetList = new TargetList(mainRoot, target);
+ await targetList.startListening();
+
+ // Very naive sanity check against getAllTargets([frame])
+ const frames = await targetList.getAllTargets([TargetList.TYPES.FRAME]);
+ const hasBrowserDocument = frames.find(
+ frameTarget => frameTarget.url == window.location.href
+ );
+ ok(hasBrowserDocument, "retrieve the target for the browser document");
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames2 = await targetList.getAllTargets([TargetList.TYPES.FRAME]);
+ is(frames2.length, frames.length, "retrieved the same number of frames");
+
+ function sortFronts(f1, f2) {
+ return f1.actorID < f2.actorID;
+ }
+ frames.sort(sortFronts);
+ frames2.sort(sortFronts);
+ for (let i = 0; i < frames.length; i++) {
+ is(frames[i], frames2[i], `frame ${i} targets are the same`);
+ }
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const onAvailable = ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TargetList.TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ targetFront == target ? targetFront.isTopLevel : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ targets.push(targetFront);
+ };
+ await targetList.watchTargets([TargetList.TYPES.FRAME], onAvailable);
+ is(
+ targets.length,
+ frames.length,
+ "retrieved the same number of frames via watchTargets"
+ );
+
+ frames.sort(sortFronts);
+ targets.sort(sortFronts);
+ for (let i = 0; i < frames.length; i++) {
+ is(
+ frames[i],
+ targets[i],
+ `frame ${i} targets are the same via watchTargets`
+ );
+ }
+ targetList.unwatchTargets([TargetList.TYPES.FRAME], onAvailable);
+
+ /* NOT READY YET, need to implement frame listening
+ // Open a new tab and see if the frame target is reported by watchTargets and getAllTargets
+ const tab = await addTab(TEST_URL);
+
+ is(targets.length, frames.length + 1, "Opening a tab reported a new frame");
+ is(targets[targets.length - 1].url, TEST_URL, "This frame target is about the new tab");
+
+ const frames3 = await targetList.getAllTargets([TargetList.TYPES.FRAME]);
+ const hasTabDocument = frames3.find(target => target.url == TEST_URL);
+ ok(hasTabDocument, "retrieve the target for tab via getAllTargets");
+ */
+
+ targetList.destroy();
+ await waitForAllTargetsToBeAttached(targetList);
+}
+
+async function testTabFrames(mainRoot) {
+ info("Test TargetList against frames via a tab target");
+
+ // Create a TargetList for a given test tab
+ const tab = await addTab(FISSION_TEST_URL);
+ const descriptor = await mainRoot.getTab({ tab });
+ const target = await descriptor.getTarget();
+ const targetList = new TargetList(mainRoot, target);
+
+ await targetList.startListening();
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames = await targetList.getAllTargets([TargetList.TYPES.FRAME]);
+ // When fission is enabled, we also get the remote example.org iframe.
+ const expectedFramesCount = isFissionEnabled() ? 2 : 1;
+ is(
+ frames.length,
+ expectedFramesCount,
+ "retrieved only the top level document"
+ );
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const onAvailable = ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TargetList.TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ targetFront == target ? targetFront.isTopLevel : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ targets.push(targetFront);
+ };
+ await targetList.watchTargets([TargetList.TYPES.FRAME], onAvailable);
+ is(
+ targets.length,
+ frames.length,
+ "retrieved the same number of frames via watchTargets"
+ );
+
+ for (const frame of frames) {
+ ok(
+ targets.find(t => t === frame),
+ "frame " + frame.actorID + " target is the same via watchTargets"
+ );
+ }
+ targetList.unwatchTargets([TargetList.TYPES.FRAME], onAvailable);
+
+ targetList.destroy();
+ await waitForAllTargetsToBeAttached(targetList);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+ BrowserTestUtils.removeTab(tab);
+}
diff --git a/devtools/shared/resources/tests/browser_target_list_getAllTargets.js b/devtools/shared/resources/tests/browser_target_list_getAllTargets.js
new file mode 100644
index 0000000000..85822a7ede
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_target_list_getAllTargets.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetList API getAllTargets.
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+const { ALL_TYPES, TYPES } = TargetList;
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const CHROME_WORKER_URL = CHROME_URL_ROOT + "test_worker.js";
+
+add_task(async function() {
+ // Enabled devtools.browsertoolbox.fission to listen to all target types.
+ await pushPref("devtools.browsertoolbox.fission", true);
+
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ info("Setup the test page with workers of all types");
+ const client = await createLocalClient();
+ const mainRoot = client.mainRoot;
+
+ const tab = await addTab(FISSION_TEST_URL);
+
+ // Instantiate a worker in the parent process
+ // eslint-disable-next-line no-unused-vars
+ const worker = new Worker(CHROME_WORKER_URL + "#simple-worker");
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker(CHROME_WORKER_URL + "#shared-worker");
+
+ info("Create a target list for the main process target");
+ const targetDescriptor = await mainRoot.getMainProcess();
+ const target = await targetDescriptor.getTarget();
+ const targetList = new TargetList(mainRoot, target);
+ await targetList.startListening();
+
+ info("Check getAllTargets will throw when providing invalid arguments");
+ Assert.throws(
+ () => targetList.getAllTargets(),
+ e => e.message === "getAllTargets expects a non-empty array of types"
+ );
+
+ Assert.throws(
+ () => targetList.getAllTargets([]),
+ e => e.message === "getAllTargets expects a non-empty array of types"
+ );
+
+ info("Check getAllTargets returns consistent results with several types");
+ const workerTargets = targetList.getAllTargets([TYPES.WORKER]);
+ const serviceWorkerTargets = targetList.getAllTargets([TYPES.SERVICE_WORKER]);
+ const sharedWorkerTargets = targetList.getAllTargets([TYPES.SHARED_WORKER]);
+ const processTargets = targetList.getAllTargets([TYPES.PROCESS]);
+ const frameTargets = targetList.getAllTargets([TYPES.FRAME]);
+
+ const allWorkerTargetsReference = [
+ ...workerTargets,
+ ...serviceWorkerTargets,
+ ...sharedWorkerTargets,
+ ];
+ const allWorkerTargets = targetList.getAllTargets([
+ TYPES.WORKER,
+ TYPES.SERVICE_WORKER,
+ TYPES.SHARED_WORKER,
+ ]);
+
+ is(
+ allWorkerTargets.length,
+ allWorkerTargetsReference.length,
+ "getAllTargets([worker, service, shared]) returned the expected number of targets"
+ );
+
+ ok(
+ allWorkerTargets.every(t => allWorkerTargetsReference.includes(t)),
+ "getAllTargets([worker, service, shared]) returned the expected targets"
+ );
+
+ const allTargetsReference = [
+ ...allWorkerTargets,
+ ...processTargets,
+ ...frameTargets,
+ ];
+ const allTargets = targetList.getAllTargets(ALL_TYPES);
+ is(
+ allTargets.length,
+ allTargetsReference.length,
+ "getAllTargets(TYPES.ALL_TYPES) returned the expected number of targets"
+ );
+
+ ok(
+ allTargets.every(t => allTargetsReference.includes(t)),
+ "getAllTargets(TYPES.ALL_TYPES) returned the expected targets"
+ );
+
+ targetList.destroy();
+
+ // Wait for all the targets to be fully attached so we don't have pending requests.
+ await waitForAllTargetsToBeAttached(targetList);
+
+ await client.close();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+});
diff --git a/devtools/shared/resources/tests/browser_target_list_preffedoff.js b/devtools/shared/resources/tests/browser_target_list_preffedoff.js
new file mode 100644
index 0000000000..244ffe6c53
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_target_list_preffedoff.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetList API when DevTools Fission preference is false
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+
+add_task(async function() {
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ const client = await createLocalClient();
+ const mainRoot = client.mainRoot;
+ const targetDescriptor = await mainRoot.getMainProcess();
+ const mainProcess = await targetDescriptor.getTarget();
+
+ // Assert the limited behavior of this API with fission preffed off
+ await pushPref("devtools.browsertoolbox.fission", false);
+
+ // Test with Main process targets as top level target
+ await testPreffedOffMainProcess(mainRoot, mainProcess);
+
+ await client.close();
+});
+
+async function testPreffedOffMainProcess(mainRoot, mainProcess) {
+ info(
+ "Test TargetList when devtools's fission pref is false, via the parent process target"
+ );
+
+ const targetList = new TargetList(mainRoot, mainProcess);
+ await targetList.startListening();
+
+ // The API should only report the top level target,
+ // i.e. the Main process target, which is considered as frame
+ // and not as process.
+ const processes = await targetList.getAllTargets([TargetList.TYPES.PROCESS]);
+ is(
+ processes.length,
+ 0,
+ "We only get a frame target for the top level target"
+ );
+ const frames = await targetList.getAllTargets([TargetList.TYPES.FRAME]);
+ is(frames.length, 1, "We get only one frame when preffed-off");
+ is(
+ frames[0],
+ mainProcess,
+ "The target is the top level one via getAllTargets"
+ );
+
+ const processTargets = [];
+ const onProcessAvailable = ({ targetFront }) => {
+ processTargets.push(targetFront);
+ };
+ await targetList.watchTargets([TargetList.TYPES.PROCESS], onProcessAvailable);
+ is(processTargets.length, 0, "We get no process when preffed-off");
+ targetList.unwatchTargets([TargetList.TYPES.PROCESS], onProcessAvailable);
+
+ const frameTargets = [];
+ const onFrameAvailable = ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TargetList.TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ targetFront.isTopLevel,
+ "We are only notified about the top level target"
+ );
+ frameTargets.push(targetFront);
+ };
+ await targetList.watchTargets([TargetList.TYPES.FRAME], onFrameAvailable);
+ is(
+ frameTargets.length,
+ 1,
+ "We get one frame via watchTargets when preffed-off"
+ );
+ is(
+ frameTargets[0],
+ mainProcess,
+ "The target is the top level one via watchTargets"
+ );
+ targetList.unwatchTargets([TargetList.TYPES.FRAME], onFrameAvailable);
+
+ targetList.destroy();
+}
diff --git a/devtools/shared/resources/tests/browser_target_list_processes.js b/devtools/shared/resources/tests/browser_target_list_processes.js
new file mode 100644
index 0000000000..f3614b399d
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_target_list_processes.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetList API around processes
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
+
+add_task(async function() {
+ // Enabled fission's pref as the TargetList is almost disabled without it
+ await pushPref("devtools.browsertoolbox.fission", true);
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ const client = await createLocalClient();
+ const mainRoot = client.mainRoot;
+ const targetDescriptor = await mainRoot.getMainProcess();
+ const mainProcess = await targetDescriptor.getTarget();
+
+ const targetList = new TargetList(mainRoot, mainProcess);
+ await targetList.startListening();
+
+ await testProcesses(targetList, mainProcess);
+
+ await targetList.destroy();
+ // Wait for all the targets to be fully attached so we don't have pending requests.
+ await Promise.all(
+ targetList
+ .getAllTargets(targetList.ALL_TYPES)
+ .map(t => t.attachAndInitThread(targetList))
+ );
+
+ await client.close();
+});
+
+async function testProcesses(targetList, target) {
+ info("Test TargetList against processes");
+
+ // Note that ppmm also includes the parent process, which is considered as a frame rather than a process
+ const originalProcessesCount = Services.ppmm.childCount - 1;
+ const processes = await targetList.getAllTargets([TargetList.TYPES.PROCESS]);
+ is(
+ processes.length,
+ originalProcessesCount,
+ "Get a target for all content processes"
+ );
+
+ const processes2 = await targetList.getAllTargets([TargetList.TYPES.PROCESS]);
+ is(
+ processes2.length,
+ originalProcessesCount,
+ "retrieved the same number of processes"
+ );
+ function sortFronts(f1, f2) {
+ return f1.actorID < f2.actorID;
+ }
+ processes.sort(sortFronts);
+ processes2.sort(sortFronts);
+ for (let i = 0; i < processes.length; i++) {
+ is(processes[i], processes2[i], `process ${i} targets are the same`);
+ }
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = new Set();
+ const onAvailable = ({ targetFront }) => {
+ if (targets.has(targetFront)) {
+ ok(false, "The same target is notified multiple times via onAvailable");
+ }
+ is(
+ targetFront.targetType,
+ TargetList.TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ ok(
+ targetFront == target ? targetFront.isTopLevel : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ targets.add(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ if (!targets.has(targetFront)) {
+ ok(
+ false,
+ "A target is declared destroyed via onDestroyed without being notified via onAvailable"
+ );
+ }
+ is(
+ targetFront.targetType,
+ TargetList.TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ ok(
+ !targetFront.isTopLevel,
+ "We are never notified about the top level target destruction"
+ );
+ targets.delete(targetFront);
+ };
+ await targetList.watchTargets(
+ [TargetList.TYPES.PROCESS],
+ onAvailable,
+ onDestroyed
+ );
+ is(
+ targets.size,
+ originalProcessesCount,
+ "retrieved the same number of processes via watchTargets"
+ );
+ for (let i = 0; i < processes.length; i++) {
+ ok(
+ targets.has(processes[i]),
+ `process ${i} targets are the same via watchTargets`
+ );
+ }
+
+ const previousTargets = new Set(targets);
+ // Assert that onAvailable is called for processes created *after* the call to watchTargets
+ const onProcessCreated = new Promise(resolve => {
+ const onAvailable2 = ({ targetFront }) => {
+ if (previousTargets.has(targetFront)) {
+ return;
+ }
+ targetList.unwatchTargets([TargetList.TYPES.PROCESS], onAvailable2);
+ resolve(targetFront);
+ };
+ targetList.watchTargets([TargetList.TYPES.PROCESS], onAvailable2);
+ });
+ const tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+ const createdTarget = await onProcessCreated;
+ // For some reason, creating a new tab purges processes created from previous tests
+ // so it is not reasonable to assert the size of `targets` as it may be lower than expected.
+ ok(targets.has(createdTarget), "The new tab process is in the list");
+
+ const processCountAfterTabOpen = targets.size;
+
+ // Assert that onDestroyed is called for destroyed processes
+ const onProcessDestroyed = new Promise(resolve => {
+ const onAvailable3 = () => {};
+ const onDestroyed3 = ({ targetFront }) => {
+ resolve(targetFront);
+ targetList.unwatchTargets(
+ [TargetList.TYPES.PROCESS],
+ onAvailable3,
+ onDestroyed3
+ );
+ };
+ targetList.watchTargets(
+ [TargetList.TYPES.PROCESS],
+ onAvailable3,
+ onDestroyed3
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ const destroyedTarget = await onProcessDestroyed;
+ is(
+ targets.size,
+ processCountAfterTabOpen - 1,
+ "The closed tab's process has been reported as destroyed"
+ );
+ ok(
+ !targets.has(destroyedTarget),
+ "The destroyed target is no longer in the list"
+ );
+ is(
+ destroyedTarget,
+ createdTarget,
+ "The destroyed target is the one that has been reported as created"
+ );
+
+ targetList.unwatchTargets(
+ [TargetList.TYPES.PROCESS],
+ onAvailable,
+ onDestroyed
+ );
+
+ // Ensure that getAllTargets still works after the call to unwatchTargets
+ const processes3 = await targetList.getAllTargets([TargetList.TYPES.PROCESS]);
+ is(
+ processes3.length,
+ processCountAfterTabOpen - 1,
+ "getAllTargets reports a new target"
+ );
+}
diff --git a/devtools/shared/resources/tests/browser_target_list_service_workers.js b/devtools/shared/resources/tests/browser_target_list_service_workers.js
new file mode 100644
index 0000000000..7af04144b1
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_target_list_service_workers.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetList API for service workers in content tabs.
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+const { TYPES } = TargetList;
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+
+add_task(async function() {
+ // Enabled devtools.browsertoolbox.fission to listen to all target types.
+ await pushPref("devtools.browsertoolbox.fission", true);
+
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ info("Setup the test page with workers of all types");
+ const client = await createLocalClient();
+ const mainRoot = client.mainRoot;
+
+ const tab = await addTab(FISSION_TEST_URL);
+
+ info("Create a target list for a tab target");
+ const descriptor = await mainRoot.getTab({ tab });
+ const target = await descriptor.getTarget();
+ const targetList = new TargetList(mainRoot, target);
+
+ // Enable Service Worker listening.
+ targetList.listenForServiceWorkers = true;
+ await targetList.startListening();
+
+ const serviceWorkerTargets = targetList.getAllTargets([TYPES.SERVICE_WORKER]);
+ is(serviceWorkerTargets.length, 1, "TargetList has 1 service worker target");
+
+ info("Check that the onAvailable is done when watchTargets resolves");
+ const targets = [];
+ const onAvailable = async ({ targetFront }) => {
+ // Wait for one second here to check that watch targets waits for
+ // the onAvailable callbacks correctly.
+ await wait(1000);
+ targets.push(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) =>
+ targets.splice(targets.indexOf(targetFront), 1);
+
+ await targetList.watchTargets(
+ [TYPES.SERVICE_WORKER],
+ onAvailable,
+ onDestroyed
+ );
+
+ // We expect onAvailable to have been called one time, for the only service
+ // worker target available in the test page.
+ is(targets.length, 1, "onAvailable has resolved");
+ is(
+ targets[0],
+ serviceWorkerTargets[0],
+ "onAvailable was called with the expected service worker target"
+ );
+
+ info("Unregister the worker and wait until onDestroyed is called.");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+ await waitUntil(() => targets.length === 0);
+
+ // Stop listening to avoid worker related requests
+ targetList.destroy();
+
+ await client.waitForRequestsToSettle();
+
+ await client.close();
+});
diff --git a/devtools/shared/resources/tests/browser_target_list_service_workers_navigation.js b/devtools/shared/resources/tests/browser_target_list_service_workers_navigation.js
new file mode 100644
index 0000000000..dc9afdfe4c
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_target_list_service_workers_navigation.js
@@ -0,0 +1,393 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetList API for service workers when navigating in content tabs.
+// When the top level target navigates, we manually call onTargetAvailable for
+// service workers which now match the page domain. We assert that the callbacks
+// will be called the expected number of times here.
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+const { TYPES } = TargetList;
+
+const COM_PAGE_URL = URL_ROOT_SSL + "test_sw_page.html";
+const COM_WORKER_URL = URL_ROOT_SSL + "test_sw_page_worker.js";
+const ORG_PAGE_URL = URL_ROOT_ORG_SSL + "test_sw_page.html";
+const ORG_WORKER_URL = URL_ROOT_ORG_SSL + "test_sw_page_worker.js";
+
+/**
+ * This test will navigate between two pages, both controlled by different
+ * service workers.
+ *
+ * The steps will be:
+ * - navigate to .com page
+ * - create target list
+ * - navigate to .org page
+ * - reload .org page
+ * - unregister .org worker
+ * - navigate back to .com page
+ * - unregister .com worker
+ *
+ * First we test this with destroyServiceWorkersOnNavigation = false.
+ * In this case we expect the following calls:
+ * - navigate to .com page
+ * - create target list
+ * - onAvailable should be called for the .com worker
+ * - navigate to .org page
+ * - onAvailable should be called for the .org worker
+ * - reload .org page
+ * - nothing should happen
+ * - unregister .org worker
+ * - onDestroyed should be called for the .org worker
+ * - navigate back to .com page
+ * - nothing should happen
+ * - unregister .com worker
+ * - onDestroyed should be called for the .com worker
+ */
+add_task(async function test_NavigationBetweenTwoDomains_NoDestroy() {
+ const { client, mainRoot } = await setupServiceWorkerNavigationTest();
+
+ const tab = await addTab(COM_PAGE_URL);
+
+ const { hooks, targetList } = await watchServiceWorkerTargets({
+ mainRoot,
+ tab,
+ destroyServiceWorkersOnNavigation: false,
+ });
+
+ // We expect onAvailable to have been called one time, for the only service
+ // worker target available in the test page.
+ await checkHooks(hooks, {
+ available: 1,
+ destroyed: 0,
+ targets: [COM_WORKER_URL],
+ });
+
+ info("Go to .org page, wait for onAvailable to be called");
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, ORG_PAGE_URL);
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 0,
+ targets: [COM_WORKER_URL, ORG_WORKER_URL],
+ });
+
+ info("Reload .org page, onAvailable and onDestroyed should not be called");
+ const reloaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.reloadTab(gBrowser.selectedTab);
+ await reloaded;
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 0,
+ targets: [COM_WORKER_URL, ORG_WORKER_URL],
+ });
+
+ info("Unregister .org service worker and wait until onDestroyed is called.");
+ await unregisterServiceWorker(tab, ORG_PAGE_URL);
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 1,
+ targets: [COM_WORKER_URL],
+ });
+
+ info("Go back to page 1");
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, COM_PAGE_URL);
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 1,
+ targets: [COM_WORKER_URL],
+ });
+
+ info("Unregister .com service worker and wait until onDestroyed is called.");
+ await unregisterServiceWorker(tab, COM_PAGE_URL);
+ await checkHooks(hooks, { available: 2, destroyed: 2, targets: [] });
+
+ // Stop listening to avoid worker related requests
+ targetList.destroy();
+
+ await client.waitForRequestsToSettle();
+ await client.close();
+ await removeTab(tab);
+});
+
+/**
+ * Same scenario as test_NavigationBetweenTwoDomains_NoDestroy, but this time
+ * with destroyServiceWorkersOnNavigation set to true.
+ *
+ * In this case we expect the following calls:
+ * - navigate to .com page
+ * - create target list
+ * - onAvailable should be called for the .com worker
+ * - navigate to .org page
+ * - onDestroyed should be called for the .com worker
+ * - onAvailable should be called for the .org worker
+ * - reload .org page
+ * - onDestroyed & onAvailable should be called for the .org worker
+ * - unregister .org worker
+ * - onDestroyed should be called for the .org worker
+ * - navigate back to .com page
+ * - onAvailable should be called for the .com worker
+ * - unregister .com worker
+ * - onDestroyed should be called for the .com worker
+ */
+add_task(async function test_NavigationBetweenTwoDomains_WithDestroy() {
+ const { client, mainRoot } = await setupServiceWorkerNavigationTest();
+
+ const tab = await addTab(COM_PAGE_URL);
+
+ const { hooks, targetList } = await watchServiceWorkerTargets({
+ mainRoot,
+ tab,
+ destroyServiceWorkersOnNavigation: true,
+ });
+
+ // We expect onAvailable to have been called one time, for the only service
+ // worker target available in the test page.
+ await checkHooks(hooks, {
+ available: 1,
+ destroyed: 0,
+ targets: [COM_WORKER_URL],
+ });
+
+ info("Go to .org page, wait for onAvailable to be called");
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, ORG_PAGE_URL);
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 1,
+ targets: [ORG_WORKER_URL],
+ });
+
+ info("Reload .org page, onAvailable and onDestroyed should be called");
+ gBrowser.reloadTab(gBrowser.selectedTab);
+ await checkHooks(hooks, {
+ available: 3,
+ destroyed: 2,
+ targets: [ORG_WORKER_URL],
+ });
+
+ info("Unregister .org service worker and wait until onDestroyed is called.");
+ await unregisterServiceWorker(tab, ORG_PAGE_URL);
+ await checkHooks(hooks, { available: 3, destroyed: 3, targets: [] });
+
+ info("Go back to page 1, wait for onDestroyed and onAvailable to be called");
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, COM_PAGE_URL);
+ await checkHooks(hooks, {
+ available: 4,
+ destroyed: 3,
+ targets: [COM_WORKER_URL],
+ });
+
+ info("Unregister .com service worker and wait until onDestroyed is called.");
+ await unregisterServiceWorker(tab, COM_PAGE_URL);
+ await checkHooks(hooks, { available: 4, destroyed: 4, targets: [] });
+
+ // Stop listening to avoid worker related requests
+ targetList.destroy();
+
+ await client.waitForRequestsToSettle();
+ await client.close();
+ await removeTab(tab);
+});
+
+/**
+ * In this test we load a service worker in a page prior to starting the
+ * TargetList. We start the target list on another page, and then we go back to
+ * the first page. We want to check that we are correctly notified about the
+ * worker that was spawned before TargetList.
+ *
+ * Steps:
+ * - navigate to .com page
+ * - navigate to .org page
+ * - create target list
+ * - unregister .org worker
+ * - navigate back to .com page
+ * - unregister .com worker
+ *
+ * The expected calls are the same whether destroyServiceWorkersOnNavigation is
+ * true or false.
+ *
+ * Expected calls:
+ * - navigate to .com page
+ * - navigate to .org page
+ * - create target list
+ * - onAvailable is called for the .org worker
+ * - unregister .org worker
+ * - onDestroyed is called for the .org worker
+ * - navigate back to .com page
+ * - onAvailable is called for the .com worker
+ * - unregister .com worker
+ * - onDestroyed is called for the .com worker
+ */
+add_task(async function test_NavigationToPageWithExistingWorker_NoDestroy() {
+ await testNavigationToPageWithExistingWorker({
+ destroyServiceWorkersOnNavigation: false,
+ });
+});
+
+add_task(async function test_NavigationToPageWithExistingWorker_WithDestroy() {
+ await testNavigationToPageWithExistingWorker({
+ destroyServiceWorkersOnNavigation: true,
+ });
+});
+
+async function testNavigationToPageWithExistingWorker({
+ destroyServiceWorkersOnNavigation,
+}) {
+ const { client, mainRoot } = await setupServiceWorkerNavigationTest();
+
+ const tab = await addTab(COM_PAGE_URL);
+
+ info("Wait until the service worker registration is registered");
+ await waitForRegistrationReady(tab, COM_PAGE_URL);
+
+ info("Navigate to another page");
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, ORG_PAGE_URL);
+
+ // Avoid TV failures, where target list still starts thinking that the
+ // current domain is .com .
+ info("Wait until we have fully navigated to the .org page");
+ await waitForRegistrationReady(tab, ORG_PAGE_URL);
+
+ const { hooks, targetList } = await watchServiceWorkerTargets({
+ mainRoot,
+ tab,
+ destroyServiceWorkersOnNavigation,
+ });
+
+ // We expect onAvailable to have been called one time, for the only service
+ // worker target available in the test page.
+ await checkHooks(hooks, {
+ available: 1,
+ destroyed: 0,
+ targets: [ORG_WORKER_URL],
+ });
+
+ info("Unregister .org service worker and wait until onDestroyed is called.");
+ await unregisterServiceWorker(tab, ORG_PAGE_URL);
+ await checkHooks(hooks, { available: 1, destroyed: 1, targets: [] });
+
+ info("Go back .com page, wait for onAvailable to be called");
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, COM_PAGE_URL);
+ await checkHooks(hooks, {
+ available: 2,
+ destroyed: 1,
+ targets: [COM_WORKER_URL],
+ });
+
+ info("Unregister .com service worker and wait until onDestroyed is called.");
+ await unregisterServiceWorker(tab, COM_PAGE_URL);
+ await checkHooks(hooks, { available: 2, destroyed: 2, targets: [] });
+
+ // Stop listening to avoid worker related requests
+ targetList.destroy();
+
+ await client.waitForRequestsToSettle();
+ await client.close();
+ await removeTab(tab);
+}
+
+async function setupServiceWorkerNavigationTest() {
+ // Enabled devtools.browsertoolbox.fission to listen to all target types.
+ await pushPref("devtools.browsertoolbox.fission", true);
+
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ info("Setup the test page with workers of all types");
+ const client = await createLocalClient();
+ const mainRoot = client.mainRoot;
+ return { client, mainRoot };
+}
+
+async function watchServiceWorkerTargets({
+ destroyServiceWorkersOnNavigation,
+ mainRoot,
+ tab,
+}) {
+ info("Create a target list for a tab target");
+ const descriptor = await mainRoot.getTab({ tab });
+ const target = await descriptor.getTarget();
+ const targetList = new TargetList(mainRoot, target);
+
+ // Enable Service Worker listening.
+ targetList.listenForServiceWorkers = true;
+ info(
+ "Set targetList.destroyServiceWorkersOnNavigation to " +
+ destroyServiceWorkersOnNavigation
+ );
+ targetList.destroyServiceWorkersOnNavigation = destroyServiceWorkersOnNavigation;
+ await targetList.startListening();
+
+ // Setup onAvailable & onDestroyed callbacks so that we can check how many
+ // times they are called and with which targetFront.
+ const hooks = {
+ availableCount: 0,
+ destroyedCount: 0,
+ targets: [],
+ };
+
+ const onAvailable = async ({ targetFront }) => {
+ hooks.availableCount++;
+ hooks.targets.push(targetFront);
+ };
+
+ const onDestroyed = ({ targetFront }) => {
+ hooks.destroyedCount++;
+ hooks.targets.splice(hooks.targets.indexOf(targetFront), 1);
+ };
+
+ await targetList.watchTargets(
+ [TYPES.SERVICE_WORKER],
+ onAvailable,
+ onDestroyed
+ );
+
+ return { hooks, targetList };
+}
+
+async function unregisterServiceWorker(tab, expectedPageUrl) {
+ await waitForRegistrationReady(tab, expectedPageUrl);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+}
+
+/**
+ * Wait until the expected URL is loaded and win.registration has resolved.
+ */
+async function waitForRegistrationReady(tab, expectedPageUrl) {
+ await asyncWaitUntil(() =>
+ SpecialPowers.spawn(tab.linkedBrowser, [expectedPageUrl], function(_url) {
+ try {
+ const win = content.wrappedJSObject;
+ const isExpectedUrl = win.location.href === _url;
+ const hasRegistration = !!win.registrationPromise;
+ return isExpectedUrl && hasRegistration;
+ } catch (e) {
+ return false;
+ }
+ })
+ );
+}
+
+/**
+ * Assert helper for the `hooks` object, updated by the onAvailable and
+ * onDestroyed callbacks. Assert that the callbacks have been called the
+ * expected number of times, with the expected targets.
+ */
+async function checkHooks(hooks, { available, destroyed, targets }) {
+ info(`Wait for availableCount=${available} and destroyedCount=${destroyed}`);
+ await waitUntil(
+ () => hooks.availableCount == available && hooks.destroyedCount == destroyed
+ );
+ is(hooks.availableCount, available, "onAvailable was called as expected");
+ is(hooks.destroyedCount, destroyed, "onDestroyed was called as expected");
+
+ is(hooks.targets.length, targets.length, "Expected number of targets");
+ targets.forEach((url, i) => {
+ is(hooks.targets[i].url, url, `SW target ${i} has the expected url`);
+ });
+}
diff --git a/devtools/shared/resources/tests/browser_target_list_switchToTarget.js b/devtools/shared/resources/tests/browser_target_list_switchToTarget.js
new file mode 100644
index 0000000000..ea166e40fc
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_target_list_switchToTarget.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetList API switchToTarget function
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+
+add_task(async function() {
+ const client = await createLocalClient();
+
+ await testSwitchToTarget(client);
+
+ await client.close();
+});
+
+async function testSwitchToTarget(client) {
+ info("Test TargetList.switchToTarget method");
+
+ const { mainRoot } = client;
+ // Create a first target to switch from, a new tab with an iframe
+ const firstTab = await addTab(
+ `data:text/html,<iframe src="data:text/html,foo"></iframe>`
+ );
+ const firstDescriptor = await mainRoot.getTab({ tab: gBrowser.selectedTab });
+ const firstTarget = await firstDescriptor.getTarget();
+
+ const targetList = new TargetList(mainRoot, firstTarget);
+
+ await targetList.startListening();
+
+ is(
+ targetList.targetFront,
+ firstTarget,
+ "The target list top level target is the main process one"
+ );
+
+ // Create a second target to switch to, a new tab with an iframe
+ const secondTab = await addTab(
+ `data:text/html,<iframe src="data:text/html,bar"></iframe>`
+ );
+ const secondDescriptor = await mainRoot.getTab({ tab: gBrowser.selectedTab });
+ const secondTarget = await secondDescriptor.getTarget();
+
+ const frameTargets = [];
+ let currentTarget = firstTarget;
+ const onFrameAvailable = ({ targetFront, isTargetSwitching }) => {
+ is(
+ targetFront.targetType,
+ TargetList.TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ targetFront == currentTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ if (targetFront.isTopLevel) {
+ // When calling watchTargets, this will be false, but it will be true when calling switchToTarget
+ is(
+ isTargetSwitching,
+ currentTarget == secondTarget,
+ "target switching boolean is correct"
+ );
+ } else {
+ ok(!isTargetSwitching, "for now, only top level target can be switched");
+ }
+ frameTargets.push(targetFront);
+ };
+ const destroyedTargets = [];
+ const onFrameDestroyed = ({ targetFront, isTargetSwitching }) => {
+ is(
+ targetFront.targetType,
+ TargetList.TYPES.FRAME,
+ "target-destroyed: We are only notified about frame targets"
+ );
+ ok(
+ targetFront == firstTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "target-destroyed: isTopLevel property is correct"
+ );
+ if (targetFront.isTopLevel) {
+ is(
+ isTargetSwitching,
+ true,
+ "target-destroyed: target switching boolean is correct"
+ );
+ } else {
+ ok(
+ !isTargetSwitching,
+ "target-destroyed: for now, only top level target can be switched"
+ );
+ }
+ destroyedTargets.push(targetFront);
+ };
+ await targetList.watchTargets(
+ [TargetList.TYPES.FRAME],
+ onFrameAvailable,
+ onFrameDestroyed
+ );
+
+ // Save the original list of targets
+ const createdTargets = [...frameTargets];
+ // Clear the recorded target list of all existing targets
+ frameTargets.length = 0;
+
+ currentTarget = secondTarget;
+ await targetList.switchToTarget(secondTarget);
+
+ is(
+ targetList.targetFront,
+ currentTarget,
+ "After the switch, the top level target has been updated"
+ );
+ // Because JS Window Actor API isn't used yet, FrameDescriptor.getTarget returns null
+ // And there is no target being created for the iframe, yet.
+ // As soon as bug 1565200 is resolved, this should return two frames, including the iframe.
+ is(
+ frameTargets.length,
+ 1,
+ "We get the report of the top level iframe when switching to the new target"
+ );
+ is(frameTargets[0], currentTarget);
+ //is(frameTargets[1].url, "data:text/html,foo");
+
+ // Ensure that all the targets reported before the call to switchToTarget
+ // are reported as destroyed while calling switchToTarget.
+ is(
+ destroyedTargets.length,
+ createdTargets.length,
+ "All targets original reported are destroyed"
+ );
+ for (const newTarget of createdTargets) {
+ ok(
+ destroyedTargets.includes(newTarget),
+ "Each originally target is reported as destroyed"
+ );
+ }
+
+ targetList.destroy();
+
+ BrowserTestUtils.removeTab(firstTab);
+ BrowserTestUtils.removeTab(secondTab);
+}
diff --git a/devtools/shared/resources/tests/browser_target_list_tab_workers.js b/devtools/shared/resources/tests/browser_target_list_tab_workers.js
new file mode 100644
index 0000000000..e4e1a5a0dd
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_target_list_tab_workers.js
@@ -0,0 +1,330 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetList API around workers
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+const { TYPES } = TargetList;
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const IFRAME_FILE = "fission_iframe.html";
+const REMOTE_IFRAME_URL = URL_ROOT_ORG_SSL + IFRAME_FILE;
+const IFRAME_URL = URL_ROOT_SSL + IFRAME_FILE;
+const WORKER_FILE = "test_worker.js";
+const WORKER_URL = URL_ROOT_SSL + WORKER_FILE;
+const IFRAME_WORKER_URL = WORKER_FILE;
+
+add_task(async function() {
+ // Disable the preloaded process as it creates processes intermittently
+ // which forces the emission of RDP requests we aren't correctly waiting for.
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ const client = await createLocalClient();
+ const mainRoot = client.mainRoot;
+
+ // The WorkerDebuggerManager#getWorkerDebuggerEnumerator method we're using to retrieve
+ // workers loops through _all_ the workers in the process, which means it goes over workers
+ // from other tabs as well. Here we add a few tabs that are not going to be used in the
+ // test, just to check that their workers won't be retrieved by getAllTargets/watchTargets.
+ await addTab(`${FISSION_TEST_URL}?id=first-untargetted-tab&noServiceWorker`);
+ await addTab(`${FISSION_TEST_URL}?id=second-untargetted-tab&noServiceWorker`);
+
+ info("Test TargetList against workers via a tab target");
+ const tab = await addTab(`${FISSION_TEST_URL}?&noServiceWorker`);
+
+ // Create a TargetList for the tab
+ const descriptor = await mainRoot.getTab({ tab });
+ const target = await descriptor.getTarget();
+
+ // Ensure attaching the target as BrowsingContextTargetActor.listWorkers
+ // assert that the target actor is attached.
+ // It isn't clear if this assertion is meaningful?
+ await target.attach();
+
+ const targetList = new TargetList(mainRoot, target);
+
+ // Workaround to allow listening for workers in the content toolbox
+ // without the fission preferences
+ targetList.listenForWorkers = true;
+
+ await targetList.startListening();
+
+ info("Check that getAllTargets only returns dedicated workers");
+ const workers = await targetList.getAllTargets([
+ TYPES.WORKER,
+ TYPES.SHARED_WORKER,
+ ]);
+
+ // XXX: This should be modified in Bug 1607778, where we plan to add support for shared workers.
+ is(workers.length, 2, "Retrieved two worker…");
+ const mainPageWorker = workers.find(
+ worker => worker.url == `${WORKER_URL}#simple-worker`
+ );
+ const iframeWorker = workers.find(
+ worker => worker.url == `${IFRAME_WORKER_URL}#simple-worker-in-iframe`
+ );
+ ok(mainPageWorker, "…the dedicated worker on the main page");
+ ok(iframeWorker, "…and the dedicated worker on the iframe");
+
+ info(
+ "Assert that watchTargets will call the create callback for existing dedicated workers"
+ );
+ const targets = [];
+ const destroyedTargets = [];
+ const onAvailable = async ({ targetFront }) => {
+ info(`onAvailable called for ${targetFront.url}`);
+ is(
+ targetFront.targetType,
+ TYPES.WORKER,
+ "We are only notified about worker targets"
+ );
+ ok(!targetFront.isTopLevel, "The workers are never top level");
+ targets.push(targetFront);
+ info(`Handled ${targets.length} targets\n`);
+ };
+ const onDestroy = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.WORKER,
+ "We are only notified about worker targets"
+ );
+ ok(!targetFront.isTopLevel, "The workers are never top level");
+ destroyedTargets.push(targetFront);
+ };
+
+ await targetList.watchTargets(
+ [TYPES.WORKER, TYPES.SHARED_WORKER],
+ onAvailable,
+ onDestroy
+ );
+
+ // XXX: This should be modified in Bug 1607778, where we plan to add support for shared workers.
+ info("Check that watched targets return the same fronts as getAllTargets");
+ is(targets.length, 2, "watcheTargets retrieved 2 worker…");
+ const mainPageWorkerTarget = targets.find(t => t === mainPageWorker);
+ const iframeWorkerTarget = targets.find(t => t === iframeWorker);
+
+ ok(
+ mainPageWorkerTarget,
+ "…the dedicated worker in main page, which is the same front we received from getAllTargets"
+ );
+ ok(
+ iframeWorkerTarget,
+ "…the dedicated worker in iframe, which is the same front we received from getAllTargets"
+ );
+
+ info("Spawn workers in main page and iframe");
+ await SpecialPowers.spawn(tab.linkedBrowser, [WORKER_FILE], workerUrl => {
+ // Put the worker on the global so we can access it later
+ content.spawnedWorker = new content.Worker(`${workerUrl}#spawned-worker`);
+ const iframe = content.document.querySelector("iframe");
+ SpecialPowers.spawn(iframe, [workerUrl], innerWorkerUrl => {
+ // Put the worker on the global so we can access it later
+ content.spawnedWorker = new content.Worker(
+ `${innerWorkerUrl}#spawned-worker-in-iframe`
+ );
+ });
+ });
+
+ await TestUtils.waitForCondition(
+ () => targets.length === 4,
+ "Wait for the target list to notify us about the spawned worker"
+ );
+ const mainPageSpawnedWorkerTarget = targets.find(
+ innerTarget => innerTarget.url === `${WORKER_FILE}#spawned-worker`
+ );
+ ok(mainPageSpawnedWorkerTarget, "Retrieved spawned worker");
+ const iframeSpawnedWorkerTarget = targets.find(
+ innerTarget => innerTarget.url === `${WORKER_FILE}#spawned-worker-in-iframe`
+ );
+ ok(iframeSpawnedWorkerTarget, "Retrieved spawned worker in iframe");
+
+ await wait(100);
+
+ info(
+ "Check that the target list calls onDestroy when a worker is terminated"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.spawnedWorker.terminate();
+ content.spawnedWorker = null;
+
+ SpecialPowers.spawn(content.document.querySelector("iframe"), [], () => {
+ content.spawnedWorker.terminate();
+ content.spawnedWorker = null;
+ });
+ });
+ await TestUtils.waitForCondition(
+ () =>
+ destroyedTargets.includes(mainPageSpawnedWorkerTarget) &&
+ destroyedTargets.includes(iframeSpawnedWorkerTarget),
+ "Wait for the target list to notify us about the terminated workers"
+ );
+
+ ok(
+ true,
+ "The target list handled the terminated workers (from the main page and the iframe)"
+ );
+
+ info(
+ "Check that reloading the page will notify about the terminated worker and the new existing one"
+ );
+ const targetsCountBeforeReload = targets.length;
+ tab.linkedBrowser.reload();
+
+ await TestUtils.waitForCondition(() => {
+ return (
+ destroyedTargets.includes(mainPageWorkerTarget) &&
+ destroyedTargets.includes(iframeWorkerTarget)
+ );
+ }, `Wait for the target list to notify us about the terminated workers when reloading`);
+ ok(
+ true,
+ "The target list notified us about all the expected workers being destroyed when reloading"
+ );
+
+ await TestUtils.waitForCondition(
+ () => targets.length === targetsCountBeforeReload + 2,
+ "Wait for the target list to notify us about the new workers after reloading"
+ );
+
+ const mainPageWorkerTargetAfterReload = targets.find(
+ t => t !== mainPageWorkerTarget && t.url == `${WORKER_URL}#simple-worker`
+ );
+ const iframeWorkerTargetAfterReload = targets.find(
+ t =>
+ t !== iframeWorkerTarget &&
+ t.url == `${IFRAME_WORKER_URL}#simple-worker-in-iframe`
+ );
+
+ ok(
+ mainPageWorkerTargetAfterReload,
+ "The target list handled the worker created once the page navigated"
+ );
+ ok(
+ iframeWorkerTargetAfterReload,
+ "The target list handled the worker created in the iframe once the page navigated"
+ );
+
+ const targetCount = targets.length;
+
+ info(
+ "Check that when removing an iframe we're notified about its workers being terminated"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.querySelector("iframe").remove();
+ });
+ await TestUtils.waitForCondition(() => {
+ return destroyedTargets.includes(iframeWorkerTargetAfterReload);
+ }, `Wait for the target list to notify us about the terminated workers when removing an iframe`);
+
+ info("Check that target list handles adding iframes with workers");
+ const iframeUrl = `${IFRAME_URL}?noServiceWorker=true&hashSuffix=in-created-iframe`;
+ const remoteIframeUrl = `${REMOTE_IFRAME_URL}?noServiceWorker=true&hashSuffix=in-created-remote-iframe`;
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [iframeUrl, remoteIframeUrl],
+ (url, remoteUrl) => {
+ const firstIframe = content.document.createElement("iframe");
+ content.document.body.append(firstIframe);
+ firstIframe.src = url + "-1";
+
+ const secondIframe = content.document.createElement("iframe");
+ content.document.body.append(secondIframe);
+ secondIframe.src = url + "-2";
+
+ const firstRemoteIframe = content.document.createElement("iframe");
+ content.document.body.append(firstRemoteIframe);
+ firstRemoteIframe.src = remoteUrl + "-1";
+
+ const secondRemoteIframe = content.document.createElement("iframe");
+ content.document.body.append(secondRemoteIframe);
+ secondRemoteIframe.src = remoteUrl + "-2";
+ }
+ );
+
+ // It's important to check the length of `targets` here to ensure we don't get unwanted
+ // worker targets.
+ await TestUtils.waitForCondition(
+ () => targets.length === targetCount + 4,
+ "Wait for the target list to notify us about the workers in the new iframes"
+ );
+ const firstSpawnedIframeWorkerTarget = targets.find(
+ worker => worker.url == `${WORKER_FILE}#simple-worker-in-created-iframe-1`
+ );
+ const secondSpawnedIframeWorkerTarget = targets.find(
+ worker => worker.url == `${WORKER_FILE}#simple-worker-in-created-iframe-2`
+ );
+ const firstSpawnedRemoteIframeWorkerTarget = targets.find(
+ worker =>
+ worker.url == `${WORKER_FILE}#simple-worker-in-created-remote-iframe-1`
+ );
+ const secondSpawnedRemoteIframeWorkerTarget = targets.find(
+ worker =>
+ worker.url == `${WORKER_FILE}#simple-worker-in-created-remote-iframe-2`
+ );
+
+ ok(
+ firstSpawnedIframeWorkerTarget,
+ "The target list handled the worker in the first new same-origin iframe"
+ );
+ ok(
+ secondSpawnedIframeWorkerTarget,
+ "The target list handled the worker in the second new same-origin iframe"
+ );
+ ok(
+ firstSpawnedRemoteIframeWorkerTarget,
+ "The target list handled the worker in the first new remote iframe"
+ );
+ ok(
+ secondSpawnedRemoteIframeWorkerTarget,
+ "The target list handled the worker in the second new remote iframe"
+ );
+
+ info("Check that navigating away does destroy all targets");
+ BrowserTestUtils.loadURI(
+ tab.linkedBrowser,
+ "data:text/html,<meta charset=utf8>Away"
+ );
+
+ await TestUtils.waitForCondition(
+ () => destroyedTargets.length === targets.length,
+ "Wait for all the targets to be reporeted as destroyed"
+ );
+
+ ok(
+ destroyedTargets.includes(mainPageWorkerTargetAfterReload),
+ "main page worker target was destroyed"
+ );
+ ok(
+ destroyedTargets.includes(firstSpawnedIframeWorkerTarget),
+ "first spawned same-origin iframe worker target was destroyed"
+ );
+ ok(
+ destroyedTargets.includes(secondSpawnedIframeWorkerTarget),
+ "second spawned same-origin iframe worker target was destroyed"
+ );
+ ok(
+ destroyedTargets.includes(firstSpawnedRemoteIframeWorkerTarget),
+ "first spawned remote iframe worker target was destroyed"
+ );
+ ok(
+ destroyedTargets.includes(secondSpawnedRemoteIframeWorkerTarget),
+ "second spawned remote iframe worker target was destroyed"
+ );
+
+ targetList.unwatchTargets(
+ [TYPES.WORKER, TYPES.SHARED_WORKER],
+ onAvailable,
+ onDestroy
+ );
+ targetList.destroy();
+
+ info("Unregister service workers so they don't appear in other tests.");
+ await unregisterAllServiceWorkers(client);
+
+ BrowserTestUtils.removeTab(tab);
+ await client.close();
+});
diff --git a/devtools/shared/resources/tests/browser_target_list_watchTargets.js b/devtools/shared/resources/tests/browser_target_list_watchTargets.js
new file mode 100644
index 0000000000..68d0a381fd
--- /dev/null
+++ b/devtools/shared/resources/tests/browser_target_list_watchTargets.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetList's `watchTargets` function
+
+const { TargetList } = require("devtools/shared/resources/target-list");
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
+
+add_task(async function() {
+ // Enabled fission's pref as the TargetList is almost disabled without it
+ await pushPref("devtools.browsertoolbox.fission", true);
+ // Disable the preloaded process as it gets created lazily and may interfere
+ // with process count assertions
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+ // This preference helps destroying the content process when we close the tab
+ await pushPref("dom.ipc.keepProcessesAlive.web", 1);
+
+ const client = await createLocalClient();
+ const mainRoot = client.mainRoot;
+
+ await testWatchTargets(mainRoot);
+ await testContentProcessTarget(mainRoot);
+ await testThrowingInOnAvailable(mainRoot);
+
+ await client.close();
+});
+
+async function testWatchTargets(mainRoot) {
+ info("Test TargetList watchTargets function");
+
+ const targetDescriptor = await mainRoot.getMainProcess();
+ const target = await targetDescriptor.getTarget();
+ const targetList = new TargetList(mainRoot, target);
+
+ await targetList.startListening();
+
+ // Note that ppmm also includes the parent process, which is considered as a frame rather than a process
+ const originalProcessesCount = Services.ppmm.childCount - 1;
+
+ info(
+ "Check that onAvailable is called for processes already created *before* the call to watchTargets"
+ );
+ const targets = new Set();
+ const onAvailable = ({ targetFront }) => {
+ if (targets.has(targetFront)) {
+ ok(false, "The same target is notified multiple times via onAvailable");
+ }
+ is(
+ targetFront.targetType,
+ TargetList.TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ ok(
+ targetFront == target ? targetFront.isTopLevel : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ targets.add(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ if (!targets.has(targetFront)) {
+ ok(
+ false,
+ "A target is declared destroyed via onDestroyed without being notified via onAvailable"
+ );
+ }
+ is(
+ targetFront.targetType,
+ TargetList.TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ ok(
+ !targetFront.isTopLevel,
+ "We are not notified about the top level target destruction"
+ );
+ targets.delete(targetFront);
+ };
+ await targetList.watchTargets(
+ [TargetList.TYPES.PROCESS],
+ onAvailable,
+ onDestroyed
+ );
+ is(
+ targets.size,
+ originalProcessesCount,
+ "retrieved the expected number of processes via watchTargets"
+ );
+ // Start from 1 in order to ignore the parent process target, which is considered as a frame rather than a process
+ for (let i = 1; i < Services.ppmm.childCount; i++) {
+ const process = Services.ppmm.getChildAt(i);
+ const hasTargetWithSamePID = [...targets].find(
+ processTarget => processTarget.targetForm.processID == process.osPid
+ );
+ ok(
+ hasTargetWithSamePID,
+ `Process with PID ${process.osPid} has been reported via onAvailable`
+ );
+ }
+
+ info(
+ "Check that onAvailable is called for processes created *after* the call to watchTargets"
+ );
+ const previousTargets = new Set(targets);
+ const onProcessCreated = new Promise(resolve => {
+ const onAvailable2 = ({ targetFront }) => {
+ if (previousTargets.has(targetFront)) {
+ return;
+ }
+ targetList.unwatchTargets([TargetList.TYPES.PROCESS], onAvailable2);
+ resolve(targetFront);
+ };
+ targetList.watchTargets([TargetList.TYPES.PROCESS], onAvailable2);
+ });
+ const tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+ const createdTarget = await onProcessCreated;
+
+ // For some reason, creating a new tab purges processes created from previous tests
+ // so it is not reasonable to assert the side of `targets` as it may be lower than expected.
+ ok(targets.has(createdTarget), "The new tab process is in the list");
+
+ const processCountAfterTabOpen = targets.size;
+
+ // Assert that onDestroyed is called for destroyed processes
+ const onProcessDestroyed = new Promise(resolve => {
+ const onAvailable3 = () => {};
+ const onDestroyed3 = ({ targetFront }) => {
+ resolve(targetFront);
+ targetList.unwatchTargets(
+ [TargetList.TYPES.PROCESS],
+ onAvailable3,
+ onDestroyed3
+ );
+ };
+ targetList.watchTargets(
+ [TargetList.TYPES.PROCESS],
+ onAvailable3,
+ onDestroyed3
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ const destroyedTarget = await onProcessDestroyed;
+ is(
+ targets.size,
+ processCountAfterTabOpen - 1,
+ "The closed tab's process has been reported as destroyed"
+ );
+ ok(
+ !targets.has(destroyedTarget),
+ "The destroyed target is no longer in the list"
+ );
+ is(
+ destroyedTarget,
+ createdTarget,
+ "The destroyed target is the one that has been reported as created"
+ );
+
+ targetList.unwatchTargets(
+ [TargetList.TYPES.PROCESS],
+ onAvailable,
+ onDestroyed
+ );
+
+ targetList.destroy();
+}
+
+async function testContentProcessTarget(mainRoot) {
+ info("Test TargetList watchTargets with a content process target");
+
+ const processes = await mainRoot.listProcesses();
+ const target = await processes[1].getTarget();
+ const targetList = new TargetList(mainRoot, target);
+
+ await targetList.startListening();
+
+ // Assert that watchTargets is only called for the top level content process target
+ // as listening for additional target is only enable for the parent process target.
+ // See bug 1593928.
+ const targets = new Set();
+ const onAvailable = ({ targetFront }) => {
+ if (targets.has(targetFront)) {
+ // This may fail if the top level target is reported by LegacyImplementation
+ // to TargetList and emits an available event for it.
+ ok(false, "The same target is notified multiple times via onAvailable");
+ }
+ is(
+ targetFront.targetType,
+ TargetList.TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ is(targetFront, target, "This is the existing top level target");
+ ok(
+ targetFront.isTopLevel,
+ "We are only notified about the top level target"
+ );
+ targets.add(targetFront);
+ };
+ const onDestroyed = _ => {
+ ok(false, "onDestroyed should never be called in this test");
+ };
+ await targetList.watchTargets(
+ [TargetList.TYPES.PROCESS],
+ onAvailable,
+ onDestroyed
+ );
+
+ // This may fail if the top level target is reported by LegacyImplementation
+ // to TargetList and registers a duplicated entry
+ is(targets.size, 1, "We were only notified about the top level target");
+
+ targetList.destroy();
+}
+
+async function testThrowingInOnAvailable(mainRoot) {
+ info(
+ "Test TargetList watchTargets function when an exception is thrown in onAvailable callback"
+ );
+
+ const targetDescriptor = await mainRoot.getMainProcess();
+ const target = await targetDescriptor.getTarget();
+ const targetList = new TargetList(mainRoot, target);
+
+ await targetList.startListening();
+
+ // Note that ppmm also includes the parent process, which is considered as a frame rather than a process
+ const originalProcessesCount = Services.ppmm.childCount - 1;
+
+ info(
+ "Check that onAvailable is called for processes already created *before* the call to watchTargets"
+ );
+ const targets = new Set();
+ let thrown = false;
+ const onAvailable = ({ targetFront }) => {
+ if (!thrown) {
+ thrown = true;
+ throw new Error("Force an exception when processing the first target");
+ }
+ targets.add(targetFront);
+ };
+ await targetList.watchTargets([TargetList.TYPES.PROCESS], onAvailable);
+ is(
+ targets.size,
+ originalProcessesCount - 1,
+ "retrieved the expected number of processes via onAvailable. All but the first one where we have thrown."
+ );
+
+ targetList.destroy();
+}
diff --git a/devtools/shared/resources/tests/early_console_document.html b/devtools/shared/resources/tests/early_console_document.html
new file mode 100644
index 0000000000..e4523dbdeb
--- /dev/null
+++ b/devtools/shared/resources/tests/early_console_document.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+ "use strict";
+ console.log("early-page-log");
+ </script>
+</head>
+<body></body>
+</html>
diff --git a/devtools/shared/resources/tests/fission_document.html b/devtools/shared/resources/tests/fission_document.html
new file mode 100644
index 0000000000..24f9704a76
--- /dev/null
+++ b/devtools/shared/resources/tests/fission_document.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+ "use strict";
+
+ const params = new URLSearchParams(document.location.search);
+
+ // eslint-disable-next-line no-unused-vars
+ const worker = new Worker("https://example.com/browser/devtools/shared/resources/tests/test_worker.js#simple-worker");
+
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker("https://example.com/browser/devtools/shared/resources/tests/test_worker.js#shared-worker");
+
+ if (!params.has("noServiceWorker")) {
+ // Expose a reference to the registration so that tests can unregister it.
+ window.registrationPromise = navigator.serviceWorker.register("https://example.com/browser/devtools/shared/resources/tests/test_service_worker.js#service-worker");
+ }
+
+ /* exported logMessageInWorker */
+ function logMessageInWorker(message) {
+ worker.postMessage({
+ type: "log-in-worker",
+ message,
+ });
+ }
+ </script>
+</head>
+<body>
+<p>Test fission iframe</p>
+
+<script>
+ "use strict";
+ const iframe = document.createElement("iframe");
+ let iframeUrl = `https://example.org/browser/devtools/shared/resources/tests/fission_iframe.html`;
+ if (document.location.search) {
+ iframeUrl += `?${new URLSearchParams(document.location.search)}`;
+ }
+ iframe.src = iframeUrl;
+ document.body.append(iframe);
+</script>
+</body>
+</html>
diff --git a/devtools/shared/resources/tests/fission_iframe.html b/devtools/shared/resources/tests/fission_iframe.html
new file mode 100644
index 0000000000..deae49f833
--- /dev/null
+++ b/devtools/shared/resources/tests/fission_iframe.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test fission iframe document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script>
+ "use strict";
+ const params = new URLSearchParams(document.location.search);
+ const hashSuffix = params.get("hashSuffix") || "in-iframe";
+ // eslint-disable-next-line no-unused-vars
+ const worker = new Worker("test_worker.js#simple-worker-" + hashSuffix);
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker("test_worker.js#shared-worker-" + hashSuffix);
+
+ /* exported logMessageInWorker */
+ function logMessageInWorker(message) {
+ worker.postMessage({
+ type: "log-in-worker",
+ message,
+ });
+ }
+ </script>
+</head>
+<body>
+<p>remote iframe</p>
+</body>
+</html>
diff --git a/devtools/shared/resources/tests/head.js b/devtools/shared/resources/tests/head.js
new file mode 100644
index 0000000000..988592f9a1
--- /dev/null
+++ b/devtools/shared/resources/tests/head.js
@@ -0,0 +1,150 @@
+/* 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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../client/shared/test/shared-head.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+const { DevToolsClient } = require("devtools/client/devtools-client");
+const { DevToolsServer } = require("devtools/server/devtools-server");
+
+async function createLocalClient() {
+ // Instantiate a minimal server
+ DevToolsServer.init();
+ DevToolsServer.allowChromeProcess = true;
+ if (!DevToolsServer.createRootActor) {
+ DevToolsServer.registerAllActors();
+ }
+ const transport = DevToolsServer.connectPipe();
+ const client = new DevToolsClient(transport);
+ await client.connect();
+ return client;
+}
+
+async function _initResourceWatcherFromDescriptor(
+ client,
+ descriptor,
+ { listenForWorkers = false } = {}
+) {
+ const { TargetList } = require("devtools/shared/resources/target-list");
+ const {
+ ResourceWatcher,
+ } = require("devtools/shared/resources/resource-watcher");
+
+ const target = await descriptor.getTarget();
+ const targetList = new TargetList(client.mainRoot, target);
+ if (listenForWorkers) {
+ targetList.listenForWorkers = true;
+ }
+ await targetList.startListening();
+
+ // Now create a ResourceWatcher
+ const resourceWatcher = new ResourceWatcher(targetList);
+
+ return { client, resourceWatcher, targetList };
+}
+
+/**
+ * Instantiate a ResourceWatcher for the given tab.
+ *
+ * @param {Tab} tab
+ * The browser frontend's tab to connect to.
+ * @param {Object} options
+ * @param {Boolean} options.listenForWorkers
+ * @return {Object} object
+ * @return {ResourceWatcher} object.client
+ * The underlying client instance.
+ * @return {DevToolsClient} object.client
+ * The underlying client instance.
+ * @return {DevToolsClient} object.targetList
+ * The underlying target list instance.
+ */
+async function initResourceWatcher(tab, options) {
+ const client = await createLocalClient();
+ const descriptor = await client.mainRoot.getTab({ tab });
+ return _initResourceWatcherFromDescriptor(client, descriptor, options);
+}
+
+/**
+ * Instantiate a multi-process ResourceWatcher, watching all type of targets.
+ *
+ * @return {Object} object
+ * @return {ResourceWatcher} object.client
+ * The underlying client instance.
+ * @return {DevToolsClient} object.client
+ * The underlying client instance.
+ * @return {DevToolsClient} object.targetList
+ * The underlying target list instance.
+ */
+async function initMultiProcessResourceWatcher() {
+ const client = await createLocalClient();
+ const descriptor = await client.mainRoot.getMainProcess();
+ return _initResourceWatcherFromDescriptor(client, descriptor);
+}
+
+// Copied from devtools/shared/webconsole/test/chrome/common.js
+function checkObject(object, expected) {
+ if (object && object.getGrip) {
+ object = object.getGrip();
+ }
+
+ for (const name of Object.keys(expected)) {
+ const expectedValue = expected[name];
+ const value = object[name];
+ checkValue(name, value, expectedValue);
+ }
+}
+
+function checkValue(name, value, expected) {
+ if (expected === null) {
+ is(value, null, `'${name}' is null`);
+ } else if (value === undefined) {
+ is(value, undefined, `'${name}' is undefined`);
+ } else if (value === null) {
+ is(value, expected, `'${name}' has expected value`);
+ } else if (
+ typeof expected == "string" ||
+ typeof expected == "number" ||
+ typeof expected == "boolean"
+ ) {
+ is(value, expected, "property '" + name + "'");
+ } else if (expected instanceof RegExp) {
+ ok(expected.test(value), name + ": " + expected + " matched " + value);
+ } else if (Array.isArray(expected)) {
+ info("checking array for property '" + name + "'");
+ ok(Array.isArray(value), `property '${name}' is an array`);
+
+ is(value.length, expected.length, "Array has expected length");
+ if (value.length !== expected.length) {
+ is(JSON.stringify(value, null, 2), JSON.stringify(expected, null, 2));
+ } else {
+ checkObject(value, expected);
+ }
+ } else if (typeof expected == "object") {
+ info("checking object for property '" + name + "'");
+ checkObject(value, expected);
+ }
+}
+
+async function triggerNetworkRequests(browser, commands) {
+ for (let i = 0; i < commands.length; i++) {
+ await SpecialPowers.spawn(browser, [commands[i]], async function(code) {
+ const script = content.document.createElement("script");
+ script.append(
+ content.document.createTextNode(
+ `async function triggerRequest() {${code}}`
+ )
+ );
+ content.document.body.append(script);
+ await content.wrappedJSObject.triggerRequest();
+ script.remove();
+ });
+ }
+}
diff --git a/devtools/shared/resources/tests/network_document.html b/devtools/shared/resources/tests/network_document.html
new file mode 100644
index 0000000000..5c4744cb0c
--- /dev/null
+++ b/devtools/shared/resources/tests/network_document.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Test for network events</title>
+ </head>
+ <body>
+ <p>Test for network events</p>
+ </body>
+</html>
diff --git a/devtools/shared/resources/tests/service-worker-sources.js b/devtools/shared/resources/tests/service-worker-sources.js
new file mode 100644
index 0000000000..614644ee5d
--- /dev/null
+++ b/devtools/shared/resources/tests/service-worker-sources.js
@@ -0,0 +1,2 @@
+/* eslint-disable */
+function serviceWorkerSource() {}
diff --git a/devtools/shared/resources/tests/sources.html b/devtools/shared/resources/tests/sources.html
new file mode 100644
index 0000000000..d765f25ef0
--- /dev/null
+++ b/devtools/shared/resources/tests/sources.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype HTML>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ /* eslint-disable */
+ function inlineSource() {}
+ // Assign it to a global in order to avoid it being GCed
+ eval("this.global = function evalFunction() {}");
+ // Assign the worker to a global variable in order to avoid
+ // having it be GCed.
+ this.worker = new Worker("worker-sources.js");
+ window.registrationPromise = navigator.serviceWorker.register("service-worker-sources.js");
+ </script>
+ <script src="sources.js"></script>
+ </body>
+</html>
diff --git a/devtools/shared/resources/tests/sources.js b/devtools/shared/resources/tests/sources.js
new file mode 100644
index 0000000000..7ae6c6272b
--- /dev/null
+++ b/devtools/shared/resources/tests/sources.js
@@ -0,0 +1,2 @@
+/* eslint-disable */
+function scriptSource() {}
diff --git a/devtools/shared/resources/tests/style_document.css b/devtools/shared/resources/tests/style_document.css
new file mode 100644
index 0000000000..aa54533924
--- /dev/null
+++ b/devtools/shared/resources/tests/style_document.css
@@ -0,0 +1 @@
+body { margin: 1px; }
diff --git a/devtools/shared/resources/tests/style_document.html b/devtools/shared/resources/tests/style_document.html
new file mode 100644
index 0000000000..1b5e97dfe7
--- /dev/null
+++ b/devtools/shared/resources/tests/style_document.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Test style document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <style>
+ body { color: lime; }
+ </style>
+ <link href="style_document.css" rel="stylesheet" type="text/css"/>
+ </head>
+ <body>
+ <iframe src="https://example.org/browser/devtools/shared/resources/tests/style_iframe.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/shared/resources/tests/style_iframe.css b/devtools/shared/resources/tests/style_iframe.css
new file mode 100644
index 0000000000..30e7ae802b
--- /dev/null
+++ b/devtools/shared/resources/tests/style_iframe.css
@@ -0,0 +1 @@
+body { padding: 1px; }
diff --git a/devtools/shared/resources/tests/style_iframe.html b/devtools/shared/resources/tests/style_iframe.html
new file mode 100644
index 0000000000..11ad9f785b
--- /dev/null
+++ b/devtools/shared/resources/tests/style_iframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Test style iframe document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <style>
+ body { background-color: pink; }
+ </style>
+ <link href="style_iframe.css" rel="stylesheet" type="text/css"/>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/shared/resources/tests/test_service_worker.js b/devtools/shared/resources/tests/test_service_worker.js
new file mode 100644
index 0000000000..3b69a40dcd
--- /dev/null
+++ b/devtools/shared/resources/tests/test_service_worker.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// We don't need any computation in the worker,
+// but at least register a fetch listener so that
+// we force instantiating the SW when loading the page.
+self.onfetch = function(event) {
+ // do nothing.
+};
diff --git a/devtools/shared/resources/tests/test_sw_page.html b/devtools/shared/resources/tests/test_sw_page.html
new file mode 100644
index 0000000000..38aad04259
--- /dev/null
+++ b/devtools/shared/resources/tests/test_sw_page.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test sw page</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test sw page</p>
+
+<script>
+"use strict";
+
+// Expose a reference to the registration so that tests can unregister it.
+window.registrationPromise = navigator.serviceWorker.register("test_sw_page_worker.js");
+</script>
+</body>
+</html>
diff --git a/devtools/shared/resources/tests/test_sw_page_worker.js b/devtools/shared/resources/tests/test_sw_page_worker.js
new file mode 100644
index 0000000000..29cda68560
--- /dev/null
+++ b/devtools/shared/resources/tests/test_sw_page_worker.js
@@ -0,0 +1,5 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// We don't need any computation in the worker,
+// just it to be alive
diff --git a/devtools/shared/resources/tests/test_worker.js b/devtools/shared/resources/tests/test_worker.js
new file mode 100644
index 0000000000..873041fcf0
--- /dev/null
+++ b/devtools/shared/resources/tests/test_worker.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+console.log("[WORKER] started", globalThis.location.toString(), globalThis);
+
+globalThis.onmessage = function(e) {
+ const { type, message } = e.data;
+
+ if (type === "log-in-worker") {
+ // Printing `e` so we can check that we have an object and not a stringified version
+ console.log("[WORKER]", message, e);
+ }
+};
diff --git a/devtools/shared/resources/tests/websocket_backend_wsh.py b/devtools/shared/resources/tests/websocket_backend_wsh.py
new file mode 100644
index 0000000000..564715114b
--- /dev/null
+++ b/devtools/shared/resources/tests/websocket_backend_wsh.py
@@ -0,0 +1,21 @@
+from __future__ import absolute_import
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ while not request.client_terminated:
+ resp = msgutil.receive_message(request)
+ msgutil.send_message(request, resp)
+
+
+def web_socket_passive_closing_handshake(request):
+ # If we use `pass` here, the `payload` of `frameReceived` which will be happened
+ # of communication of closing will be `\u0003è`. In order to make the `payload`
+ # to be empty string, return code and reason explicitly.
+ code = None
+ reason = None
+ return code, reason
diff --git a/devtools/shared/resources/tests/websocket_frontend.html b/devtools/shared/resources/tests/websocket_frontend.html
new file mode 100644
index 0000000000..ce9b3b93bd
--- /dev/null
+++ b/devtools/shared/resources/tests/websocket_frontend.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype HTML>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ <script type="text/javascript">
+ /* exported openConnection, closeConnection, sendData */
+ "use strict";
+
+ let webSocket;
+ function openConnection() {
+ return new Promise(resolve => {
+ webSocket = new WebSocket(
+ "ws://mochi.test:8888/browser/devtools/shared/resources/tests/websocket_backend"
+ );
+ webSocket.onopen = () => {
+ resolve();
+ };
+ });
+ }
+
+ function closeConnection() {
+ return new Promise(resolve => {
+ webSocket.onclose = () => {
+ resolve();
+ };
+ webSocket.close();
+ })
+ }
+
+ function sendData(payload) {
+ webSocket.send(payload);
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/shared/resources/tests/worker-sources.js b/devtools/shared/resources/tests/worker-sources.js
new file mode 100644
index 0000000000..dcf2ed8031
--- /dev/null
+++ b/devtools/shared/resources/tests/worker-sources.js
@@ -0,0 +1,2 @@
+/* eslint-disable */
+function workerSource() {}