diff options
Diffstat (limited to 'dom/tests/browser/browser_hasbeforeunload.js')
-rw-r--r-- | dom/tests/browser/browser_hasbeforeunload.js | 875 |
1 files changed, 875 insertions, 0 deletions
diff --git a/dom/tests/browser/browser_hasbeforeunload.js b/dom/tests/browser/browser_hasbeforeunload.js new file mode 100644 index 0000000000..de86a1ea7a --- /dev/null +++ b/dom/tests/browser/browser_hasbeforeunload.js @@ -0,0 +1,875 @@ +"use strict"; + +const PAGE_URL = + "http://example.com/browser/dom/tests/browser/beforeunload_test_page.html"; + +/** + * Adds 1 or more inert beforeunload event listeners in this browser. + * By default, will target the top-level content window, but callers + * can specify the index of a subframe to target. See prepareSubframes + * for an idea of how the subframes are structured. + * + * @param {<xul:browser>} browser + * The browser to add the beforeunload event listener in. + * @param {int} howMany + * How many beforeunload event listeners to add. Note that these + * beforeunload event listeners are inert and will not actually + * prevent the host window from navigating. + * @param {optional int} frameDepth + * The depth of the frame to add the event listener to. Defaults + * to 0, which is the top-level content window. + * @return {Promise} + */ +function addBeforeUnloadListeners(browser, howMany = 1, frameDepth = 0) { + return controlFrameAt(browser, frameDepth, { + name: "AddBeforeUnload", + howMany, + }); +} + +/** + * Adds 1 or more inert beforeunload event listeners in this browser on + * a particular subframe. By default, this will target the first subframe + * under the top-level content window, but callers can specify the index + * of a subframe to target. See prepareSubframes for an idea of how the + * subframes are structured. + * + * Note that this adds the beforeunload event listener on the "outer" window, + * by doing: + * + * iframe.addEventListener("beforeunload", ...); + * + * @param {<xul:browser>} browser + * The browser to add the beforeunload event listener in. + * @param {int} howMany + * How many beforeunload event listeners to add. Note that these + * beforeunload event listeners are inert and will not actually + * prevent the host window from navigating. + * @param {optional int} frameDepth + * The depth of the frame to add the event listener to. Defaults + * to 1, which is the first subframe inside the top-level content + * window. Setting this to 0 will throw. + * @return {Promise} + */ +function addOuterBeforeUnloadListeners(browser, howMany = 1, frameDepth = 1) { + if (frameDepth == 0) { + throw new Error( + "When adding a beforeunload listener on an outer " + + "window, the frame you're targeting needs to be at " + + "depth > 0." + ); + } + + return controlFrameAt(browser, frameDepth, { + name: "AddOuterBeforeUnload", + howMany, + }); +} + +/** + * Removes 1 or more inert beforeunload event listeners in this browser. + * This assumes that addBeforeUnloadListeners has been called previously + * for the target frame. + * + * By default, will target the top-level content window, but callers + * can specify the index of a subframe to target. See prepareSubframes + * for an idea of how the subframes are structured. + * + * @param {<xul:browser>} browser + * The browser to remove the beforeunload event listener from. + * @param {int} howMany + * How many beforeunload event listeners to remove. + * @param {optional int} frameDepth + * The depth of the frame to remove the event listener from. Defaults + * to 0, which is the top-level content window. + * @return {Promise} + */ +function removeBeforeUnloadListeners(browser, howMany = 1, frameDepth = 0) { + return controlFrameAt(browser, frameDepth, { + name: "RemoveBeforeUnload", + howMany, + }); +} + +/** + * Removes 1 or more inert beforeunload event listeners in this browser on + * a particular subframe. By default, this will target the first subframe + * under the top-level content window, but callers can specify the index + * of a subframe to target. See prepareSubframes for an idea of how the + * subframes are structured. + * + * Note that this removes the beforeunload event listener on the "outer" window, + * by doing: + * + * iframe.removeEventListener("beforeunload", ...); + * + * @param {<xul:browser>} browser + * The browser to remove the beforeunload event listener from. + * @param {int} howMany + * How many beforeunload event listeners to remove. + * @param {optional int} frameDepth + * The depth of the frame to remove the event listener from. Defaults + * to 1, which is the first subframe inside the top-level content + * window. Setting this to 0 will throw. + * @return {Promise} + */ +function removeOuterBeforeUnloadListeners( + browser, + howMany = 1, + frameDepth = 1 +) { + if (frameDepth == 0) { + throw new Error( + "When removing a beforeunload listener from an outer " + + "window, the frame you're targeting needs to be at " + + "depth > 0." + ); + } + + return controlFrameAt(browser, frameDepth, { + name: "RemoveOuterBeforeUnload", + howMany, + }); +} + +/** + * Navigates a content window to a particular URL and waits for it to + * finish loading that URL. + * + * By default, will target the top-level content window, but callers + * can specify the index of a subframe to target. See prepareSubframes + * for an idea of how the subframes are structured. + * + * @param {<xul:browser>} browser + * The browser that will have the navigation occur within it. + * @param {string} url + * The URL to send the content window to. + * @param {optional int} frameDepth + * The depth of the frame to navigate. Defaults to 0, which is + * the top-level content window. + * @return {Promise} + */ +function navigateSubframe(browser, url, frameDepth = 0) { + let navigatePromise = controlFrameAt(browser, frameDepth, { + name: "Navigate", + url, + }); + let subframeLoad = BrowserTestUtils.browserLoaded(browser, true); + return Promise.all([navigatePromise, subframeLoad]); +} + +/** + * Removes the <iframe> from a content window pointed at PAGE_URL. + * + * By default, will target the top-level content window, but callers + * can specify the index of a subframe to target. See prepareSubframes + * for an idea of how the subframes are structured. + * + * @param {<xul:browser>} browser + * The browser that will have removal occur within it. + * @param {optional int} frameDepth + * The depth of the frame that will have the removal occur within + * it. Defaults to 0, which is the top-level content window, meaning + * that the first subframe will be removed. + * @return {Promise} + */ +function removeSubframeFrom(browser, frameDepth = 0) { + return controlFrameAt(browser, frameDepth, { + name: "RemoveSubframe", + }); +} + +/** + * Sends a command to a frame pointed at PAGE_URL. There are utility + * functions defined in this file that call this function. You should + * use those instead. + * + * @param {<xul:browser>} browser + * The browser to send the command to. + * @param {int} frameDepth + * The depth of the frame that we'll send the command to. 0 means + * sending it to the top-level content window. + * @param {object} command + * An object with the following structure: + * + * { + * name: (string), + * <arbitrary arguments to send with the command> + * } + * + * Here are the commands that can be sent: + * + * AddBeforeUnload + * {int} howMany + * How many beforeunload event listeners to add. + * + * AddOuterBeforeUnload + * {int} howMany + * How many beforeunload event listeners to add to + * the iframe in the document at this depth. + * + * RemoveBeforeUnload + * {int} howMany + * How many beforeunload event listeners to remove. + * + * RemoveOuterBeforeUnload + * {int} howMany + * How many beforeunload event listeners to remove from + * the iframe in the document at this depth. + * + * Navigate + * {string} url + * The URL to send the frame to. + * + * RemoveSubframe + * + * @return {Promise} + */ +function controlFrameAt(browser, frameDepth, command) { + return SpecialPowers.spawn( + browser, + [{ frameDepth, command }], + async function (args) { + const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" + ); + + let { command: contentCommand, frameDepth: contentFrameDepth } = args; + + let targetContent = content; + let targetSubframe = content.document.getElementById("subframe"); + + // We want to not only find the frame that maps to the + // target frame depth that we've been given, but we also want + // to count the total depth so that if a middle frame is removed + // or navigated, then we know how many outer-window-destroyed + // observer notifications to expect. + let currentContent = targetContent; + let currentSubframe = targetSubframe; + + let depth = 0; + + do { + currentContent = currentSubframe.contentWindow; + currentSubframe = currentContent.document.getElementById("subframe"); + depth++; + if (depth == contentFrameDepth) { + targetContent = currentContent; + targetSubframe = currentSubframe; + } + } while (currentSubframe); + + switch (contentCommand.name) { + case "AddBeforeUnload": { + let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader; + Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page."); + BeforeUnloader.pushInner(contentCommand.howMany); + break; + } + case "AddOuterBeforeUnload": { + let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader; + Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page."); + BeforeUnloader.pushOuter(contentCommand.howMany); + break; + } + case "RemoveBeforeUnload": { + let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader; + Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page."); + BeforeUnloader.popInner(contentCommand.howMany); + break; + } + case "RemoveOuterBeforeUnload": { + let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader; + Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page."); + BeforeUnloader.popOuter(contentCommand.howMany); + break; + } + case "Navigate": { + // How many frames are going to be destroyed when we do this? We + // need to wait for that many window destroyed notifications. + targetContent.location = contentCommand.url; + + let destroyedOuterWindows = depth - contentFrameDepth; + if (destroyedOuterWindows) { + await TestUtils.topicObserved("outer-window-destroyed", () => { + destroyedOuterWindows--; + return !destroyedOuterWindows; + }); + } + break; + } + case "RemoveSubframe": { + let subframe = targetContent.document.getElementById("subframe"); + Assert.ok( + subframe, + "Found subframe at frame depth of " + contentFrameDepth + ); + subframe.remove(); + + let destroyedOuterWindows = depth - contentFrameDepth; + if (destroyedOuterWindows) { + await TestUtils.topicObserved("outer-window-destroyed", () => { + destroyedOuterWindows--; + return !destroyedOuterWindows; + }); + } + break; + } + } + } + ).catch(console.error); +} + +/** + * Sets up a structure where a page at PAGE_URL will host an + * <iframe> also pointed at PAGE_URL, and does this repeatedly + * until we've achieved the desired frame depth. Note that this + * will cause the top-level browser to reload, and wipe out any + * previous changes to the DOM under it. + * + * @param {<xul:browser>} browser + * The browser in which we'll load our structure at the + * top level. + * @param {Array<object>} options + * Set-up options for each subframe. The following properties + * are accepted: + * + * {string} sandboxAttributes + * The value to set the sandbox attribute to. If null, no sandbox + * attribute will be set (and any pre-existing sandbox attributes) + * on the <iframe> will be removed. + * + * The number of entries on the options Array corresponds to how many + * subframes are under the top-level content window. + * + * Example: + * + * yield prepareSubframes(browser, [ + * { sandboxAttributes: null }, + * { sandboxAttributes: "allow-modals" }, + * ]); + * + * This would create the following structure: + * + * <top-level content window at PAGE_URL> + * | + * |--> <iframe at PAGE_URL, no sandbox attributes> + * | + * |--> <iframe at PAGE_URL, sandbox="allow-modals"> + * + * @return {Promise} + */ +async function prepareSubframes(browser, options) { + browser.reload(); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [{ options, PAGE_URL }], + async function (args) { + let { options: allSubframeOptions, PAGE_URL: contentPageURL } = args; + function loadBeforeUnloadHelper(doc, subframeOptions) { + let subframe = doc.getElementById("subframe"); + subframe.remove(); + if (subframeOptions.sandboxAttributes === null) { + subframe.removeAttribute("sandbox"); + } else { + subframe.setAttribute("sandbox", subframeOptions.sandboxAttributes); + } + doc.body.appendChild(subframe); + subframe.contentWindow.location = contentPageURL; + return ContentTaskUtils.waitForEvent(subframe, "load").then(() => { + return subframe.contentDocument; + }); + } + + let currentDoc = content.document; + for (let subframeOptions of allSubframeOptions) { + currentDoc = await loadBeforeUnloadHelper(currentDoc, subframeOptions); + } + } + ); +} + +/** + * Ensures that a browser's nsIRemoteTab hasBeforeUnload attribute + * is set to the expected value. + * + * @param {<xul:browser>} browser + * The browser whose nsIRemoteTab we will check. + * @param {bool} expected + * True if hasBeforeUnload is expected to be true. + */ +function assertHasBeforeUnload(browser, expected) { + Assert.equal(browser.hasBeforeUnload, expected); +} + +/** + * Tests that the MozBrowser hasBeforeUnload property works under + * a number of different scenarios on inner windows. At a high-level, + * we test that hasBeforeUnload works properly during page / iframe + * navigation, or when an <iframe> with a beforeunload listener on its + * inner window is removed from the DOM. + */ +add_task(async function test_inner_window_scenarios() { + // Turn this off because the test expects the page to be not bfcached. + await SpecialPowers.pushPrefEnv({ + set: [ + ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", false], + ], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE_URL, + }, + async function (browser) { + Assert.ok( + browser.isRemoteBrowser, + "This test only makes sense with out of process browsers." + ); + assertHasBeforeUnload(browser, false); + + // Test the simple case on the top-level window by adding a single + // beforeunload event listener on the inner window and then removing + // it. + await addBeforeUnloadListeners(browser); + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser); + assertHasBeforeUnload(browser, false); + + // Now let's add several beforeunload listeners, and + // ensure that we only set hasBeforeUnload to false once + // the last listener is removed. + await addBeforeUnloadListeners(browser, 3); + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser); // 2 left... + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser); // 1 left... + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser); // None left! + + assertHasBeforeUnload(browser, false); + + // Now let's have the top-level content window navigate + // away with a beforeunload listener set, and ensure + // that we clear the hasBeforeUnload value. + await addBeforeUnloadListeners(browser, 5); + await navigateSubframe(browser, "http://example.com"); + assertHasBeforeUnload(browser, false); + + // Now send the page back to the test page for + // the next few tests. + BrowserTestUtils.startLoadingURIString(browser, PAGE_URL); + await BrowserTestUtils.browserLoaded(browser); + + // We want to test hasBeforeUnload works properly with + // beforeunload event listeners in <iframe> elements too. + // We prepare a structure like this with 3 content windows + // to exercise: + // + // <top-level content window at PAGE_URL> (TOP) + // | + // |--> <iframe at PAGE_URL> (MIDDLE) + // | + // |--> <iframe at PAGE_URL> (BOTTOM) + // + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + // These constants are just to make it easier to know which + // frame we're referring to without having to remember the + // exact indices. + const TOP = 0; + const MIDDLE = 1; + const BOTTOM = 2; + + // We should initially start with hasBeforeUnload set to false. + assertHasBeforeUnload(browser, false); + + // Tests that if there are beforeunload event listeners on + // all levels of our window structure, that we only set + // hasBeforeUnload to false once the last beforeunload + // listener has been unset. + await addBeforeUnloadListeners(browser, 2, MIDDLE); + assertHasBeforeUnload(browser, true); + await addBeforeUnloadListeners(browser, 1, TOP); + assertHasBeforeUnload(browser, true); + await addBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeBeforeUnloadListeners(browser, 1, TOP); + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser, 2, MIDDLE); + assertHasBeforeUnload(browser, false); + + // Tests that if a beforeunload event listener is set on + // an iframe that navigates away to a page without a + // beforeunload listener, that hasBeforeUnload is set + // to false. + await addBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + + await navigateSubframe(browser, "http://example.com", BOTTOM); + assertHasBeforeUnload(browser, false); + + // Reset our window structure now. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + + // This time, add beforeunload event listeners to both the + // MIDDLE and BOTTOM frame, and then navigate the MIDDLE + // away. This should set hasBeforeUnload to false. + await addBeforeUnloadListeners(browser, 3, MIDDLE); + await addBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await navigateSubframe(browser, "http://example.com", MIDDLE); + assertHasBeforeUnload(browser, false); + + // Tests that if the MIDDLE and BOTTOM frames have beforeunload + // event listeners, and if we remove the BOTTOM <iframe> and the + // MIDDLE <iframe>, that hasBeforeUnload is set to false. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + await addBeforeUnloadListeners(browser, 3, MIDDLE); + await addBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, MIDDLE); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, TOP); + assertHasBeforeUnload(browser, false); + + // Tests that if the MIDDLE and BOTTOM frames have beforeunload + // event listeners, and if we remove just the MIDDLE <iframe>, that + // hasBeforeUnload is set to false. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + await addBeforeUnloadListeners(browser, 3, MIDDLE); + await addBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, TOP); + assertHasBeforeUnload(browser, false); + + // Test that two sandboxed iframes, _without_ the allow-modals + // permission, do not result in the hasBeforeUnload attribute + // being set to true when beforeunload event listeners are added. + await prepareSubframes(browser, [ + { sandboxAttributes: "allow-scripts" }, + { sandboxAttributes: "allow-scripts" }, + ]); + + await addBeforeUnloadListeners(browser, 3, MIDDLE); + await addBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, false); + + await removeBeforeUnloadListeners(browser, 3, MIDDLE); + await removeBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, false); + + // Test that two sandboxed iframes, both with the allow-modals + // permission, cause the hasBeforeUnload attribute to be set + // to true when beforeunload event listeners are added. + await prepareSubframes(browser, [ + { sandboxAttributes: "allow-scripts allow-modals" }, + { sandboxAttributes: "allow-scripts allow-modals" }, + ]); + + await addBeforeUnloadListeners(browser, 3, MIDDLE); + await addBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, false); + } + ); +}); + +/** + * Tests that the nsIRemoteTab hasBeforeUnload attribute works under + * a number of different scenarios on outer windows. Very similar to + * the above set of tests, except that we add the beforeunload listeners + * to the iframe DOM nodes instead of the inner windows. + */ +add_task(async function test_outer_window_scenarios() { + // Turn this off because the test expects the page to be not bfcached. + await SpecialPowers.pushPrefEnv({ + set: [ + ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", false], + ], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE_URL, + }, + async function (browser) { + Assert.ok( + browser.isRemoteBrowser, + "This test only makes sense with out of process browsers." + ); + assertHasBeforeUnload(browser, false); + + // We want to test hasBeforeUnload works properly with + // beforeunload event listeners in <iframe> elements. + // We prepare a structure like this with 3 content windows + // to exercise: + // + // <top-level content window at PAGE_URL> (TOP) + // | + // |--> <iframe at PAGE_URL> (MIDDLE) + // | + // |--> <iframe at PAGE_URL> (BOTTOM) + // + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + + // These constants are just to make it easier to know which + // frame we're referring to without having to remember the + // exact indices. + const TOP = 0; + const MIDDLE = 1; + const BOTTOM = 2; + + // Test the simple case on the top-level window by adding a single + // beforeunload event listener on the outer window of the iframe + // in the TOP document. + await addOuterBeforeUnloadListeners(browser); + assertHasBeforeUnload(browser, true); + + await removeOuterBeforeUnloadListeners(browser); + assertHasBeforeUnload(browser, false); + + // Now let's add several beforeunload listeners, and + // ensure that we only set hasBeforeUnload to false once + // the last listener is removed. + await addOuterBeforeUnloadListeners(browser, 3); + assertHasBeforeUnload(browser, true); + await removeOuterBeforeUnloadListeners(browser); // 2 left... + assertHasBeforeUnload(browser, true); + await removeOuterBeforeUnloadListeners(browser); // 1 left... + assertHasBeforeUnload(browser, true); + await removeOuterBeforeUnloadListeners(browser); // None left! + + assertHasBeforeUnload(browser, false); + + // Now let's have the top-level content window navigate away + // with a beforeunload listener set on the outer window of the + // iframe inside it, and ensure that we clear the hasBeforeUnload + // value. + await addOuterBeforeUnloadListeners(browser, 5); + await navigateSubframe(browser, "http://example.com", TOP); + assertHasBeforeUnload(browser, false); + + // Now send the page back to the test page for + // the next few tests. + BrowserTestUtils.startLoadingURIString(browser, PAGE_URL); + await BrowserTestUtils.browserLoaded(browser); + + // We should initially start with hasBeforeUnload set to false. + assertHasBeforeUnload(browser, false); + + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + + // Tests that if there are beforeunload event listeners on + // all levels of our window structure, that we only set + // hasBeforeUnload to false once the last beforeunload + // listener has been unset. + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, true); + await addOuterBeforeUnloadListeners(browser, 7, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeOuterBeforeUnloadListeners(browser, 7, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, false); + + // Tests that if a beforeunload event listener is set on + // an iframe that navigates away to a page without a + // beforeunload listener, that hasBeforeUnload is set + // to false. We're setting the event listener on the + // outer window on the <iframe> in the MIDDLE, which + // itself contains the BOTTOM frame it our structure. + await addOuterBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + + // Now navigate that BOTTOM frame. + await navigateSubframe(browser, "http://example.com", BOTTOM); + assertHasBeforeUnload(browser, false); + + // Reset our window structure now. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + + // This time, add beforeunload event listeners to the outer + // windows for MIDDLE and BOTTOM. Then navigate the MIDDLE + // frame. This should set hasBeforeUnload to false. + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await addOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await navigateSubframe(browser, "http://example.com", MIDDLE); + assertHasBeforeUnload(browser, false); + + // Adds beforeunload event listeners to the outer windows of + // MIDDLE and BOTOTM, and then removes those iframes. Removing + // both iframes should set hasBeforeUnload to false. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await addOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, MIDDLE); + assertHasBeforeUnload(browser, false); + + // Adds beforeunload event listeners to the outer windows of MIDDLE + // and BOTTOM, and then removes just the MIDDLE iframe (which will + // take the bottom one with it). This should set hasBeforeUnload to + // false. + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await addOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeSubframeFrom(browser, TOP); + assertHasBeforeUnload(browser, false); + + // Test that two sandboxed iframes, _without_ the allow-modals + // permission, do not result in the hasBeforeUnload attribute + // being set to true when beforeunload event listeners are added + // to the outer windows. Note that this requires the + // allow-same-origin permission, otherwise a cross-origin + // security exception is thrown. + await prepareSubframes(browser, [ + { sandboxAttributes: "allow-same-origin allow-scripts" }, + { sandboxAttributes: "allow-same-origin allow-scripts" }, + ]); + + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await addOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, false); + + await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await removeOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, false); + + // Test that two sandboxed iframes, both with the allow-modals + // permission, cause the hasBeforeUnload attribute to be set + // to true when beforeunload event listeners are added. Note + // that this requires the allow-same-origin permission, + // otherwise a cross-origin security exception is thrown. + await prepareSubframes(browser, [ + { sandboxAttributes: "allow-same-origin allow-scripts allow-modals" }, + { sandboxAttributes: "allow-same-origin allow-scripts allow-modals" }, + ]); + + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + await addOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeOuterBeforeUnloadListeners(browser, 1, BOTTOM); + assertHasBeforeUnload(browser, true); + await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, false); + } + ); +}); + +/** + * Tests hasBeforeUnload behaviour when beforeunload event listeners + * are added on both inner and outer windows. + */ +add_task(async function test_mixed_inner_and_outer_window_scenarios() { + // Turn this off because the test expects the page to be not bfcached. + await SpecialPowers.pushPrefEnv({ + set: [ + ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", false], + ], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE_URL, + }, + async function (browser) { + Assert.ok( + browser.isRemoteBrowser, + "This test only makes sense with out of process browsers." + ); + assertHasBeforeUnload(browser, false); + + // We want to test hasBeforeUnload works properly with + // beforeunload event listeners in <iframe> elements. + // We prepare a structure like this with 3 content windows + // to exercise: + // + // <top-level content window at PAGE_URL> (TOP) + // | + // |--> <iframe at PAGE_URL> (MIDDLE) + // | + // |--> <iframe at PAGE_URL> (BOTTOM) + // + await prepareSubframes(browser, [ + { sandboxAttributes: null }, + { sandboxAttributes: null }, + ]); + + // These constants are just to make it easier to know which + // frame we're referring to without having to remember the + // exact indices. + const TOP = 0; + const MIDDLE = 1; + const BOTTOM = 2; + + await addBeforeUnloadListeners(browser, 1, TOP); + assertHasBeforeUnload(browser, true); + await addBeforeUnloadListeners(browser, 2, MIDDLE); + assertHasBeforeUnload(browser, true); + await addBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + + await addOuterBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, true); + await addOuterBeforeUnloadListeners(browser, 7, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeBeforeUnloadListeners(browser, 5, BOTTOM); + assertHasBeforeUnload(browser, true); + + await removeBeforeUnloadListeners(browser, 2, MIDDLE); + assertHasBeforeUnload(browser, true); + + await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE); + assertHasBeforeUnload(browser, true); + + await removeBeforeUnloadListeners(browser, 1, TOP); + assertHasBeforeUnload(browser, true); + + await removeOuterBeforeUnloadListeners(browser, 7, BOTTOM); + assertHasBeforeUnload(browser, false); + } + ); +}); |