summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/target/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/shared/commands/target/tests
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/commands/target/tests')
-rw-r--r--devtools/shared/commands/target/tests/browser.ini48
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_bfcache.js499
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_browser_workers.js246
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_detach.js59
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_frames.js649
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_frames_popups.js168
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_frames_reload_server_side_targets.js104
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_getAllTargets.js119
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_invalid_api_usage.js78
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_processes.js242
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_reload.js115
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_scope_flag.js190
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_service_workers.js77
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_service_workers_navigation.js389
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_switchToTarget.js138
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_tab_workers.js322
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_tab_workers_bfcache_navigation.js134
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_various_descriptors.js283
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_watchTargets.js214
-rw-r--r--devtools/shared/commands/target/tests/browser_watcher_actor_getter_caching.js87
-rw-r--r--devtools/shared/commands/target/tests/fission_document.html47
-rw-r--r--devtools/shared/commands/target/tests/fission_iframe.html29
-rw-r--r--devtools/shared/commands/target/tests/head.js32
-rw-r--r--devtools/shared/commands/target/tests/incremental-js-value-script.sjs23
-rw-r--r--devtools/shared/commands/target/tests/simple_document.html12
-rw-r--r--devtools/shared/commands/target/tests/test_service_worker.js11
-rw-r--r--devtools/shared/commands/target/tests/test_sw_page.html19
-rw-r--r--devtools/shared/commands/target/tests/test_sw_page_worker.js5
-rw-r--r--devtools/shared/commands/target/tests/test_worker.js13
29 files changed, 4352 insertions, 0 deletions
diff --git a/devtools/shared/commands/target/tests/browser.ini b/devtools/shared/commands/target/tests/browser.ini
new file mode 100644
index 0000000000..b28e700de0
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser.ini
@@ -0,0 +1,48 @@
+[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/highlighter-test-actor.js
+ head.js
+ simple_document.html
+ incremental-js-value-script.sjs
+ fission_document.html
+ fission_iframe.html
+ test_service_worker.js
+ test_sw_page.html
+ test_sw_page_worker.js
+ test_worker.js
+
+[browser_target_command_bfcache.js]
+[browser_target_command_browser_workers.js]
+[browser_target_command_detach.js]
+[browser_target_command_frames_popups.js]
+skip-if =
+ win10_2004 && debug && fission && socketprocess_networking # high frequency intermittent
+[browser_target_command_frames_reload_server_side_targets.js]
+skip-if = !fission
+[browser_target_command_frames.js]
+[browser_target_command_getAllTargets.js]
+[browser_target_command_invalid_api_usage.js]
+[browser_target_command_scope_flag.js]
+[browser_target_command_processes.js]
+[browser_target_command_reload.js]
+[browser_target_command_service_workers.js]
+skip-if =
+ http3 # Bug 1781324
+[browser_target_command_service_workers_navigation.js]
+skip-if =
+ os == "linux" && bits == 64 # Bug 1726270
+ os == "win" && bits == 64 # Bug 1726270
+ os == "mac" && fission # Bug 1726270
+[browser_target_command_switchToTarget.js]
+[browser_target_command_tab_workers.js]
+[browser_target_command_tab_workers_bfcache_navigation.js]
+skip-if = debug # Bug 1721859
+[browser_target_command_various_descriptors.js]
+skip-if =
+ os == "linux" && bits == 64 && !debug #Bug 1701056
+[browser_target_command_watchTargets.js]
+[browser_watcher_actor_getter_caching.js]
diff --git a/devtools/shared/commands/target/tests/browser_target_command_bfcache.js b/devtools/shared/commands/target/tests/browser_target_command_bfcache.js
new file mode 100644
index 0000000000..a5deeb9260
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_bfcache.js
@@ -0,0 +1,499 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API when bfcache navigations happen
+
+const TEST_COM_URL = URL_ROOT_SSL + "simple_document.html";
+
+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);
+
+ info("## Test with bfcache in parent DISABLED");
+ await pushPref("fission.bfcacheInParent", false);
+ await testTopLevelNavigations(false);
+ await testIframeNavigations(false);
+ await testTopLevelNavigationsOnDocumentWithIframe(false);
+
+ // bfcacheInParent only works if sessionHistoryInParent is enable
+ // so only test it if both settings are enabled.
+ // (it looks like sessionHistoryInParent is enabled by default when fission is enabled)
+ if (Services.appinfo.sessionHistoryInParent) {
+ info("## Test with bfcache in parent ENABLED");
+ await pushPref("fission.bfcacheInParent", true);
+ await testTopLevelNavigations(true);
+ await testIframeNavigations(true);
+ await testTopLevelNavigationsOnDocumentWithIframe(true);
+ }
+});
+
+async function testTopLevelNavigations(bfcacheInParent) {
+ info(" # Test TOP LEVEL navigations");
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(TEST_COM_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const onAvailable = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(targetFront.isTopLevel, "all targets of this test are top level");
+ targets.push(targetFront);
+ };
+ const destroyedTargets = [];
+ const onDestroyed = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(targetFront.isTopLevel, "all targets of this test are top level");
+ destroyedTargets.push(targetFront);
+ };
+
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+ is(targets.length, 1, "retrieved only the top level target");
+ is(targets[0], targetCommand.targetFront, "the target is the top level one");
+ is(
+ destroyedTargets.length,
+ 0,
+ "We get no destruction when calling watchTargets"
+ );
+ ok(
+ targets[0].targetForm.followWindowGlobalLifeCycle,
+ "the first server side target follows the WindowGlobal lifecycle, when server target switching is enabled"
+ );
+
+ // Navigate to the same page with query params
+ info("Load the second page");
+ const secondPageUrl = TEST_COM_URL + "?second-load";
+ const previousBrowsingContextID = gBrowser.selectedBrowser.browsingContext.id;
+ ok(
+ previousBrowsingContextID,
+ "Fetch the tab's browsing context id before navigation"
+ );
+ const onLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ secondPageUrl
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, secondPageUrl);
+ await onLoaded;
+
+ // Assert BrowsingContext changes as it impact the behavior of targets
+ if (bfcacheInParent) {
+ isnot(
+ previousBrowsingContextID,
+ gBrowser.selectedBrowser.browsingContext.id,
+ "When bfcacheInParent is enabled, same-origin navigations spawn new BrowsingContext"
+ );
+ } else {
+ is(
+ previousBrowsingContextID,
+ gBrowser.selectedBrowser.browsingContext.id,
+ "When bfcacheInParent is disabled, same-origin navigations re-use the same BrowsingContext"
+ );
+ }
+
+ // Same-origin navigations also spawn a new top level target
+ await waitFor(
+ () => targets.length == 2,
+ "wait for the next top level target"
+ );
+ is(
+ targets[1],
+ targetCommand.targetFront,
+ "the second target is the top level one"
+ );
+ // As targetFront.url isn't reliable and might be about:blank,
+ // try to assert that we got the right target via other means.
+ // outerWindowID should change when navigating to another process,
+ // while it would stay equal for in-process navigations.
+ is(
+ targets[1].outerWindowID,
+ gBrowser.selectedBrowser.outerWindowID,
+ "the second target is for the second page"
+ );
+ ok(
+ targets[1].targetForm.followWindowGlobalLifeCycle,
+ "the new server side target follows the WindowGlobal lifecycle"
+ );
+ ok(targets[0].isDestroyed(), "the first target is destroyed");
+ is(destroyedTargets.length, 1, "We get one target being destroyed...");
+ is(destroyedTargets[0], targets[0], "...and that's the first one");
+
+ // Go back to the first page, this should be a bfcache navigation, and,
+ // we should get a new target
+ info("Go back to the first page");
+ gBrowser.selectedBrowser.goBack();
+
+ await waitFor(
+ () => targets.length == 3,
+ "wait for the next top level target"
+ );
+ is(
+ targets[2],
+ targetCommand.targetFront,
+ "the third target is the top level one"
+ );
+ // Here as this is revived from cache, the url should always be correct
+ is(targets[2].url, TEST_COM_URL, "the third target is for the first url");
+ ok(
+ targets[2].targetForm.followWindowGlobalLifeCycle,
+ "the third target for bfcache navigations is following the WindowGlobal lifecycle"
+ );
+ ok(targets[1].isDestroyed(), "the second target is destroyed");
+ is(
+ destroyedTargets.length,
+ 2,
+ "We get one additional target being destroyed..."
+ );
+ is(destroyedTargets[1], targets[1], "...and that's the second one");
+
+ // Wait for full attach in order to having breaking any pending requests
+ // when navigating to another page and switching to new process and target.
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ // Go forward and resurect the second page, this should also be a bfcache navigation, and,
+ // get a new target.
+ info("Go forward to the second page");
+
+ // When a new target will be created, we need to wait until it's fully processed
+ // to avoid pending promises.
+ const onNewTargetProcessed = bfcacheInParent
+ ? new Promise(resolve => {
+ targetCommand.on(
+ "processed-available-target",
+ function onProcessedAvailableTarget(targetFront) {
+ if (targetFront === targets[3]) {
+ resolve();
+ targetCommand.off(
+ "processed-available-target",
+ onProcessedAvailableTarget
+ );
+ }
+ }
+ );
+ })
+ : null;
+
+ gBrowser.selectedBrowser.goForward();
+
+ await waitFor(
+ () => targets.length == 4,
+ "wait for the next top level target"
+ );
+ is(
+ targets[3],
+ targetCommand.targetFront,
+ "the 4th target is the top level one"
+ );
+ // Same here, as the document is revived from the cache, the url should always be correct
+ is(targets[3].url, secondPageUrl, "the 4th target is for the second url");
+ ok(
+ targets[3].targetForm.followWindowGlobalLifeCycle,
+ "the 4th target for bfcache navigations is following the WindowGlobal lifecycle"
+ );
+ ok(targets[2].isDestroyed(), "the third target is destroyed");
+ is(
+ destroyedTargets.length,
+ 3,
+ "We get one additional target being destroyed..."
+ );
+ is(destroyedTargets[2], targets[2], "...and that's the third one");
+
+ // Wait for full attach in order to having breaking any pending requests
+ // when navigating to another page and switching to new process and target.
+ await waitForAllTargetsToBeAttached(targetCommand);
+ await onNewTargetProcessed;
+
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
+
+async function testTopLevelNavigationsOnDocumentWithIframe(bfcacheInParent) {
+ info(" # Test TOP LEVEL navigations on document with iframe");
+ // Create a TargetCommand for a given test tab
+ const tab =
+ await addTab(`https://example.com/document-builder.sjs?id=top&html=
+ <h1>Top level</h1>
+ <iframe src="${encodeURIComponent(
+ "https://example.com/document-builder.sjs?id=iframe&html=<h2>In iframe</h2>"
+ )}">
+ </iframe>`);
+ const getLocationIdParam = url =>
+ new URLSearchParams(new URL(url).search).get("id");
+
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const onAvailable = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ targets.push(targetFront);
+ };
+ const destroyedTargets = [];
+ const onDestroyed = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ destroyedTargets.push(targetFront);
+ };
+
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+
+ if (isEveryFrameTargetEnabled()) {
+ is(
+ targets.length,
+ 2,
+ "retrieved targets for top level and iframe documents"
+ );
+ is(
+ targets[0],
+ targetCommand.targetFront,
+ "the target is the top level one"
+ );
+ is(
+ getLocationIdParam(targets[1].url),
+ "iframe",
+ "the second target is the iframe one"
+ );
+ } else {
+ is(targets.length, 1, "retrieved only the top level target");
+ is(
+ targets[0],
+ targetCommand.targetFront,
+ "the target is the top level one"
+ );
+ }
+
+ is(
+ destroyedTargets.length,
+ 0,
+ "We get no destruction when calling watchTargets"
+ );
+
+ info("Navigate to a new page");
+ let targetCountBeforeNavigation = targets.length;
+ const secondPageUrl = `https://example.com/document-builder.sjs?html=second`;
+ const onLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ secondPageUrl
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, secondPageUrl);
+ await onLoaded;
+
+ // Same-origin navigations also spawn a new top level target
+ await waitFor(
+ () => targets.length == targetCountBeforeNavigation + 1,
+ "wait for the next top level target"
+ );
+ is(
+ targets.at(-1),
+ targetCommand.targetFront,
+ "the new target is the top level one"
+ );
+
+ ok(targets[0].isDestroyed(), "the first target is destroyed");
+ if (isEveryFrameTargetEnabled()) {
+ ok(targets[1].isDestroyed(), "the second target is destroyed");
+ is(destroyedTargets.length, 2, "The two targets were destroyed");
+ } else {
+ is(destroyedTargets.length, 1, "Only one target was destroyed");
+ }
+
+ // Go back to the first page, this should be a bfcache navigation, and,
+ // we should get a new target (or 2 if EFT is enabled)
+ targetCountBeforeNavigation = targets.length;
+ info("Go back to the first page");
+ gBrowser.selectedBrowser.goBack();
+
+ await waitFor(
+ () =>
+ targets.length ===
+ targetCountBeforeNavigation + (isEveryFrameTargetEnabled() ? 2 : 1),
+ "wait for the next top level target"
+ );
+
+ if (isEveryFrameTargetEnabled()) {
+ await waitFor(() => targets.at(-2).url && targets.at(-1).url);
+ is(
+ getLocationIdParam(targets.at(-2).url),
+ "top",
+ "the first new target is for the top document…"
+ );
+ is(
+ getLocationIdParam(targets.at(-1).url),
+ "iframe",
+ "…and the second one is for the iframe"
+ );
+ } else {
+ is(
+ getLocationIdParam(targets.at(-1).url),
+ "top",
+ "the new target is for the first url"
+ );
+ }
+
+ ok(
+ targets[targetCountBeforeNavigation - 1].isDestroyed(),
+ "the target for the second page is destroyed"
+ );
+ is(
+ destroyedTargets.length,
+ targetCountBeforeNavigation,
+ "We get one additional target being destroyed…"
+ );
+ is(
+ destroyedTargets.at(-1),
+ targets[targetCountBeforeNavigation - 1],
+ "…and that's the second page one"
+ );
+
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
+
+async function testIframeNavigations() {
+ info(" # Test IFRAME navigations");
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(
+ `http://example.org/document-builder.sjs?html=<iframe src="${TEST_COM_URL}"></iframe>`
+ );
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const onAvailable = async ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ targets.push(targetFront);
+ };
+ await targetCommand.watchTargets({ types: [TYPES.FRAME], onAvailable });
+
+ // When fission/EFT is off, there isn't much to test for iframes as they are debugged
+ // when the unique top level target
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ is(
+ targets.length,
+ 1,
+ "Without fission/EFT, there is only the top level target"
+ );
+ await commands.destroy();
+ return;
+ }
+ is(targets.length, 2, "retrieved the top level and the iframe targets");
+ is(
+ targets[0],
+ targetCommand.targetFront,
+ "the first target is the top level one"
+ );
+ is(targets[1].url, TEST_COM_URL, "the second target is the iframe one");
+
+ // Navigate to the same page with query params
+ info("Load the second page");
+ const secondPageUrl = TEST_COM_URL + "?second-load";
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [secondPageUrl],
+ function (url) {
+ const iframe = content.document.querySelector("iframe");
+ iframe.src = url;
+ }
+ );
+
+ await waitFor(() => targets.length == 3, "wait for the next target");
+ is(targets[2].url, secondPageUrl, "the second target is for the second url");
+ ok(targets[1].isDestroyed(), "the first target is destroyed");
+
+ // Go back to the first page, this should be a bfcache navigation, and,
+ // we should get a new target
+ info("Go back to the first page");
+ const iframeBrowsingContext = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ function () {
+ const iframe = content.document.querySelector("iframe");
+ return iframe.browsingContext;
+ }
+ );
+ await SpecialPowers.spawn(iframeBrowsingContext, [], function () {
+ content.history.back();
+ });
+
+ await waitFor(() => targets.length == 4, "wait for the next target");
+ is(targets[3].url, TEST_COM_URL, "the third target is for the first url");
+ ok(targets[2].isDestroyed(), "the second target is destroyed");
+
+ // Go forward and resurect the second page, this should also be a bfcache navigation, and,
+ // get a new target.
+ info("Go forward to the second page");
+ await SpecialPowers.spawn(iframeBrowsingContext, [], function () {
+ content.history.forward();
+ });
+
+ await waitFor(() => targets.length == 5, "wait for the next target");
+ is(targets[4].url, secondPageUrl, "the 4th target is for the second url");
+ ok(targets[3].isDestroyed(), "the third target is destroyed");
+
+ targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable });
+
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_browser_workers.js b/devtools/shared/commands/target/tests/browser_target_command_browser_workers.js
new file mode 100644
index 0000000000..181cfa2614
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_browser_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 TargetCommand API around workers
+
+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 TargetCommand is almost disabled without it
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ // 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 tab = await addTab(FISSION_TEST_URL);
+
+ info("Test TargetCommand 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 commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+ await targetCommand.startListening();
+
+ // Very naive sanity check against getAllTargets([workerType])
+ info("Check that getAllTargets returned the expected targets");
+ const workers = await targetCommand.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 targetCommand.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 targetCommand.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 targetCommand.getAllTargets([TYPES.WORKER]);
+ const sharedWorkers2 = await targetCommand.getAllTargets([
+ TYPES.SHARED_WORKER,
+ ]);
+ const serviceWorkers2 = await targetCommand.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 topLevelTarget = await commands.targetCommand.targetFront;
+ 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 == topLevelTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ targets.push(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [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`
+ );
+ }
+
+ targetCommand.unwatchTargets({
+ types: [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;
+ }
+ targetCommand.unwatchTargets({
+ types: [TYPES.WORKER],
+ onAvailable: onAvailable2,
+ });
+ resolve(targetFront);
+ };
+ targetCommand.watchTargets({
+ types: [TYPES.WORKER],
+ onAvailable: 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"
+ );
+ is(
+ workerTarget.name,
+ "test_worker.js#second",
+ "The worker target has the expected name"
+ );
+
+ const workers3 = await targetCommand.getAllTargets([TYPES.WORKER]);
+ const hasWorker2 = workers3.find(
+ ({ url }) => url == `${CHROME_WORKER_URL}#second`
+ );
+ ok(hasWorker2, "retrieve the target for tab via getAllTargets");
+
+ info(
+ "Check that terminating the worker does trigger the onDestroyed callback"
+ );
+ const onWorkerDestroyed = new Promise(resolve => {
+ const emptyFn = () => {};
+ const onDestroyed = ({ targetFront }) => {
+ targetCommand.unwatchTargets({
+ types: [TYPES.WORKER],
+ onAvailable: emptyFn,
+ onDestroyed,
+ });
+ resolve(targetFront);
+ };
+
+ targetCommand.watchTargets({
+ types: [TYPES.WORKER],
+ onAvailable: emptyFn,
+ onDestroyed,
+ });
+ });
+ worker2.terminate();
+ const workerTargetFront = await onWorkerDestroyed;
+ ok(true, "onDestroyed was called when the worker was terminated");
+
+ workerTargetFront.isTopLevel;
+ ok(
+ true,
+ "isTopLevel can be called on the target front after onDestroyed was called"
+ );
+
+ workerTargetFront.name;
+ ok(
+ true,
+ "name can be accessed on the target front after onDestroyed was called"
+ );
+
+ targetCommand.destroy();
+
+ info("Unregister service workers so they don't appear in other tests.");
+ await unregisterAllServiceWorkers(commands.client);
+
+ await commands.destroy();
+
+ 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/commands/target/tests/browser_target_command_detach.js b/devtools/shared/commands/target/tests/browser_target_command_detach.js
new file mode 100644
index 0000000000..a0056cd7a5
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_detach.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand's when detaching the top target
+//
+// Do this with the "remote tab" codepath, which will avoid
+// destroying the DevToolsClient when the target is destroyed.
+// Otherwise, with "local tab", the client is closed and everything is destroy
+// on both client and server side.
+
+const TEST_URL = "data:text/html,test-page";
+
+add_task(async function () {
+ info(" ### Test detaching the top target");
+
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(TEST_URL);
+
+ info("Create a first commands, which will destroy its top target");
+ const commands = await CommandsFactory.forRemoteTab(
+ tab.linkedBrowser.browserId
+ );
+ const targetCommand = commands.targetCommand;
+
+ // We have to start listening in order to ensure having a targetFront available
+ await targetCommand.startListening();
+
+ info("Call any target front method, to ensure it works fine");
+ await targetCommand.targetFront.focus();
+
+ // Destroying the target front should end up calling "WindowGlobalTargetActor.detach"
+ // which should destroy the target on the server side
+ await targetCommand.targetFront.destroy();
+
+ info(
+ "Now create a second commands after destroy, to see if we can spawn a new, functional target"
+ );
+ const secondCommands = await CommandsFactory.forRemoteTab(
+ tab.linkedBrowser.browserId,
+ {
+ client: commands.client,
+ }
+ );
+ const secondTargetCommand = secondCommands.targetCommand;
+
+ // We have to start listening in order to ensure having a targetFront available
+ await secondTargetCommand.startListening();
+
+ info("Call any target front method, to ensure it works fine");
+ await secondTargetCommand.targetFront.focus();
+
+ BrowserTestUtils.removeTab(tab);
+
+ info("Close the two commands");
+ await commands.destroy();
+ await secondCommands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_frames.js b/devtools/shared/commands/target/tests/browser_target_command_frames.js
new file mode 100644
index 0000000000..bfa297801a
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_frames.js
@@ -0,0 +1,649 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API around frames
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const IFRAME_URL = URL_ROOT_ORG_SSL + "fission_iframe.html";
+const SECOND_PAGE_URL = "https://example.org/document-builder.sjs?html=org";
+
+const PID_REGEXP = /^\d+$/;
+
+add_task(async function () {
+ // Disable bfcache for Fission for now.
+ // If Fission is disabled, the pref is no-op.
+ await SpecialPowers.pushPrefEnv({
+ set: [["fission.bfcacheInParent", false]],
+ });
+
+ // Enabled fission prefs
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ // 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);
+
+ // Test fetching the frames from the main process descriptor
+ await testBrowserFrames();
+
+ // Test fetching the frames from a tab descriptor
+ await testTabFrames();
+
+ // Test what happens with documents running in the parent process
+ await testOpeningOnParentProcessDocument();
+ await testNavigationToParentProcessDocument();
+
+ // Test what happens with about:blank documents
+ await testOpeningOnAboutBlankDocument();
+ await testNavigationToAboutBlankDocument();
+
+ await testNestedIframes();
+});
+
+async function testOpeningOnParentProcessDocument() {
+ info("Test opening against a parent process document");
+ const tab = await addTab("about:robots");
+ is(
+ tab.linkedBrowser.browsingContext.currentWindowGlobal.osPid,
+ -1,
+ "The tab is loaded in the parent process"
+ );
+
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]);
+ is(frames.length, 1);
+ is(frames[0].url, "about:robots", "target url is correct");
+ is(
+ frames[0],
+ targetCommand.targetFront,
+ "the target is the current top level one"
+ );
+
+ await commands.destroy();
+}
+
+async function testNavigationToParentProcessDocument() {
+ info("Test navigating to parent process document");
+ const firstLocation = "data:text/html,foo";
+ const secondLocation = "about:robots";
+
+ const tab = await addTab(firstLocation);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ // When the first top level target is created from the server,
+ // `startListening` emits a spurious switched-target event
+ // which isn't necessarily emited before it resolves.
+ // So ensure waiting for it, otherwise we may resolve too eagerly
+ // in our expected listener.
+ const onSwitchedTarget1 = targetCommand.once("switched-target");
+ await targetCommand.startListening();
+ info("wait for first top level target");
+ await onSwitchedTarget1;
+
+ const firstTarget = targetCommand.targetFront;
+ is(firstTarget.url, firstLocation, "first target url is correct");
+
+ info("Navigate to a parent process page");
+ const onSwitchedTarget = targetCommand.once("switched-target");
+ const browser = tab.linkedBrowser;
+ const onLoaded = BrowserTestUtils.browserLoaded(browser);
+ await BrowserTestUtils.loadURIString(browser, secondLocation);
+ await onLoaded;
+ is(
+ browser.browsingContext.currentWindowGlobal.osPid,
+ -1,
+ "The tab is loaded in the parent process"
+ );
+
+ await onSwitchedTarget;
+ isnot(targetCommand.targetFront, firstTarget, "got a new target");
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]);
+ is(frames.length, 1);
+ is(frames[0].url, secondLocation, "second target url is correct");
+ is(
+ frames[0],
+ targetCommand.targetFront,
+ "second target is the current top level one"
+ );
+
+ await commands.destroy();
+}
+
+async function testOpeningOnAboutBlankDocument() {
+ info("Test opening against about:blank document");
+ const tab = await addTab("about:blank");
+
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]);
+ is(frames.length, 1);
+ is(frames[0].url, "about:blank", "target url is correct");
+ is(
+ frames[0],
+ targetCommand.targetFront,
+ "the target is the current top level one"
+ );
+
+ await commands.destroy();
+}
+
+async function testNavigationToAboutBlankDocument() {
+ info("Test navigating to about:blank");
+ const firstLocation = "data:text/html,foo";
+ const secondLocation = "about:blank";
+
+ const tab = await addTab(firstLocation);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ // When the first top level target is created from the server,
+ // `startListening` emits a spurious switched-target event
+ // which isn't necessarily emited before it resolves.
+ // So ensure waiting for it, otherwise we may resolve too eagerly
+ // in our expected listener.
+ const onSwitchedTarget1 = targetCommand.once("switched-target");
+ await targetCommand.startListening();
+ info("wait for first top level target");
+ await onSwitchedTarget1;
+
+ const firstTarget = targetCommand.targetFront;
+ is(firstTarget.url, firstLocation, "first target url is correct");
+
+ info("Navigate to about:blank page");
+ const onSwitchedTarget = targetCommand.once("switched-target");
+ const browser = tab.linkedBrowser;
+ const onLoaded = BrowserTestUtils.browserLoaded(browser);
+ await BrowserTestUtils.loadURIString(browser, secondLocation);
+ await onLoaded;
+
+ await onSwitchedTarget;
+ isnot(targetCommand.targetFront, firstTarget, "got a new target");
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]);
+ is(frames.length, 1);
+ is(frames[0].url, secondLocation, "second target url is correct");
+ is(
+ frames[0],
+ targetCommand.targetFront,
+ "second target is the current top level one"
+ );
+
+ await commands.destroy();
+}
+
+async function testBrowserFrames() {
+ info("Test TargetCommand against frames via the parent process target");
+
+ const aboutBlankTab = await addTab("about:blank");
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+ await targetCommand.startListening();
+
+ // Very naive sanity check against getAllTargets([frame])
+ const frames = await targetCommand.getAllTargets([TYPES.FRAME]);
+ const hasBrowserDocument = frames.find(
+ frameTarget => frameTarget.url == window.location.href
+ );
+ ok(hasBrowserDocument, "retrieve the target for the browser document");
+
+ const hasAboutBlankDocument = frames.find(
+ frameTarget =>
+ frameTarget.browsingContextID ==
+ aboutBlankTab.linkedBrowser.browsingContext.id
+ );
+ ok(hasAboutBlankDocument, "retrieve the target for the about:blank tab");
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames2 = await targetCommand.getAllTargets([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 topLevelTarget = targetCommand.targetFront;
+
+ const noParentTarget = await topLevelTarget.getParentTarget();
+ is(noParentTarget, null, "The top level target has no parent target");
+
+ const onAvailable = ({ targetFront }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ targetFront == topLevelTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ ok(
+ PID_REGEXP.test(targetFront.processID),
+ `Target has processID of expected shape (${targetFront.processID})`
+ );
+ targets.push(targetFront);
+ };
+ await targetCommand.watchTargets({ types: [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`
+ );
+ }
+
+ async function addTabAndAssertNewTarget(url) {
+ const previousTargetCount = targets.length;
+ const tab = await addTab(url);
+ await waitFor(
+ () => targets.length == previousTargetCount + 1,
+ "Wait for all expected targets after tab opening"
+ );
+ is(
+ targets.length,
+ previousTargetCount + 1,
+ "Opening a tab reported a new frame"
+ );
+ const newTabTarget = targets.at(-1);
+ is(newTabTarget.url, url, "This frame target is about the new tab");
+ // Internaly, the tab, which uses a <browser type='content'> element is considered detached from their owner document
+ // and so the target is having a null parentInnerWindowId. But the framework will attach all non-top-level targets
+ // as children of the top level.
+ const tabParentTarget = await newTabTarget.getParentTarget();
+ is(
+ tabParentTarget,
+ targetCommand.targetFront,
+ "tab's WindowGlobal/BrowsingContext is detached and has no parent, but we report them as children of the top level target"
+ );
+
+ const frames3 = await targetCommand.getAllTargets([TYPES.FRAME]);
+ const hasTabDocument = frames3.find(target => target.url == url);
+ ok(hasTabDocument, "retrieve the target for tab via getAllTargets");
+
+ return tab;
+ }
+
+ info("Open a tab loaded in content process");
+ await addTabAndAssertNewTarget("data:text/html,content-process-page");
+
+ info("Open a tab loaded in the parent process");
+ const parentProcessTab = await addTabAndAssertNewTarget("about:robots");
+ is(
+ parentProcessTab.linkedBrowser.browsingContext.currentWindowGlobal.osPid,
+ -1,
+ "The tab is loaded in the parent process"
+ );
+
+ info("Open a new content window via window.open");
+ info("First open a tab on .org domain");
+ const tabUrl = "https://example.org/document-builder.sjs?html=org";
+ await addTabAndAssertNewTarget(tabUrl);
+ const previousTargetCount = targets.length;
+
+ info("Then open a popup on .com domain");
+ const popupUrl = "https://example.com/document-builder.sjs?html=com";
+ const onPopupOpened = BrowserTestUtils.waitForNewTab(gBrowser, popupUrl);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [popupUrl], async url => {
+ content.window.open(url, "_blank");
+ });
+ await onPopupOpened;
+
+ await waitFor(
+ () => targets.length == previousTargetCount + 1,
+ "Wait for all expected targets after window.open()"
+ );
+ is(
+ targets.length,
+ previousTargetCount + 1,
+ "Opening a new content window reported a new frame"
+ );
+ is(
+ targets.at(-1).url,
+ popupUrl,
+ "This frame target is about the new content window"
+ );
+
+ // About:blank are a bit special because we ignore a transcient about:blank
+ // document when navigating to another process. But we should not ignore
+ // tabs, loading a real, final about:blank document.
+ info("Open a tab with about:blank");
+ await addTabAndAssertNewTarget("about:blank");
+
+ // Until we start spawning target for all WindowGlobals,
+ // including the one running in the same process as their parent,
+ // we won't create dedicated target for new top level windows.
+ // Instead, these document will be debugged via the ParentProcessTargetActor.
+ info("Open a top level chrome window");
+ const expectedTargets = targets.length;
+ const chromeWindow = Services.ww.openWindow(
+ null,
+ "about:robots",
+ "_blank",
+ "chrome",
+ null
+ );
+ await wait(250);
+ is(
+ targets.length,
+ expectedTargets,
+ "New top level window shouldn't spawn new target"
+ );
+ chromeWindow.close();
+
+ targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable });
+
+ targetCommand.destroy();
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ await commands.destroy();
+}
+
+async function testTabFrames(mainRoot) {
+ info("Test TargetCommand against frames via a tab target");
+
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(FISSION_TEST_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames = await targetCommand.getAllTargets([TYPES.FRAME]);
+ // When fission is enabled, we also get the remote example.org iframe.
+ const expectedFramesCount =
+ isFissionEnabled() || isEveryFrameTargetEnabled() ? 2 : 1;
+ is(
+ frames.length,
+ expectedFramesCount,
+ "retrieved the expected number of targets"
+ );
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const destroyedTargets = [];
+ const topLevelTarget = targetCommand.targetFront;
+ const onAvailable = ({ targetFront, isTargetSwitching }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ PID_REGEXP.test(targetFront.processID),
+ `Target has processID of expected shape (${targetFront.processID})`
+ );
+ targets.push({ targetFront, isTargetSwitching });
+ };
+ const onDestroyed = ({ targetFront, isTargetSwitching }) => {
+ is(
+ targetFront.targetType,
+ TYPES.FRAME,
+ "We are only notified about frame targets"
+ );
+ ok(
+ targetFront == topLevelTarget
+ ? targetFront.isTopLevel
+ : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ destroyedTargets.push({ targetFront, isTargetSwitching });
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+ is(
+ targets.length,
+ frames.length,
+ "retrieved the same number of frames via watchTargets"
+ );
+ is(destroyedTargets.length, 0, "Should be no destroyed target initialy");
+
+ for (const frame of frames) {
+ ok(
+ targets.find(({ targetFront }) => targetFront === frame),
+ "frame " + frame.actorID + " target is the same via watchTargets"
+ );
+ }
+ is(
+ targets[0].targetFront.url,
+ FISSION_TEST_URL,
+ "First target should be the top document one"
+ );
+ is(
+ targets[0].targetFront.isTopLevel,
+ true,
+ "First target is a top level one"
+ );
+ is(
+ !targets[0].isTargetSwitching,
+ true,
+ "First target is not considered as a target switching"
+ );
+ const noParentTarget = await targets[0].targetFront.getParentTarget();
+ is(noParentTarget, null, "The top level target has no parent target");
+
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ is(
+ targets[1].targetFront.url,
+ IFRAME_URL,
+ "Second target should be the iframe one"
+ );
+ is(
+ !targets[1].targetFront.isTopLevel,
+ true,
+ "Iframe target isn't top level"
+ );
+ is(
+ !targets[1].isTargetSwitching,
+ true,
+ "Iframe target isn't a target swich"
+ );
+ const parentTarget = await targets[1].targetFront.getParentTarget();
+ is(
+ parentTarget,
+ targets[0].targetFront,
+ "The parent target for the iframe is the top level target"
+ );
+ }
+
+ // Before navigating to another process, ensure cleaning up everything from the first page
+ await waitForAllTargetsToBeAttached(targetCommand);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // registrationPromise is set by the test page.
+ const registration = await content.wrappedJSObject.registrationPromise;
+ registration.unregister();
+ });
+
+ info("Navigate to another domain and process (if fission is enabled)");
+ // When a new target will be created, we need to wait until it's fully processed
+ // to avoid pending promises.
+ const onNewTargetProcessed = targetCommand.once("processed-available-target");
+
+ const browser = tab.linkedBrowser;
+ const onLoaded = BrowserTestUtils.browserLoaded(browser);
+ await BrowserTestUtils.loadURIString(browser, SECOND_PAGE_URL);
+ await onLoaded;
+
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ const afterNavigationFramesCount = 3;
+ await waitFor(
+ () => targets.length == afterNavigationFramesCount,
+ "Wait for all expected targets after navigation"
+ );
+ is(
+ targets.length,
+ afterNavigationFramesCount,
+ "retrieved all targets after navigation"
+ );
+ // As targetFront.url isn't reliable and might be about:blank,
+ // try to assert that we got the right target via other means.
+ // outerWindowID should change when navigating to another process,
+ // while it would stay equal for in-process navigations.
+ is(
+ targets[2].targetFront.outerWindowID,
+ browser.outerWindowID,
+ "The new target should be the newly loaded document"
+ );
+ is(
+ targets[2].isTargetSwitching,
+ true,
+ "and should be flagged as a target switching"
+ );
+
+ is(
+ destroyedTargets.length,
+ 2,
+ "The two existing targets should be destroyed"
+ );
+ is(
+ destroyedTargets[0].targetFront,
+ targets[1].targetFront,
+ "The first destroyed should be the iframe one"
+ );
+ is(
+ destroyedTargets[0].isTargetSwitching,
+ false,
+ "the target destruction is not flagged as target switching for iframes"
+ );
+ is(
+ destroyedTargets[1].targetFront,
+ targets[0].targetFront,
+ "The second destroyed should be the previous top level one (because it is delayed to be fired *after* will-navigate)"
+ );
+ is(
+ destroyedTargets[1].isTargetSwitching,
+ true,
+ "the target destruction is flagged as target switching"
+ );
+ } else {
+ await waitFor(
+ () => targets.length == 2,
+ "Wait for all expected targets after navigation"
+ );
+ is(
+ destroyedTargets.length,
+ 1,
+ "with JSWindowActor based target, the top level target is destroyed"
+ );
+ is(
+ targetCommand.targetFront,
+ targets[1].targetFront,
+ "we got a new target"
+ );
+ ok(
+ !targetCommand.targetFront.isDestroyed(),
+ "that target is not destroyed"
+ );
+ ok(
+ targets[0].targetFront.isDestroyed(),
+ "but the previous one is destroyed"
+ );
+ }
+
+ await onNewTargetProcessed;
+
+ targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable });
+
+ targetCommand.destroy();
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
+
+async function testNestedIframes() {
+ info("Test TargetCommand against nested frames");
+
+ const nestedIframeUrl = `https://example.com/document-builder.sjs?html=${encodeURIComponent(
+ "<title>second</title><h3>second level iframe</h3>"
+ )}&delay=500`;
+
+ const testUrl = `data:text/html;charset=utf-8,
+ <h1>Top-level</h1>
+ <iframe id=first-level
+ src='data:text/html;charset=utf-8,${encodeURIComponent(
+ `<title>first</title><h2>first level iframe</h2><iframe id=second-level src="${nestedIframeUrl}"></iframe>`
+ )}'
+ ></iframe>`;
+
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(testUrl);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Check that calling getAllTargets([frame]) return the same target instances
+ const frames = await targetCommand.getAllTargets([TYPES.FRAME]);
+
+ is(frames[0], targetCommand.targetFront, "First target is the top level one");
+ const topParent = await frames[0].getParentTarget();
+ is(topParent, null, "Top level target has no parent");
+
+ if (isEveryFrameTargetEnabled()) {
+ const firstIframeTarget = frames.find(target => target.title == "first");
+ ok(
+ firstIframeTarget,
+ "With EFT, got the target for the first level iframe"
+ );
+ const firstParent = await firstIframeTarget.getParentTarget();
+ is(
+ firstParent,
+ targetCommand.targetFront,
+ "With EFT, first level has top level target as parent"
+ );
+
+ const secondIframeTarget = frames.find(target => target.title == "second");
+ ok(secondIframeTarget, "Got the target for the second level iframe");
+ const secondParent = await secondIframeTarget.getParentTarget();
+ is(
+ secondParent,
+ firstIframeTarget,
+ "With EFT, second level has the first level target as parent"
+ );
+ } else if (isFissionEnabled()) {
+ const secondIframeTarget = frames.find(target => target.title == "second");
+ ok(secondIframeTarget, "Got the target for the second level iframe");
+ const secondParent = await secondIframeTarget.getParentTarget();
+ is(
+ secondParent,
+ targetCommand.targetFront,
+ "With fission, second level has top level target as parent"
+ );
+ }
+
+ await commands.destroy();
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_frames_popups.js b/devtools/shared/commands/target/tests/browser_target_command_frames_popups.js
new file mode 100644
index 0000000000..68f7244671
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_frames_popups.js
@@ -0,0 +1,168 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we create targets for popups
+
+const TEST_URL = "https://example.org/document-builder.sjs?html=main page";
+const POPUP_URL = "https://example.com/document-builder.sjs?html=popup";
+const POPUP_SECOND_URL =
+ "https://example.com/document-builder.sjs?html=popup-navigated";
+
+add_task(async function () {
+ await pushPref("devtools.popups.debug", true);
+ // We expect to create a target for a same-process iframe
+ // in the test against window.open to load a document in an iframe.
+ await pushPref("devtools.every-frame-target.enabled", true);
+
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(TEST_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const destroyedTargets = [];
+ const onAvailable = ({ targetFront }) => {
+ targets.push(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ destroyedTargets.push(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+
+ is(targets.length, 1, "At first, we only get one target");
+ is(
+ targets[0],
+ targetCommand.targetFront,
+ "And this target is the top level one"
+ );
+
+ info("Open a popup");
+ const firstPopupBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [POPUP_URL],
+ url => {
+ const win = content.open(url);
+ return win.browsingContext;
+ }
+ );
+
+ await waitFor(() => targets.length === 2);
+ ok(true, "We are notified about the first popup's target");
+
+ is(
+ targets[1].browsingContextID,
+ firstPopupBrowsingContext.id,
+ "the new target is for the popup"
+ );
+ is(targets[1].url, POPUP_URL, "the new target has the right url");
+
+ info("Navigate the popup to a second location");
+ await SpecialPowers.spawn(
+ firstPopupBrowsingContext,
+ [POPUP_SECOND_URL],
+ url => {
+ content.location.href = url;
+ }
+ );
+
+ await waitFor(() => targets.length === 3);
+ ok(true, "We are notified about the new location popup's target");
+
+ await waitFor(() => destroyedTargets.length === 1);
+ ok(true, "The first popup's target is destroyed");
+ is(
+ destroyedTargets[0],
+ targets[1],
+ "The destroyed target is the popup's one"
+ );
+
+ is(
+ targets[2].browsingContextID,
+ firstPopupBrowsingContext.id,
+ "the new location target is for the popup"
+ );
+ is(
+ targets[2].url,
+ POPUP_SECOND_URL,
+ "the new location target has the right url"
+ );
+
+ info("Close the popup");
+ await SpecialPowers.spawn(firstPopupBrowsingContext, [], () => {
+ content.close();
+ });
+
+ await waitFor(() => destroyedTargets.length === 2);
+ ok(true, "The popup's target is destroyed");
+ is(
+ destroyedTargets[1],
+ targets[2],
+ "The destroyed target is the popup's one"
+ );
+
+ info("Open a about:blank popup");
+ const aboutBlankPopupBrowsingContext = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ const win = content.open("about:blank");
+ return win.browsingContext;
+ }
+ );
+
+ await waitFor(() => targets.length === 4);
+ ok(true, "We are notified about the about:blank popup's target");
+
+ is(
+ targets[3].browsingContextID,
+ aboutBlankPopupBrowsingContext.id,
+ "the new target is for the popup"
+ );
+ is(targets[3].url, "about:blank", "the new target has the right url");
+
+ info("Select the original tab and reload it");
+ gBrowser.selectedTab = tab;
+ await BrowserTestUtils.reloadTab(tab);
+
+ await waitFor(() => targets.length === 5);
+ is(targets[4], targetCommand.targetFront, "We get a new top level target");
+ ok(!targets[3].isDestroyed(), "The about:blank popup target is still alive");
+
+ info("Call about:blank popup method to ensure it really is functional");
+ await targets[3].logInPage("foo");
+
+ info(
+ "Ensure that iframe using window.open to load their document aren't considered as popups"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ const iframe = content.document.createElement("iframe");
+ iframe.setAttribute("name", "test-iframe");
+ content.document.documentElement.appendChild(iframe);
+ content.open("data:text/html,iframe", "test-iframe");
+ });
+ await waitFor(() => targets.length === 6);
+ is(
+ targets[5].targetForm.isPopup,
+ false,
+ "The iframe target isn't considered as a popup"
+ );
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+ targetCommand.destroy();
+ BrowserTestUtils.removeTab(tab);
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_frames_reload_server_side_targets.js b/devtools/shared/commands/target/tests/browser_target_command_frames_reload_server_side_targets.js
new file mode 100644
index 0000000000..d05ff5a962
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_frames_reload_server_side_targets.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the framework handles reloading a document with multiple remote frames (See Bug 1724909).
+
+const REMOTE_ORIGIN = "https://example.com/";
+const REMOTE_IFRAME_URL_1 =
+ REMOTE_ORIGIN + "/document-builder.sjs?html=first_remote_iframe";
+const REMOTE_IFRAME_URL_2 =
+ REMOTE_ORIGIN + "/document-builder.sjs?html=second_remote_iframe";
+const TEST_URL =
+ "https://example.org/document-builder.sjs?html=org" +
+ `<iframe src=${REMOTE_IFRAME_URL_1}></iframe>` +
+ `<iframe src=${REMOTE_IFRAME_URL_2}></iframe>`;
+
+add_task(async function () {
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(TEST_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.startListening();
+
+ // Assert that watchTargets will call the create callback for all existing frames
+ const targets = [];
+ const destroyedTargets = [];
+ const onAvailable = ({ targetFront }) => {
+ targets.push(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ destroyedTargets.push(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+
+ await waitFor(() => targets.length === 3);
+ ok(
+ true,
+ "We are notified about the top-level document and the 2 remote iframes"
+ );
+
+ info("Reload the page");
+ // When a new target will be created, we need to wait until it's fully processed
+ // to avoid pending promises.
+ const onNewTargetProcessed = targetCommand.once("processed-available-target");
+ gBrowser.reloadTab(tab);
+ await onNewTargetProcessed;
+
+ await waitFor(() => targets.length === 6 && destroyedTargets.length === 3);
+
+ // Get the previous targets in a dedicated array and remove them from `targets`
+ const previousTargets = targets.splice(0, 3);
+ ok(
+ previousTargets.every(targetFront => targetFront.isDestroyed()),
+ "The previous targets are all destroyed"
+ );
+ ok(
+ targets.every(targetFront => !targetFront.isDestroyed()),
+ "The new targets are not destroyed"
+ );
+
+ info("Reload one of the iframe");
+ SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const iframeEl = content.document.querySelector("iframe");
+ SpecialPowers.spawn(iframeEl.browsingContext, [], () => {
+ content.document.location.reload();
+ });
+ });
+ await waitFor(
+ () =>
+ targets.length + previousTargets.length === 7 &&
+ destroyedTargets.length === 4
+ );
+ const iframeTarget = targets.find(t => t === destroyedTargets.at(-1));
+ ok(iframeTarget, "Got the iframe target that got destroyed");
+ for (const target of targets) {
+ if (target == iframeTarget) {
+ ok(
+ target.isDestroyed(),
+ "The iframe target we navigated from is destroyed"
+ );
+ } else {
+ ok(
+ !target.isDestroyed(),
+ `Target ${target.actorID}|${target.url} isn't destroyed`
+ );
+ }
+ }
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+ targetCommand.destroy();
+ BrowserTestUtils.removeTab(tab);
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_getAllTargets.js b/devtools/shared/commands/target/tests/browser_target_command_getAllTargets.js
new file mode 100644
index 0000000000..a7d5e51b3c
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_getAllTargets.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API getAllTargets.
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const CHROME_WORKER_URL = CHROME_URL_ROOT + "test_worker.js";
+
+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);
+
+ info("Setup the test page with workers of all types");
+
+ 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 commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+ await targetCommand.startListening();
+
+ info("Check getAllTargets will throw when providing invalid arguments");
+ Assert.throws(
+ () => targetCommand.getAllTargets(),
+ e => e.message === "getAllTargets expects a non-empty array of types"
+ );
+
+ Assert.throws(
+ () => targetCommand.getAllTargets([]),
+ e => e.message === "getAllTargets expects a non-empty array of types"
+ );
+
+ info("Check getAllTargets returns consistent results with several types");
+ const workerTargets = targetCommand.getAllTargets([TYPES.WORKER]);
+ const serviceWorkerTargets = targetCommand.getAllTargets([
+ TYPES.SERVICE_WORKER,
+ ]);
+ const sharedWorkerTargets = targetCommand.getAllTargets([
+ TYPES.SHARED_WORKER,
+ ]);
+ const processTargets = targetCommand.getAllTargets([TYPES.PROCESS]);
+ const frameTargets = targetCommand.getAllTargets([TYPES.FRAME]);
+
+ const allWorkerTargetsReference = [
+ ...workerTargets,
+ ...serviceWorkerTargets,
+ ...sharedWorkerTargets,
+ ];
+ const allWorkerTargets = targetCommand.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 = targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ is(
+ allTargets.length,
+ allTargetsReference.length,
+ "getAllTargets(ALL_TYPES) returned the expected number of targets"
+ );
+
+ ok(
+ allTargets.every(t => allTargetsReference.includes(t)),
+ "getAllTargets(ALL_TYPES) returned the expected targets"
+ );
+
+ for (const target of allTargets) {
+ is(
+ target.commands,
+ commands,
+ "Each target front has a `commands` attribute - " + target
+ );
+ }
+
+ // Wait for all the targets to be fully attached so we don't have pending requests.
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ ok(
+ !targetCommand.isDestroyed(),
+ "TargetCommand isn't destroyed before calling commands.destroy()"
+ );
+ await commands.destroy();
+ ok(
+ targetCommand.isDestroyed(),
+ "TargetCommand is destroyed after calling commands.destroy()"
+ );
+
+ 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/commands/target/tests/browser_target_command_invalid_api_usage.js b/devtools/shared/commands/target/tests/browser_target_command_invalid_api_usage.js
new file mode 100644
index 0000000000..dbdaae7f05
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_invalid_api_usage.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test watch/unwatchTargets throw when provided with invalid types.
+
+const TEST_URL = "data:text/html;charset=utf-8,invalid api usage test";
+
+add_task(async function () {
+ info("Setup the test page with workers of all types");
+ const tab = await addTab(TEST_URL);
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+
+ const onAvailable = function () {};
+
+ await Assert.rejects(
+ targetCommand.watchTargets({ types: [null], onAvailable }),
+ /TargetCommand.watchTargets invoked with an unknown type/,
+ "watchTargets should throw for null type"
+ );
+
+ await Assert.rejects(
+ targetCommand.watchTargets({ types: [undefined], onAvailable }),
+ /TargetCommand.watchTargets invoked with an unknown type/,
+ "watchTargets should throw for undefined type"
+ );
+
+ await Assert.rejects(
+ targetCommand.watchTargets({ types: ["NOT_A_TARGET"], onAvailable }),
+ /TargetCommand.watchTargets invoked with an unknown type/,
+ "watchTargets should throw for unknown type"
+ );
+
+ await Assert.rejects(
+ targetCommand.watchTargets({
+ types: [targetCommand.TYPES.FRAME, "NOT_A_TARGET"],
+ onAvailable,
+ }),
+ /TargetCommand.watchTargets invoked with an unknown type/,
+ "watchTargets should throw for unknown type mixed with a correct type"
+ );
+
+ Assert.throws(
+ () => targetCommand.unwatchTargets({ types: [null], onAvailable }),
+ /TargetCommand.unwatchTargets invoked with an unknown type/,
+ "unwatchTargets should throw for null type"
+ );
+
+ Assert.throws(
+ () => targetCommand.unwatchTargets({ types: [undefined], onAvailable }),
+ /TargetCommand.unwatchTargets invoked with an unknown type/,
+ "unwatchTargets should throw for undefined type"
+ );
+
+ Assert.throws(
+ () =>
+ targetCommand.unwatchTargets({ types: ["NOT_A_TARGET"], onAvailable }),
+ /TargetCommand.unwatchTargets invoked with an unknown type/,
+ "unwatchTargets should throw for unknown type"
+ );
+
+ Assert.throws(
+ () =>
+ targetCommand.unwatchTargets({
+ types: [targetCommand.TYPES.CONSOLE_MESSAGE, "NOT_A_TARGET"],
+ onAvailable,
+ }),
+ /TargetCommand.unwatchTargets invoked with an unknown type/,
+ "unwatchTargets should throw for unknown type mixed with a correct type"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_processes.js b/devtools/shared/commands/target/tests/browser_target_command_processes.js
new file mode 100644
index 0000000000..d9e7ec65a9
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_processes.js
@@ -0,0 +1,242 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API around processes
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
+
+add_task(async function () {
+ // Enabled fission's pref as the TargetCommand is almost disabled without it
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ // 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 commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ await testProcesses(targetCommand, targetCommand.targetFront);
+
+ targetCommand.destroy();
+ // Wait for all the targets to be fully attached so we don't have pending requests.
+ await Promise.all(
+ targetCommand.getAllTargets(targetCommand.ALL_TYPES).map(t => t.initialized)
+ );
+
+ await commands.destroy();
+});
+
+add_task(async function () {
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const created = [];
+ const destroyed = [];
+ const onAvailable = ({ targetFront }) => {
+ created.push(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ destroyed.push(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [targetCommand.TYPES.PROCESS],
+ onAvailable,
+ onDestroyed,
+ });
+ ok(created.length > 1, "We get many content process targets");
+
+ targetCommand.stopListening();
+
+ await waitFor(
+ () => created.length == destroyed.length,
+ "Wait for the destruction of all content process targets when calling stopListening"
+ );
+ is(
+ created.length,
+ destroyed.length,
+ "Got notification of destruction for all previously reported targets"
+ );
+
+ targetCommand.destroy();
+ // Wait for all the targets to be fully attached so we don't have pending requests.
+ await Promise.all(
+ targetCommand.getAllTargets(targetCommand.ALL_TYPES).map(t => t.initialized)
+ );
+
+ await commands.destroy();
+});
+
+async function testProcesses(targetCommand, target) {
+ info("Test TargetCommand against processes");
+ const { TYPES } = targetCommand;
+
+ // 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 targetCommand.getAllTargets([TYPES.PROCESS]);
+ is(
+ processes.length,
+ originalProcessesCount,
+ "Get a target for all content processes"
+ );
+
+ const processes2 = await targetCommand.getAllTargets([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 pidRegExp = /^\d+$/;
+
+ const onAvailable = ({ targetFront }) => {
+ if (targets.has(targetFront)) {
+ ok(false, "The same target is notified multiple times via onAvailable");
+ }
+ is(
+ targetFront.targetType,
+ TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ ok(
+ targetFront == target ? targetFront.isTopLevel : !targetFront.isTopLevel,
+ "isTopLevel property is correct"
+ );
+ ok(
+ pidRegExp.test(targetFront.processID),
+ `Target has processID of expected shape (${targetFront.processID})`
+ );
+ targets.add(targetFront);
+ };
+ const onDestroyed = ({ targetFront }) => {
+ if (!targets.has(targetFront)) {
+ ok(
+ false,
+ "A target is declared destroyed via onDestroy without being notified via onAvailable"
+ );
+ }
+ is(
+ targetFront.targetType,
+ 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 targetCommand.watchTargets({
+ types: [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;
+ }
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable2,
+ });
+ resolve(targetFront);
+ };
+ targetCommand.watchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: 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 onDestroy is called for destroyed processes
+ const onProcessDestroyed = new Promise(resolve => {
+ const onAvailable3 = () => {};
+ const onDestroyed3 = ({ targetFront }) => {
+ resolve(targetFront);
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable3,
+ onDestroyed: onDestroyed3,
+ });
+ };
+ targetCommand.watchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable3,
+ onDestroyed: 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"
+ );
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable,
+ onDestroyed,
+ });
+
+ // Ensure that getAllTargets still works after the call to unwatchTargets
+ const processes3 = await targetCommand.getAllTargets([TYPES.PROCESS]);
+ is(
+ processes3.length,
+ processCountAfterTabOpen - 1,
+ "getAllTargets reports a new target"
+ );
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_reload.js b/devtools/shared/commands/target/tests/browser_target_command_reload.js
new file mode 100644
index 0000000000..9d8cacd23d
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_reload.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand's reload method
+//
+// Note that we reload against main process,
+// but this is hard/impossible to test as it reloads the test script itself
+// and so stops its execution.
+
+// Load a page with a JS script that change its value everytime we load it
+// (that's to see if the reload loads from cache or not)
+const TEST_URL = URL_ROOT + "incremental-js-value-script.sjs";
+
+add_task(async function () {
+ info(" ### Test reloading a Tab");
+
+ // Create a TargetCommand for a given test tab
+ const tab = await addTab(TEST_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+
+ // We have to start listening in order to ensure having a targetFront available
+ await targetCommand.startListening();
+
+ const firstJSValue = await getContentVariable();
+ is(firstJSValue, "1", "Got an initial value for the JS variable");
+
+ const onReloaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await targetCommand.reloadTopLevelTarget();
+ info("Wait for the tab to be reloaded");
+ await onReloaded;
+
+ const secondJSValue = await getContentVariable();
+ is(
+ secondJSValue,
+ "1",
+ "The first reload didn't bypass the cache, so the JS Script is the same and we got the same value"
+ );
+
+ const onSecondReloaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ await targetCommand.reloadTopLevelTarget(true);
+ info("Wait for the tab to be reloaded");
+ await onSecondReloaded;
+
+ // The value is 3 and not 2, because we got a HTTP request, but it returned 304 and the browser fetched his cached content
+ const thirdJSValue = await getContentVariable();
+ is(
+ thirdJSValue,
+ "3",
+ "The second reload did bypass the cache, so the JS Script is different and we got a new value"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+});
+
+add_task(async function () {
+ info(" ### Test reloading an Add-on");
+
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ background() {
+ const { browser } = this;
+ browser.test.log("background script executed");
+ },
+ });
+
+ await extension.startup();
+
+ const commands = await CommandsFactory.forAddon(extension.id);
+ const targetCommand = commands.targetCommand;
+
+ // We have to start listening in order to ensure having a targetFront available
+ await targetCommand.startListening();
+
+ const { onResource: onReloaded } =
+ await commands.resourceCommand.waitForNextResource(
+ commands.resourceCommand.TYPES.DOCUMENT_EVENT,
+ {
+ ignoreExistingResources: true,
+ predicate(resource) {
+ return resource.name == "dom-loading";
+ },
+ }
+ );
+
+ const backgroundPageURL = targetCommand.targetFront.url;
+ ok(backgroundPageURL, "Got the background page URL");
+ await targetCommand.reloadTopLevelTarget();
+
+ info("Wait for next dom-loading DOCUMENT_EVENT");
+ const event = await onReloaded;
+
+ // If we get about:blank here, it most likely means we receive notification
+ // for the previous background page being unload and navigating to about:blank
+ is(
+ event.url,
+ backgroundPageURL,
+ "We received the DOCUMENT_EVENT's for the expected document: the new background page."
+ );
+
+ await commands.destroy();
+
+ await extension.unload();
+});
+function getContentVariable() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.jsValue;
+ });
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_scope_flag.js b/devtools/shared/commands/target/tests/browser_target_command_scope_flag.js
new file mode 100644
index 0000000000..f07a6aaac3
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_scope_flag.js
@@ -0,0 +1,190 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API with changes made to devtools.browsertoolbox.scope
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
+
+add_task(async function () {
+ // Do not run this test when both fission and EFT is disabled as it changes
+ // the number of targets
+ if (!isFissionEnabled() && !isEveryFrameTargetEnabled()) {
+ return;
+ }
+
+ // 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);
+
+ // First test with multiprocess debugging enabled
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+ const { TYPES } = targetCommand;
+
+ const targets = new Set();
+ const destroyedTargetIsModeSwitchingMap = new Map();
+ const onAvailable = async ({ targetFront }) => {
+ targets.add(targetFront);
+ };
+ const onDestroyed = ({ targetFront, isModeSwitching }) => {
+ destroyedTargetIsModeSwitchingMap.set(targetFront, isModeSwitching);
+ targets.delete(targetFront);
+ };
+ await targetCommand.watchTargets({
+ types: [TYPES.PROCESS, TYPES.FRAME],
+ onAvailable,
+ onDestroyed,
+ });
+ ok(targets.size > 1, "We get many targets");
+
+ info("Open a tab in a new content process");
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+
+ const newTabProcessID =
+ gBrowser.selectedTab.linkedBrowser.browsingContext.currentWindowGlobal
+ .osPid;
+ const newTabInnerWindowId =
+ gBrowser.selectedTab.linkedBrowser.browsingContext.currentWindowGlobal
+ .innerWindowId;
+
+ info("Wait for the tab content process target");
+ const processTarget = await waitFor(() =>
+ [...targets].find(
+ target =>
+ target.targetType == TYPES.PROCESS &&
+ target.processID == newTabProcessID
+ )
+ );
+
+ info("Wait for the tab window global target");
+ const windowGlobalTarget = await waitFor(() =>
+ [...targets].find(
+ target =>
+ target.targetType == TYPES.FRAME &&
+ target.innerWindowId == newTabInnerWindowId
+ )
+ );
+
+ let multiprocessTargetCount = targets.size;
+
+ info("Disable multiprocess debugging");
+ await pushPref("devtools.browsertoolbox.scope", "parent-process");
+
+ info("Wait for all targets but top level and workers to be destroyed");
+ await waitFor(() =>
+ [...targets].every(
+ target =>
+ target == targetCommand.targetFront || target.targetType == TYPES.WORKER
+ )
+ );
+
+ ok(processTarget.isDestroyed(), "The process target is destroyed");
+ ok(
+ destroyedTargetIsModeSwitchingMap.get(processTarget),
+ "isModeSwitching was passed to onTargetDestroyed and is true for the process target"
+ );
+ ok(windowGlobalTarget.isDestroyed(), "The window global target is destroyed");
+ ok(
+ destroyedTargetIsModeSwitchingMap.get(windowGlobalTarget),
+ "isModeSwitching was passed to onTargetDestroyed and is true for the window global target"
+ );
+
+ info("Open a second tab in a new content process");
+ const parentProcessTargetCount = targets.size;
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+
+ await wait(1000);
+ is(
+ parentProcessTargetCount,
+ targets.size,
+ "The new tab process should be ignored and no target be created"
+ );
+
+ info("Re-enable multiprocess debugging");
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+
+ // The second tab relates to one content process target and one window global target
+ multiprocessTargetCount += 2;
+
+ await waitFor(
+ () => targets.size == multiprocessTargetCount,
+ "Wait for all targets we used to have before disable multiprocess debugging"
+ );
+
+ info("Wait for the tab content process target to be available again");
+ ok(
+ [...targets].some(
+ target =>
+ target.targetType == TYPES.PROCESS &&
+ target.processID == newTabProcessID
+ ),
+ "We have the tab content process target"
+ );
+
+ info("Wait for the tab window global target to be available again");
+ ok(
+ [...targets].some(
+ target =>
+ target.targetType == TYPES.FRAME &&
+ target.innerWindowId == newTabInnerWindowId
+ ),
+ "We have the tab window global target"
+ );
+
+ info("Open a third tab in a new content process");
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ forceNewProcess: true,
+ });
+
+ const thirdTabProcessID =
+ gBrowser.selectedTab.linkedBrowser.browsingContext.currentWindowGlobal
+ .osPid;
+ const thirdTabInnerWindowId =
+ gBrowser.selectedTab.linkedBrowser.browsingContext.currentWindowGlobal
+ .innerWindowId;
+
+ info("Wait for the third tab content process target");
+ await waitFor(() =>
+ [...targets].find(
+ target =>
+ target.targetType == TYPES.PROCESS &&
+ target.processID == thirdTabProcessID
+ )
+ );
+
+ info("Wait for the third tab window global target");
+ await waitFor(() =>
+ [...targets].find(
+ target =>
+ target.targetType == TYPES.FRAME &&
+ target.innerWindowId == thirdTabInnerWindowId
+ )
+ );
+
+ targetCommand.destroy();
+
+ // Wait for all the targets to be fully attached so we don't have pending requests.
+ await Promise.all(
+ targetCommand.getAllTargets(targetCommand.ALL_TYPES).map(t => t.initialized)
+ );
+
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_service_workers.js b/devtools/shared/commands/target/tests/browser_target_command_service_workers.js
new file mode 100644
index 0000000000..d71401fd8c
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_service_workers.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API for service workers in content tabs.
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.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);
+
+ info("Setup the test page with workers of all types");
+
+ const tab = await addTab(FISSION_TEST_URL);
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ // Enable Service Worker listening.
+ targetCommand.listenForServiceWorkers = true;
+ await targetCommand.startListening();
+
+ const serviceWorkerTargets = targetCommand.getAllTargets([
+ TYPES.SERVICE_WORKER,
+ ]);
+ is(
+ serviceWorkerTargets.length,
+ 1,
+ "TargetCommmand 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 targetCommand.watchTargets({
+ types: [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
+ targetCommand.destroy();
+
+ await commands.waitForRequestsToSettle();
+
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_service_workers_navigation.js b/devtools/shared/commands/target/tests/browser_target_command_service_workers_navigation.js
new file mode 100644
index 0000000000..caf95f11c2
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_service_workers_navigation.js
@@ -0,0 +1,389 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand 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 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() {
+ await setupServiceWorkerNavigationTest();
+
+ const tab = await addTab(COM_PAGE_URL);
+
+ const { hooks, commands, targetCommand } = await watchServiceWorkerTargets({
+ 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.loadURIString(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");
+ await BrowserTestUtils.reloadTab(gBrowser.selectedTab);
+ 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 .com page");
+ const onBrowserLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, COM_PAGE_URL);
+ await onBrowserLoaded;
+ 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
+ targetCommand.destroy();
+
+ await commands.waitForRequestsToSettle();
+ await commands.destroy();
+ 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() {
+ await setupServiceWorkerNavigationTest();
+
+ const tab = await addTab(COM_PAGE_URL);
+
+ const { hooks, commands, targetCommand } = await watchServiceWorkerTargets({
+ 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.loadURIString(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.loadURIString(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
+ targetCommand.destroy();
+
+ await commands.waitForRequestsToSettle();
+ await commands.destroy();
+ await removeTab(tab);
+});
+
+/**
+ * In this test we load a service worker in a page prior to starting the
+ * TargetCommand. 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 TargetCommand.
+ *
+ * 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,
+}) {
+ 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");
+ let onBrowserLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.loadURIString(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");
+ // wait for the browser to be loaded otherwise the task spawned in waitForRegistrationReady
+ // might be destroyed (when it still belongs to the previous content process)
+ await onBrowserLoaded;
+ await waitForRegistrationReady(tab, ORG_PAGE_URL);
+
+ const { hooks, commands, targetCommand } = await watchServiceWorkerTargets({
+ 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");
+ onBrowserLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, COM_PAGE_URL);
+ await onBrowserLoaded;
+
+ 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
+ targetCommand.destroy();
+
+ await commands.waitForRequestsToSettle();
+ await commands.destroy();
+ await removeTab(tab);
+}
+
+async function setupServiceWorkerNavigationTest() {
+ // 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);
+}
+
+async function watchServiceWorkerTargets({
+ destroyServiceWorkersOnNavigation,
+ tab,
+}) {
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+
+ // Enable Service Worker listening.
+ targetCommand.listenForServiceWorkers = true;
+ info(
+ "Set targetCommand.destroyServiceWorkersOnNavigation to " +
+ destroyServiceWorkersOnNavigation
+ );
+ targetCommand.destroyServiceWorkersOnNavigation =
+ destroyServiceWorkersOnNavigation;
+ await targetCommand.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 targetCommand.watchTargets({
+ types: [targetCommand.TYPES.SERVICE_WORKER],
+ onAvailable,
+ onDestroyed,
+ });
+
+ return { hooks, commands, targetCommand };
+}
+
+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/commands/target/tests/browser_target_command_switchToTarget.js b/devtools/shared/commands/target/tests/browser_target_command_switchToTarget.js
new file mode 100644
index 0000000000..04646117a9
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_switchToTarget.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API switchToTarget function
+
+add_task(async function testSwitchToTarget() {
+ info("Test TargetCommand.switchToTarget method");
+
+ // 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 commands = await CommandsFactory.forTab(firstTab);
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+ await targetCommand.startListening();
+
+ // 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>`
+ );
+ // We have to spawn a new distinct `commands` object for this new tab,
+ // but we will otherwise consider the first one as the main one.
+ // From this second one, we will only retrieve a new target.
+ const secondCommands = await CommandsFactory.forTab(secondTab, {
+ client: commands.client,
+ });
+ await secondCommands.targetCommand.startListening();
+ const secondTarget = secondCommands.targetCommand.targetFront;
+
+ const frameTargets = [];
+ const firstTarget = targetCommand.targetFront;
+ let currentTarget = targetCommand.targetFront;
+ const onFrameAvailable = ({ targetFront, isTargetSwitching }) => {
+ is(
+ targetFront.targetType,
+ 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,
+ 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 targetCommand.watchTargets({
+ types: [TYPES.FRAME],
+ onAvailable: onFrameAvailable,
+ onDestroyed: 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 targetCommand.switchToTarget(secondTarget);
+
+ is(
+ targetCommand.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"
+ );
+ }
+
+ targetCommand.destroy();
+
+ await commands.destroy();
+ await secondCommands.destroy();
+
+ BrowserTestUtils.removeTab(firstTab);
+ BrowserTestUtils.removeTab(secondTab);
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_tab_workers.js b/devtools/shared/commands/target/tests/browser_target_command_tab_workers.js
new file mode 100644
index 0000000000..24710879ae
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_tab_workers.js
@@ -0,0 +1,322 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API around workers
+
+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 REMOTE_IFRAME_WORKER_URL = URL_ROOT_ORG_SSL + 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);
+
+ // 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 TargetCommand against workers via a tab target");
+ const tab = await addTab(`${FISSION_TEST_URL}?&noServiceWorker`);
+
+ // Create a TargetCommand for the tab
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+
+ // Workaround to allow listening for workers in the content toolbox
+ // without the fission preferences
+ targetCommand.listenForWorkers = true;
+
+ await commands.targetCommand.startListening();
+
+ const { TYPES } = targetCommand;
+
+ info("Check that getAllTargets only returns dedicated workers");
+ const workers = await targetCommand.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 => {
+ return worker.url == `${REMOTE_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 onDestroyed = 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 targetCommand.watchTargets({
+ types: [TYPES.WORKER, TYPES.SHARED_WORKER],
+ onAvailable,
+ onDestroyed,
+ });
+
+ // 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 waitFor(
+ () => targets.length === 4,
+ "Wait for the target list to notify us about the spawned worker"
+ );
+ const mainPageSpawnedWorkerTarget = targets.find(
+ innerTarget => innerTarget.url == `${WORKER_URL}#spawned-worker`
+ );
+ ok(mainPageSpawnedWorkerTarget, "Retrieved spawned worker");
+ const iframeSpawnedWorkerTarget = targets.find(
+ innerTarget =>
+ innerTarget.url == `${REMOTE_IFRAME_WORKER_URL}#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 waitFor(
+ () =>
+ 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;
+ await reloadBrowser();
+
+ await waitFor(() => {
+ 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 waitFor(
+ () => 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 == `${REMOTE_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 waitFor(() => {
+ 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 waitFor(
+ () => 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_URL}#simple-worker-in-created-iframe-1`
+ );
+ const secondSpawnedIframeWorkerTarget = targets.find(
+ worker => worker.url == `${WORKER_URL}#simple-worker-in-created-iframe-2`
+ );
+ const firstSpawnedRemoteIframeWorkerTarget = targets.find(
+ worker =>
+ worker.url ==
+ `${REMOTE_IFRAME_WORKER_URL}#simple-worker-in-created-remote-iframe-1`
+ );
+ const secondSpawnedRemoteIframeWorkerTarget = targets.find(
+ worker =>
+ worker.url ==
+ `${REMOTE_IFRAME_WORKER_URL}#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.loadURIString(
+ tab.linkedBrowser,
+ "data:text/html,<meta charset=utf8>Away"
+ );
+
+ await waitFor(
+ () => destroyedTargets.length === targets.length,
+ "Wait for all the targets to be reported 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"
+ );
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.WORKER, TYPES.SHARED_WORKER],
+ onAvailable,
+ onDestroyed,
+ });
+ targetCommand.destroy();
+
+ info("Unregister service workers so they don't appear in other tests.");
+ await unregisterAllServiceWorkers(commands.client);
+
+ BrowserTestUtils.removeTab(tab);
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_tab_workers_bfcache_navigation.js b/devtools/shared/commands/target/tests/browser_target_command_tab_workers_bfcache_navigation.js
new file mode 100644
index 0000000000..0f1ea45a08
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_tab_workers_bfcache_navigation.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test WORKER targets when doing history navigations (BF Cache)
+//
+// Use a distinct file as this test currently hits a DEBUG assertion
+// https://searchfox.org/mozilla-central/rev/352b525ab841278cd9b3098343f655ef85933544/dom/workers/WorkerPrivate.cpp#5218
+// and so is running only on OPT builds.
+
+const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html";
+const WORKER_FILE = "test_worker.js";
+const WORKER_URL = URL_ROOT_SSL + WORKER_FILE;
+const IFRAME_WORKER_URL = URL_ROOT_ORG_SSL + 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);
+
+ // 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 bfcache navigations");
+ const tab = await addTab(`${FISSION_TEST_URL}?&noServiceWorker`);
+
+ // Create a TargetCommand for the tab
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+
+ // Workaround to allow listening for workers in the content toolbox
+ // without the fission preferences
+ targetCommand.listenForWorkers = true;
+
+ await targetCommand.startListening();
+
+ const { TYPES } = targetCommand;
+
+ info(
+ "Assert that watchTargets will call the onAvailable 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} new targets`);
+ };
+ const onDestroyed = 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 targetCommand.watchTargets({
+ types: [TYPES.WORKER, TYPES.SHARED_WORKER],
+ onAvailable,
+ onDestroyed,
+ });
+
+ is(targets.length, 2, "watchTargets retrieved 2 workers…");
+ const mainPageWorkerTarget = targets.find(
+ worker => worker.url == `${WORKER_URL}#simple-worker`
+ );
+ const iframeWorkerTarget = targets.find(
+ worker => worker.url == `${IFRAME_WORKER_URL}#simple-worker-in-iframe`
+ );
+
+ 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("Check that navigating away does destroy all targets");
+ const onBrowserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ "data:text/html,<meta charset=utf8>Away"
+ );
+ await onBrowserLoaded;
+
+ await waitFor(
+ () => destroyedTargets.length === 2,
+ "Wait for all the targets to be reported as destroyed"
+ );
+
+ info("Navigate back to the first page");
+ gBrowser.goBack();
+
+ await waitFor(
+ () => targets.length === 4,
+ "Wait for the target list to notify us about the first page workers, restored from the BF Cache"
+ );
+
+ const mainPageWorkerTargetAfterGoingBack = targets.find(
+ t => t !== mainPageWorkerTarget && t.url == `${WORKER_URL}#simple-worker`
+ );
+ const iframeWorkerTargetAfterGoingBack = targets.find(
+ t =>
+ t !== iframeWorkerTarget &&
+ t.url == `${IFRAME_WORKER_URL}#simple-worker-in-iframe`
+ );
+
+ ok(
+ mainPageWorkerTargetAfterGoingBack,
+ "The target list handled the worker created from the BF Cache"
+ );
+ ok(
+ iframeWorkerTargetAfterGoingBack,
+ "The target list handled the worker created in the iframe from the BF Cache"
+ );
+
+ targetCommand.destroy();
+ await commands.destroy();
+});
diff --git a/devtools/shared/commands/target/tests/browser_target_command_various_descriptors.js b/devtools/shared/commands/target/tests/browser_target_command_various_descriptors.js
new file mode 100644
index 0000000000..b61faf8f6e
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_various_descriptors.js
@@ -0,0 +1,283 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand API with all possible descriptors
+
+const TEST_URL = "https://example.org/document-builder.sjs?html=org";
+const SECOND_TEST_URL = "https://example.com/document-builder.sjs?html=org";
+const CHROME_WORKER_URL = CHROME_URL_ROOT + "test_worker.js";
+
+const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");
+
+add_task(async function () {
+ // Enabled fission prefs
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ // 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);
+
+ await testLocalTab();
+ await testRemoteTab();
+ await testParentProcess();
+ await testWorker();
+ await testWebExtension();
+});
+
+async function testParentProcess() {
+ info("Test TargetCommand against parent process descriptor");
+
+ const commands = await CommandsFactory.forMainProcess();
+ const { descriptorFront } = commands;
+
+ is(
+ descriptorFront.descriptorType,
+ DESCRIPTOR_TYPES.PROCESS,
+ "The descriptor type is correct"
+ );
+ is(
+ descriptorFront.isParentProcessDescriptor,
+ true,
+ "Descriptor front isParentProcessDescriptor is correct"
+ );
+ is(
+ descriptorFront.isProcessDescriptor,
+ true,
+ "Descriptor front isProcessDescriptor is correct"
+ );
+
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const targets = await targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ ok(
+ targets.length > 1,
+ "We get many targets when debugging the parent process"
+ );
+ const targetFront = targets[0];
+ is(targetFront, targetCommand.targetFront, "The first is the top level one");
+ is(
+ targetFront.targetType,
+ targetCommand.TYPES.FRAME,
+ "the parent process target is of frame type, because it inherits from WindowGlobalTargetActor"
+ );
+ is(targetFront.isTopLevel, true, "This is flagged as top level");
+
+ targetCommand.destroy();
+ await waitForAllTargetsToBeAttached(targetCommand);
+
+ await commands.destroy();
+}
+
+async function testLocalTab() {
+ info("Test TargetCommand against local tab descriptor (via getTab({ tab }))");
+
+ const tab = await addTab(TEST_URL);
+ const commands = await CommandsFactory.forTab(tab);
+ const { descriptorFront } = commands;
+ is(
+ descriptorFront.descriptorType,
+ DESCRIPTOR_TYPES.TAB,
+ "The descriptor type is correct"
+ );
+ is(
+ descriptorFront.isTabDescriptor,
+ true,
+ "Descriptor front isTabDescriptor is correct"
+ );
+
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const targets = await targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ is(targets.length, 1, "Got a unique target");
+ const targetFront = targets[0];
+ is(targetFront, targetCommand.targetFront, "The first is the top level one");
+ is(
+ targetFront.targetType,
+ targetCommand.TYPES.FRAME,
+ "the tab target is of frame type"
+ );
+ is(targetFront.isTopLevel, true, "This is flagged as top level");
+
+ targetCommand.destroy();
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
+
+async function testRemoteTab() {
+ info(
+ "Test TargetCommand against remote tab descriptor (via getTab({ browserId }))"
+ );
+
+ const tab = await addTab(TEST_URL);
+ const commands = await CommandsFactory.forRemoteTab(
+ tab.linkedBrowser.browserId
+ );
+ const { descriptorFront } = commands;
+ is(
+ descriptorFront.descriptorType,
+ DESCRIPTOR_TYPES.TAB,
+ "The descriptor type is correct"
+ );
+ is(
+ descriptorFront.isTabDescriptor,
+ true,
+ "Descriptor front isTabDescriptor is correct"
+ );
+
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const targets = await targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ is(targets.length, 1, "Got a unique target");
+ const targetFront = targets[0];
+ is(
+ targetFront,
+ targetCommand.targetFront,
+ "TargetCommand top target is the same as the first target"
+ );
+ is(
+ targetFront.targetType,
+ targetCommand.TYPES.FRAME,
+ "the tab target is of frame type"
+ );
+ is(targetFront.isTopLevel, true, "This is flagged as top level");
+
+ const browser = tab.linkedBrowser;
+ const onLoaded = BrowserTestUtils.browserLoaded(browser);
+ await BrowserTestUtils.loadURIString(browser, SECOND_TEST_URL);
+ await onLoaded;
+
+ info("Wait for the new target");
+ await waitFor(() => targetCommand.targetFront != targetFront);
+ isnot(
+ targetCommand.targetFront,
+ targetFront,
+ "The top level target changes on navigation"
+ );
+ ok(
+ !targetCommand.targetFront.isDestroyed(),
+ "The new target isn't destroyed"
+ );
+ ok(targetFront.isDestroyed(), "While the previous target is destroyed");
+
+ targetCommand.destroy();
+
+ BrowserTestUtils.removeTab(tab);
+
+ await commands.destroy();
+}
+
+async function testWebExtension() {
+ info("Test TargetCommand against webextension descriptor");
+
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ name: "Sample extension",
+ },
+ });
+
+ await extension.startup();
+
+ const commands = await CommandsFactory.forAddon(extension.id);
+ const { descriptorFront } = commands;
+ is(
+ descriptorFront.descriptorType,
+ DESCRIPTOR_TYPES.EXTENSION,
+ "The descriptor type is correct"
+ );
+ is(
+ descriptorFront.isWebExtensionDescriptor,
+ true,
+ "Descriptor front isWebExtensionDescriptor is correct"
+ );
+
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const targets = await targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ is(targets.length, 1, "Got a unique target");
+ const targetFront = targets[0];
+ is(targetFront, targetCommand.targetFront, "The first is the top level one");
+ is(
+ targetFront.targetType,
+ targetCommand.TYPES.FRAME,
+ "the web extension target is of frame type, because it inherits from WindowGlobalTargetActor"
+ );
+ is(targetFront.isTopLevel, true, "This is flagged as top level");
+
+ targetCommand.destroy();
+
+ await extension.unload();
+
+ await commands.destroy();
+}
+
+// CommandsFactory expect the worker id, which is computed from the nsIWorkerDebugger.id attribute
+function getNextWorkerDebuggerId() {
+ return new Promise(resolve => {
+ const wdm = Cc[
+ "@mozilla.org/dom/workers/workerdebuggermanager;1"
+ ].createInstance(Ci.nsIWorkerDebuggerManager);
+ const listener = {
+ onRegister(dbg) {
+ wdm.removeListener(listener);
+ resolve(dbg.id);
+ },
+ };
+ wdm.addListener(listener);
+ });
+}
+async function testWorker() {
+ info("Test TargetCommand against worker descriptor");
+
+ const workerUrl = CHROME_WORKER_URL + "#descriptor";
+ const onNextWorker = getNextWorkerDebuggerId();
+ const worker = new Worker(workerUrl);
+ const workerId = await onNextWorker;
+ ok(workerId, "Found the worker Debugger ID");
+
+ const commands = await CommandsFactory.forWorker(workerId);
+ const { descriptorFront } = commands;
+ is(
+ descriptorFront.descriptorType,
+ DESCRIPTOR_TYPES.WORKER,
+ "The descriptor type is correct"
+ );
+ is(
+ descriptorFront.isWorkerDescriptor,
+ true,
+ "Descriptor front isWorkerDescriptor is correct"
+ );
+
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const targets = await targetCommand.getAllTargets(targetCommand.ALL_TYPES);
+ is(targets.length, 1, "Got a unique target");
+ const targetFront = targets[0];
+ is(targetFront, targetCommand.targetFront, "The first is the top level one");
+ is(
+ targetFront.targetType,
+ targetCommand.TYPES.WORKER,
+ "the worker target is of worker type"
+ );
+ is(targetFront.isTopLevel, true, "This is flagged as top level");
+
+ targetCommand.destroy();
+
+ // Calling CommandsFactory.forWorker, will call RootFront.getWorker
+ // which will spawn lots of worker legacy code, firing lots of requests,
+ // which may still be pending
+ await commands.waitForRequestsToSettle();
+
+ await commands.destroy();
+ worker.terminate();
+}
diff --git a/devtools/shared/commands/target/tests/browser_target_command_watchTargets.js b/devtools/shared/commands/target/tests/browser_target_command_watchTargets.js
new file mode 100644
index 0000000000..516780be01
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_watchTargets.js
@@ -0,0 +1,214 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the TargetCommand's `watchTargets` function
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
+
+add_task(async function () {
+ // Enabled fission's pref as the TargetCommand is almost disabled without it
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ // 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);
+
+ await testWatchTargets();
+ await testThrowingInOnAvailable();
+});
+
+async function testWatchTargets() {
+ info("Test TargetCommand watchTargets function");
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.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 topLevelTarget = targetCommand.targetFront;
+ const onAvailable = ({ targetFront }) => {
+ if (targets.has(targetFront)) {
+ ok(false, "The same target is notified multiple times via onAvailable");
+ }
+ is(
+ targetFront.targetType,
+ TYPES.PROCESS,
+ "We are only notified about process targets"
+ );
+ ok(
+ targetFront == topLevelTarget
+ ? 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,
+ 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 targetCommand.watchTargets({
+ types: [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;
+ }
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable2,
+ });
+ resolve(targetFront);
+ };
+ targetCommand.watchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: 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);
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable3,
+ onDestroyed: onDestroyed3,
+ });
+ };
+ targetCommand.watchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable: onAvailable3,
+ onDestroyed: 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"
+ );
+
+ targetCommand.unwatchTargets({
+ types: [TYPES.PROCESS],
+ onAvailable,
+ onDestroyed,
+ });
+
+ targetCommand.destroy();
+
+ await commands.destroy();
+}
+
+async function testThrowingInOnAvailable() {
+ info(
+ "Test TargetCommand watchTargets function when an exception is thrown in onAvailable callback"
+ );
+
+ const commands = await CommandsFactory.forMainProcess();
+ const targetCommand = commands.targetCommand;
+ const { TYPES } = targetCommand;
+
+ await targetCommand.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 targetCommand.watchTargets({ types: [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."
+ );
+
+ targetCommand.destroy();
+
+ await commands.destroy();
+}
diff --git a/devtools/shared/commands/target/tests/browser_watcher_actor_getter_caching.js b/devtools/shared/commands/target/tests/browser_watcher_actor_getter_caching.js
new file mode 100644
index 0000000000..6dd99d243b
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_watcher_actor_getter_caching.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that watcher front/actor APIs do not lead to create duplicate actors.
+
+const TEST_URL = "data:text/html;charset=utf-8,Actor caching test";
+
+add_task(async function () {
+ info("Setup the test page with workers of all types");
+ const tab = await addTab(TEST_URL);
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const { watcherFront } = targetCommand;
+ ok(watcherFront, "A watcherFront is available on targetCommand");
+
+ info("Check that getNetworkParentActor does not create duplicate actors");
+ testActorGetter(
+ watcherFront,
+ () => watcherFront.getNetworkParentActor(),
+ "networkParent"
+ );
+
+ info("Check that getBreakpointListActor does not create duplicate actors");
+ testActorGetter(
+ watcherFront,
+ () => watcherFront.getBreakpointListActor(),
+ "breakpoint-list"
+ );
+
+ info(
+ "Check that getTargetConfigurationActor does not create duplicate actors"
+ );
+ testActorGetter(
+ watcherFront,
+ () => watcherFront.getTargetConfigurationActor(),
+ "target-configuration"
+ );
+
+ info(
+ "Check that getThreadConfigurationActor does not create duplicate actors"
+ );
+ testActorGetter(
+ watcherFront,
+ () => watcherFront.getThreadConfigurationActor(),
+ "thread-configuration"
+ );
+
+ targetCommand.destroy();
+ await commands.waitForRequestsToSettle();
+ await commands.destroy();
+});
+
+/**
+ * Check that calling an actor getter method on the watcher front leads to the
+ * creation of at most 1 actor.
+ */
+async function testActorGetter(watcherFront, actorGetterFn, typeName) {
+ checkPoolChildrenSize(watcherFront, typeName, 0);
+
+ const actor = await actorGetterFn();
+ checkPoolChildrenSize(watcherFront, typeName, 1);
+
+ const otherActor = await actorGetterFn();
+ is(actor, otherActor, "Returned the same actor for " + typeName);
+
+ checkPoolChildrenSize(watcherFront, typeName, 1);
+}
+
+/**
+ * Assert that a given parent pool has the expected number of children for
+ * a given typeName.
+ */
+function checkPoolChildrenSize(parentPool, typeName, expected) {
+ const children = [...parentPool.poolChildren()];
+ const childrenByType = children.filter(pool => pool.typeName === typeName);
+ is(
+ childrenByType.length,
+ expected,
+ `${parentPool.actorID} should have ${expected} children of type ${typeName}`
+ );
+}
diff --git a/devtools/shared/commands/target/tests/fission_document.html b/devtools/shared/commands/target/tests/fission_document.html
new file mode 100644
index 0000000000..62afe347e3
--- /dev/null
+++ b/devtools/shared/commands/target/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/commands/target/tests/test_worker.js#simple-worker");
+
+ // eslint-disable-next-line no-unused-vars
+ const sharedWorker = new SharedWorker("https://example.com/browser/devtools/shared/commands/target/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/commands/target/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/commands/target/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/commands/target/tests/fission_iframe.html b/devtools/shared/commands/target/tests/fission_iframe.html
new file mode 100644
index 0000000000..deae49f833
--- /dev/null
+++ b/devtools/shared/commands/target/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/commands/target/tests/head.js b/devtools/shared/commands/target/tests/head.js
new file mode 100644
index 0000000000..ecb3fc1828
--- /dev/null
+++ b/devtools/shared/commands/target/tests/head.js
@@ -0,0 +1,32 @@
+/* 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"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+
+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;
+}
diff --git a/devtools/shared/commands/target/tests/incremental-js-value-script.sjs b/devtools/shared/commands/target/tests/incremental-js-value-script.sjs
new file mode 100644
index 0000000000..a612a3cb59
--- /dev/null
+++ b/devtools/shared/commands/target/tests/incremental-js-value-script.sjs
@@ -0,0 +1,23 @@
+"use strict";
+
+function handleRequest(request, response) {
+ const Etag = '"4d881ab-b03-435f0a0f9ef00"';
+ const IfNoneMatch = request.hasHeader("If-None-Match")
+ ? request.getHeader("If-None-Match")
+ : "";
+
+ const counter = getState("cache-counter") || 1;
+ const page = "<script>var jsValue = '" + counter + "';</script>" + counter;
+
+ setState("cache-counter", "" + (parseInt(counter, 10) + 1));
+
+ response.setHeader("Etag", Etag, false);
+
+ if (IfNoneMatch === Etag) {
+ response.setStatusLine(request.httpVersion, "304", "Not Modified");
+ } else {
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.write(page);
+ }
+}
diff --git a/devtools/shared/commands/target/tests/simple_document.html b/devtools/shared/commands/target/tests/simple_document.html
new file mode 100644
index 0000000000..d6a449e489
--- /dev/null
+++ b/devtools/shared/commands/target/tests/simple_document.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf8">
+ <title>Test empty document</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test empty document</p>
+</body>
+</html>
diff --git a/devtools/shared/commands/target/tests/test_service_worker.js b/devtools/shared/commands/target/tests/test_service_worker.js
new file mode 100644
index 0000000000..aabc3fda0f
--- /dev/null
+++ b/devtools/shared/commands/target/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/commands/target/tests/test_sw_page.html b/devtools/shared/commands/target/tests/test_sw_page.html
new file mode 100644
index 0000000000..38aad04259
--- /dev/null
+++ b/devtools/shared/commands/target/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/commands/target/tests/test_sw_page_worker.js b/devtools/shared/commands/target/tests/test_sw_page_worker.js
new file mode 100644
index 0000000000..29cda68560
--- /dev/null
+++ b/devtools/shared/commands/target/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/commands/target/tests/test_worker.js b/devtools/shared/commands/target/tests/test_worker.js
new file mode 100644
index 0000000000..ce3dd39cea
--- /dev/null
+++ b/devtools/shared/commands/target/tests/test_worker.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+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);
+ }
+};