diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /devtools/shared/commands/target/tests | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream/115.8.0esr.tar.xz firefox-esr-upstream/115.8.0esr.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/commands/target/tests')
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); + } +}; |