summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/target/tests/browser_target_command_bfcache.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/commands/target/tests/browser_target_command_bfcache.js')
-rw-r--r--devtools/shared/commands/target/tests/browser_target_command_bfcache.js505
1 files changed, 505 insertions, 0 deletions
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..598e9c550b
--- /dev/null
+++ b/devtools/shared/commands/target/tests/browser_target_command_bfcache.js
@@ -0,0 +1,505 @@
+/* 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.startLoadingURIString(
+ 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.startLoadingURIString(
+ 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();
+}