tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_hasbeforeunload.js (33212B)


      1 "use strict";
      2 
      3 const PAGE_URL =
      4  "http://example.com/browser/dom/tests/browser/beforeunload_test_page.html";
      5 
      6 /**
      7 * Adds 1 or more inert beforeunload event listeners in this browser.
      8 * By default, will target the top-level content window, but callers
      9 * can specify the index of a subframe to target. See prepareSubframes
     10 * for an idea of how the subframes are structured.
     11 *
     12 * @param {<xul:browser>} browser
     13 *        The browser to add the beforeunload event listener in.
     14 * @param {int} howMany
     15 *        How many beforeunload event listeners to add. Note that these
     16 *        beforeunload event listeners are inert and will not actually
     17 *        prevent the host window from navigating.
     18 * @param {optional int} frameDepth
     19 *        The depth of the frame to add the event listener to. Defaults
     20 *        to 0, which is the top-level content window.
     21 * @return {Promise}
     22 */
     23 function addBeforeUnloadListeners(browser, howMany = 1, frameDepth = 0) {
     24  return controlFrameAt(browser, frameDepth, {
     25    name: "AddBeforeUnload",
     26    howMany,
     27  });
     28 }
     29 
     30 /**
     31 * Adds 1 or more inert beforeunload event listeners in this browser on
     32 * a particular subframe. By default, this will target the first subframe
     33 * under the top-level content window, but callers can specify the index
     34 * of a subframe to target. See prepareSubframes for an idea of how the
     35 * subframes are structured.
     36 *
     37 * Note that this adds the beforeunload event listener on the "outer" window,
     38 * by doing:
     39 *
     40 * iframe.addEventListener("beforeunload", ...);
     41 *
     42 * @param {<xul:browser>} browser
     43 *        The browser to add the beforeunload event listener in.
     44 * @param {int} howMany
     45 *        How many beforeunload event listeners to add. Note that these
     46 *        beforeunload event listeners are inert and will not actually
     47 *        prevent the host window from navigating.
     48 * @param {optional int} frameDepth
     49 *        The depth of the frame to add the event listener to. Defaults
     50 *        to 1, which is the first subframe inside the top-level content
     51 *        window. Setting this to 0 will throw.
     52 * @return {Promise}
     53 */
     54 function addOuterBeforeUnloadListeners(browser, howMany = 1, frameDepth = 1) {
     55  if (frameDepth == 0) {
     56    throw new Error(
     57      "When adding a beforeunload listener on an outer " +
     58        "window, the frame you're targeting needs to be at " +
     59        "depth > 0."
     60    );
     61  }
     62 
     63  return controlFrameAt(browser, frameDepth, {
     64    name: "AddOuterBeforeUnload",
     65    howMany,
     66  });
     67 }
     68 
     69 /**
     70 * Removes 1 or more inert beforeunload event listeners in this browser.
     71 * This assumes that addBeforeUnloadListeners has been called previously
     72 * for the target frame.
     73 *
     74 * By default, will target the top-level content window, but callers
     75 * can specify the index of a subframe to target. See prepareSubframes
     76 * for an idea of how the subframes are structured.
     77 *
     78 * @param {<xul:browser>} browser
     79 *        The browser to remove the beforeunload event listener from.
     80 * @param {int} howMany
     81 *        How many beforeunload event listeners to remove.
     82 * @param {optional int} frameDepth
     83 *        The depth of the frame to remove the event listener from. Defaults
     84 *        to 0, which is the top-level content window.
     85 * @return {Promise}
     86 */
     87 function removeBeforeUnloadListeners(browser, howMany = 1, frameDepth = 0) {
     88  return controlFrameAt(browser, frameDepth, {
     89    name: "RemoveBeforeUnload",
     90    howMany,
     91  });
     92 }
     93 
     94 /**
     95 * Removes 1 or more inert beforeunload event listeners in this browser on
     96 * a particular subframe. By default, this will target the first subframe
     97 * under the top-level content window, but callers can specify the index
     98 * of a subframe to target. See prepareSubframes for an idea of how the
     99 * subframes are structured.
    100 *
    101 * Note that this removes the beforeunload event listener on the "outer" window,
    102 * by doing:
    103 *
    104 * iframe.removeEventListener("beforeunload", ...);
    105 *
    106 * @param {<xul:browser>} browser
    107 *        The browser to remove the beforeunload event listener from.
    108 * @param {int} howMany
    109 *        How many beforeunload event listeners to remove.
    110 * @param {optional int} frameDepth
    111 *        The depth of the frame to remove the event listener from. Defaults
    112 *        to 1, which is the first subframe inside the top-level content
    113 *        window. Setting this to 0 will throw.
    114 * @return {Promise}
    115 */
    116 function removeOuterBeforeUnloadListeners(
    117  browser,
    118  howMany = 1,
    119  frameDepth = 1
    120 ) {
    121  if (frameDepth == 0) {
    122    throw new Error(
    123      "When removing a beforeunload listener from an outer " +
    124        "window, the frame you're targeting needs to be at " +
    125        "depth > 0."
    126    );
    127  }
    128 
    129  return controlFrameAt(browser, frameDepth, {
    130    name: "RemoveOuterBeforeUnload",
    131    howMany,
    132  });
    133 }
    134 
    135 /**
    136 * Navigates a content window to a particular URL and waits for it to
    137 * finish loading that URL.
    138 *
    139 * By default, will target the top-level content window, but callers
    140 * can specify the index of a subframe to target. See prepareSubframes
    141 * for an idea of how the subframes are structured.
    142 *
    143 * @param {<xul:browser>} browser
    144 *        The browser that will have the navigation occur within it.
    145 * @param {string} url
    146 *        The URL to send the content window to.
    147 * @param {optional int} frameDepth
    148 *        The depth of the frame to navigate. Defaults to 0, which is
    149 *        the top-level content window.
    150 * @return {Promise}
    151 */
    152 function navigateSubframe(browser, url, frameDepth = 0) {
    153  let navigatePromise = controlFrameAt(browser, frameDepth, {
    154    name: "Navigate",
    155    url,
    156  });
    157  let subframeLoad = BrowserTestUtils.browserLoaded(
    158    browser,
    159    true,
    160    new URL(url).href
    161  );
    162  return Promise.all([navigatePromise, subframeLoad]);
    163 }
    164 
    165 /**
    166 * Removes the <iframe> from a content window pointed at PAGE_URL.
    167 *
    168 * By default, will target the top-level content window, but callers
    169 * can specify the index of a subframe to target. See prepareSubframes
    170 * for an idea of how the subframes are structured.
    171 *
    172 * @param {<xul:browser>} browser
    173 *        The browser that will have removal occur within it.
    174 * @param {optional int} frameDepth
    175 *        The depth of the frame that will have the removal occur within
    176 *        it. Defaults to 0, which is the top-level content window, meaning
    177 *        that the first subframe will be removed.
    178 * @return {Promise}
    179 */
    180 function removeSubframeFrom(browser, frameDepth = 0) {
    181  return controlFrameAt(browser, frameDepth, {
    182    name: "RemoveSubframe",
    183  });
    184 }
    185 
    186 /**
    187 * Sends a command to a frame pointed at PAGE_URL. There are utility
    188 * functions defined in this file that call this function. You should
    189 * use those instead.
    190 *
    191 * @param {<xul:browser>} browser
    192 *        The browser to send the command to.
    193 * @param {int} frameDepth
    194 *        The depth of the frame that we'll send the command to. 0 means
    195 *        sending it to the top-level content window.
    196 * @param {object} command
    197 *        An object with the following structure:
    198 *
    199 *        {
    200 *          name: (string),
    201 *          <arbitrary arguments to send with the command>
    202 *        }
    203 *
    204 *        Here are the commands that can be sent:
    205 *
    206 *        AddBeforeUnload
    207 *          {int} howMany
    208 *          How many beforeunload event listeners to add.
    209 *
    210 *        AddOuterBeforeUnload
    211 *          {int} howMany
    212 *          How many beforeunload event listeners to add to
    213 *          the iframe in the document at this depth.
    214 *
    215 *        RemoveBeforeUnload
    216 *          {int} howMany
    217 *          How many beforeunload event listeners to remove.
    218 *
    219 *        RemoveOuterBeforeUnload
    220 *          {int} howMany
    221 *          How many beforeunload event listeners to remove from
    222 *          the iframe in the document at this depth.
    223 *
    224 *        Navigate
    225 *          {string} url
    226 *          The URL to send the frame to.
    227 *
    228 *        RemoveSubframe
    229 *
    230 * @return {Promise}
    231 */
    232 function controlFrameAt(browser, frameDepth, command) {
    233  return SpecialPowers.spawn(
    234    browser,
    235    [{ frameDepth, command }],
    236    async function (args) {
    237      const { TestUtils } = ChromeUtils.importESModule(
    238        "resource://testing-common/TestUtils.sys.mjs"
    239      );
    240 
    241      let { command: contentCommand, frameDepth: contentFrameDepth } = args;
    242 
    243      let targetContent = content;
    244      let targetSubframe = content.document.getElementById("subframe");
    245 
    246      // We want to not only find the frame that maps to the
    247      // target frame depth that we've been given, but we also want
    248      // to count the total depth so that if a middle frame is removed
    249      // or navigated, then we know how many outer-window-destroyed
    250      // observer notifications to expect.
    251      let currentContent = targetContent;
    252      let currentSubframe = targetSubframe;
    253 
    254      let depth = 0;
    255 
    256      do {
    257        currentContent = currentSubframe.contentWindow;
    258        currentSubframe = currentContent.document.getElementById("subframe");
    259        depth++;
    260        if (depth == contentFrameDepth) {
    261          targetContent = currentContent;
    262          targetSubframe = currentSubframe;
    263        }
    264      } while (currentSubframe);
    265 
    266      switch (contentCommand.name) {
    267        case "AddBeforeUnload": {
    268          let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader;
    269          Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page.");
    270          BeforeUnloader.pushInner(contentCommand.howMany);
    271          break;
    272        }
    273        case "AddOuterBeforeUnload": {
    274          let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader;
    275          Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page.");
    276          BeforeUnloader.pushOuter(contentCommand.howMany);
    277          break;
    278        }
    279        case "RemoveBeforeUnload": {
    280          let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader;
    281          Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page.");
    282          BeforeUnloader.popInner(contentCommand.howMany);
    283          break;
    284        }
    285        case "RemoveOuterBeforeUnload": {
    286          let BeforeUnloader = targetContent.wrappedJSObject.BeforeUnloader;
    287          Assert.ok(BeforeUnloader, "Found BeforeUnloader in the test page.");
    288          BeforeUnloader.popOuter(contentCommand.howMany);
    289          break;
    290        }
    291        case "Navigate": {
    292          // How many frames are going to be destroyed when we do this? We
    293          // need to wait for that many window destroyed notifications.
    294          targetContent.location = contentCommand.url;
    295 
    296          let destroyedOuterWindows = depth - contentFrameDepth;
    297          if (destroyedOuterWindows) {
    298            await TestUtils.topicObserved("outer-window-destroyed", () => {
    299              destroyedOuterWindows--;
    300              return !destroyedOuterWindows;
    301            });
    302          }
    303          break;
    304        }
    305        case "RemoveSubframe": {
    306          let subframe = targetContent.document.getElementById("subframe");
    307          Assert.ok(
    308            subframe,
    309            "Found subframe at frame depth of " + contentFrameDepth
    310          );
    311          subframe.remove();
    312 
    313          let destroyedOuterWindows = depth - contentFrameDepth;
    314          if (destroyedOuterWindows) {
    315            await TestUtils.topicObserved("outer-window-destroyed", () => {
    316              destroyedOuterWindows--;
    317              return !destroyedOuterWindows;
    318            });
    319          }
    320          break;
    321        }
    322      }
    323    }
    324  ).catch(console.error);
    325 }
    326 
    327 /**
    328 * Sets up a structure where a page at PAGE_URL will host an
    329 * <iframe> also pointed at PAGE_URL, and does this repeatedly
    330 * until we've achieved the desired frame depth. Note that this
    331 * will cause the top-level browser to reload, and wipe out any
    332 * previous changes to the DOM under it.
    333 *
    334 * @param {<xul:browser>} browser
    335 *        The browser in which we'll load our structure at the
    336 *        top level.
    337 * @param {Array<object>} options
    338 *        Set-up options for each subframe. The following properties
    339 *        are accepted:
    340 *
    341 *        {string} sandboxAttributes
    342 *          The value to set the sandbox attribute to. If null, no sandbox
    343 *          attribute will be set (and any pre-existing sandbox attributes)
    344 *          on the <iframe> will be removed.
    345 *
    346 *        The number of entries on the options Array corresponds to how many
    347 *        subframes are under the top-level content window.
    348 *
    349 *        Example:
    350 *
    351 *        yield prepareSubframes(browser, [
    352 *          { sandboxAttributes: null },
    353 *          { sandboxAttributes: "allow-modals" },
    354 *        ]);
    355 *
    356 *        This would create the following structure:
    357 *
    358 *        <top-level content window at PAGE_URL>
    359 *        |
    360 *        |--> <iframe at PAGE_URL, no sandbox attributes>
    361 *             |
    362 *             |--> <iframe at PAGE_URL, sandbox="allow-modals">
    363 *
    364 * @return {Promise}
    365 */
    366 async function prepareSubframes(browser, options) {
    367  browser.reload();
    368  await BrowserTestUtils.browserLoaded(browser);
    369 
    370  await SpecialPowers.spawn(
    371    browser,
    372    [{ options, PAGE_URL }],
    373    async function (args) {
    374      let { options: allSubframeOptions, PAGE_URL: contentPageURL } = args;
    375      function loadBeforeUnloadHelper(doc, url, subframeOptions) {
    376        let subframe = doc.getElementById("subframe");
    377        subframe.remove();
    378        if (subframeOptions.sandboxAttributes === null) {
    379          subframe.removeAttribute("sandbox");
    380        } else {
    381          subframe.setAttribute("sandbox", subframeOptions.sandboxAttributes);
    382        }
    383        doc.body.appendChild(subframe);
    384        subframe.contentWindow.location = url;
    385        return ContentTaskUtils.waitForEvent(subframe, "load").then(() => {
    386          return subframe.contentDocument;
    387        });
    388      }
    389 
    390      let currentDoc = content.document;
    391      let depth = 1;
    392      for (let subframeOptions of allSubframeOptions) {
    393        // Circumvent recursive load checks.
    394        let url = new URL(contentPageURL);
    395        url.search = `depth=${depth++}`;
    396        currentDoc = await loadBeforeUnloadHelper(
    397          currentDoc,
    398          url.href,
    399          subframeOptions
    400        );
    401      }
    402    }
    403  );
    404 }
    405 
    406 /**
    407 * Ensures that a browser's nsIRemoteTab hasBeforeUnload attribute
    408 * is set to the expected value.
    409 *
    410 * @param {<xul:browser>} browser
    411 *        The browser whose nsIRemoteTab we will check.
    412 * @param {bool} expected
    413 *        True if hasBeforeUnload is expected to be true.
    414 */
    415 function assertHasBeforeUnload(browser, expected) {
    416  Assert.equal(browser.hasBeforeUnload, expected);
    417 }
    418 
    419 /**
    420 * Tests that the MozBrowser hasBeforeUnload property works under
    421 * a number of different scenarios on inner windows. At a high-level,
    422 * we test that hasBeforeUnload works properly during page / iframe
    423 * navigation, or when an <iframe> with a beforeunload listener on its
    424 * inner window is removed from the DOM.
    425 */
    426 add_task(async function test_inner_window_scenarios() {
    427  // Turn this off because the test expects the page to be not bfcached.
    428  await SpecialPowers.pushPrefEnv({
    429    set: [
    430      ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", false],
    431    ],
    432  });
    433  await BrowserTestUtils.withNewTab(
    434    {
    435      gBrowser,
    436      url: PAGE_URL,
    437    },
    438    async function (browser) {
    439      Assert.ok(
    440        browser.isRemoteBrowser,
    441        "This test only makes sense with out of process browsers."
    442      );
    443      assertHasBeforeUnload(browser, false);
    444 
    445      // Test the simple case on the top-level window by adding a single
    446      // beforeunload event listener on the inner window and then removing
    447      // it.
    448      await addBeforeUnloadListeners(browser);
    449      assertHasBeforeUnload(browser, true);
    450      await removeBeforeUnloadListeners(browser);
    451      assertHasBeforeUnload(browser, false);
    452 
    453      // Now let's add several beforeunload listeners, and
    454      // ensure that we only set hasBeforeUnload to false once
    455      // the last listener is removed.
    456      await addBeforeUnloadListeners(browser, 3);
    457      assertHasBeforeUnload(browser, true);
    458      await removeBeforeUnloadListeners(browser); // 2 left...
    459      assertHasBeforeUnload(browser, true);
    460      await removeBeforeUnloadListeners(browser); // 1 left...
    461      assertHasBeforeUnload(browser, true);
    462      await removeBeforeUnloadListeners(browser); // None left!
    463 
    464      assertHasBeforeUnload(browser, false);
    465 
    466      // Now let's have the top-level content window navigate
    467      // away with a beforeunload listener set, and ensure
    468      // that we clear the hasBeforeUnload value.
    469      await addBeforeUnloadListeners(browser, 5);
    470      await navigateSubframe(browser, "http://example.com");
    471      assertHasBeforeUnload(browser, false);
    472 
    473      // Now send the page back to the test page for
    474      // the next few tests.
    475      BrowserTestUtils.startLoadingURIString(browser, PAGE_URL);
    476      await BrowserTestUtils.browserLoaded(browser);
    477 
    478      // We want to test hasBeforeUnload works properly with
    479      // beforeunload event listeners in <iframe> elements too.
    480      // We prepare a structure like this with 3 content windows
    481      // to exercise:
    482      //
    483      // <top-level content window at PAGE_URL> (TOP)
    484      // |
    485      // |--> <iframe at PAGE_URL> (MIDDLE)
    486      //      |
    487      //      |--> <iframe at PAGE_URL> (BOTTOM)
    488      //
    489      await prepareSubframes(browser, [
    490        { sandboxAttributes: null },
    491        { sandboxAttributes: null },
    492      ]);
    493      // These constants are just to make it easier to know which
    494      // frame we're referring to without having to remember the
    495      // exact indices.
    496      const TOP = 0;
    497      const MIDDLE = 1;
    498      const BOTTOM = 2;
    499 
    500      // We should initially start with hasBeforeUnload set to false.
    501      assertHasBeforeUnload(browser, false);
    502 
    503      // Tests that if there are beforeunload event listeners on
    504      // all levels of our window structure, that we only set
    505      // hasBeforeUnload to false once the last beforeunload
    506      // listener has been unset.
    507      await addBeforeUnloadListeners(browser, 2, MIDDLE);
    508      assertHasBeforeUnload(browser, true);
    509      await addBeforeUnloadListeners(browser, 1, TOP);
    510      assertHasBeforeUnload(browser, true);
    511      await addBeforeUnloadListeners(browser, 5, BOTTOM);
    512      assertHasBeforeUnload(browser, true);
    513 
    514      await removeBeforeUnloadListeners(browser, 1, TOP);
    515      assertHasBeforeUnload(browser, true);
    516      await removeBeforeUnloadListeners(browser, 5, BOTTOM);
    517      assertHasBeforeUnload(browser, true);
    518      await removeBeforeUnloadListeners(browser, 2, MIDDLE);
    519      assertHasBeforeUnload(browser, false);
    520 
    521      // Tests that if a beforeunload event listener is set on
    522      // an iframe that navigates away to a page without a
    523      // beforeunload listener, that hasBeforeUnload is set
    524      // to false.
    525      await addBeforeUnloadListeners(browser, 5, BOTTOM);
    526      assertHasBeforeUnload(browser, true);
    527 
    528      await navigateSubframe(browser, "http://example.com", BOTTOM);
    529      assertHasBeforeUnload(browser, false);
    530 
    531      // Reset our window structure now.
    532      await prepareSubframes(browser, [
    533        { sandboxAttributes: null },
    534        { sandboxAttributes: null },
    535      ]);
    536 
    537      // This time, add beforeunload event listeners to both the
    538      // MIDDLE and BOTTOM frame, and then navigate the MIDDLE
    539      // away. This should set hasBeforeUnload to false.
    540      await addBeforeUnloadListeners(browser, 3, MIDDLE);
    541      await addBeforeUnloadListeners(browser, 1, BOTTOM);
    542      assertHasBeforeUnload(browser, true);
    543      await navigateSubframe(browser, "http://example.com", MIDDLE);
    544      assertHasBeforeUnload(browser, false);
    545 
    546      // Tests that if the MIDDLE and BOTTOM frames have beforeunload
    547      // event listeners, and if we remove the BOTTOM <iframe> and the
    548      // MIDDLE <iframe>, that hasBeforeUnload is set to false.
    549      await prepareSubframes(browser, [
    550        { sandboxAttributes: null },
    551        { sandboxAttributes: null },
    552      ]);
    553      await addBeforeUnloadListeners(browser, 3, MIDDLE);
    554      await addBeforeUnloadListeners(browser, 1, BOTTOM);
    555      assertHasBeforeUnload(browser, true);
    556      await removeSubframeFrom(browser, MIDDLE);
    557      assertHasBeforeUnload(browser, true);
    558      await removeSubframeFrom(browser, TOP);
    559      assertHasBeforeUnload(browser, false);
    560 
    561      // Tests that if the MIDDLE and BOTTOM frames have beforeunload
    562      // event listeners, and if we remove just the MIDDLE <iframe>, that
    563      // hasBeforeUnload is set to false.
    564      await prepareSubframes(browser, [
    565        { sandboxAttributes: null },
    566        { sandboxAttributes: null },
    567      ]);
    568      await addBeforeUnloadListeners(browser, 3, MIDDLE);
    569      await addBeforeUnloadListeners(browser, 1, BOTTOM);
    570      assertHasBeforeUnload(browser, true);
    571      await removeSubframeFrom(browser, TOP);
    572      assertHasBeforeUnload(browser, false);
    573 
    574      // Test that two sandboxed iframes, _without_ the allow-modals
    575      // permission, do not result in the hasBeforeUnload attribute
    576      // being set to true when beforeunload event listeners are added.
    577      await prepareSubframes(browser, [
    578        { sandboxAttributes: "allow-scripts" },
    579        { sandboxAttributes: "allow-scripts" },
    580      ]);
    581 
    582      await addBeforeUnloadListeners(browser, 3, MIDDLE);
    583      await addBeforeUnloadListeners(browser, 1, BOTTOM);
    584      assertHasBeforeUnload(browser, false);
    585 
    586      await removeBeforeUnloadListeners(browser, 3, MIDDLE);
    587      await removeBeforeUnloadListeners(browser, 1, BOTTOM);
    588      assertHasBeforeUnload(browser, false);
    589 
    590      // Test that two sandboxed iframes, both with the allow-modals
    591      // permission, cause the hasBeforeUnload attribute to be set
    592      // to true when beforeunload event listeners are added.
    593      await prepareSubframes(browser, [
    594        { sandboxAttributes: "allow-scripts allow-modals" },
    595        { sandboxAttributes: "allow-scripts allow-modals" },
    596      ]);
    597 
    598      await addBeforeUnloadListeners(browser, 3, MIDDLE);
    599      await addBeforeUnloadListeners(browser, 1, BOTTOM);
    600      assertHasBeforeUnload(browser, true);
    601 
    602      await removeBeforeUnloadListeners(browser, 1, BOTTOM);
    603      assertHasBeforeUnload(browser, true);
    604      await removeBeforeUnloadListeners(browser, 3, MIDDLE);
    605      assertHasBeforeUnload(browser, false);
    606    }
    607  );
    608 });
    609 
    610 /**
    611 * Tests that the nsIRemoteTab hasBeforeUnload attribute works under
    612 * a number of different scenarios on outer windows. Very similar to
    613 * the above set of tests, except that we add the beforeunload listeners
    614 * to the iframe DOM nodes instead of the inner windows.
    615 */
    616 add_task(async function test_outer_window_scenarios() {
    617  // Turn this off because the test expects the page to be not bfcached.
    618  await SpecialPowers.pushPrefEnv({
    619    set: [
    620      ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", false],
    621    ],
    622  });
    623  await BrowserTestUtils.withNewTab(
    624    {
    625      gBrowser,
    626      url: PAGE_URL,
    627    },
    628    async function (browser) {
    629      Assert.ok(
    630        browser.isRemoteBrowser,
    631        "This test only makes sense with out of process browsers."
    632      );
    633      assertHasBeforeUnload(browser, false);
    634 
    635      // We want to test hasBeforeUnload works properly with
    636      // beforeunload event listeners in <iframe> elements.
    637      // We prepare a structure like this with 3 content windows
    638      // to exercise:
    639      //
    640      // <top-level content window at PAGE_URL> (TOP)
    641      // |
    642      // |--> <iframe at PAGE_URL> (MIDDLE)
    643      //      |
    644      //      |--> <iframe at PAGE_URL> (BOTTOM)
    645      //
    646      await prepareSubframes(browser, [
    647        { sandboxAttributes: null },
    648        { sandboxAttributes: null },
    649      ]);
    650 
    651      // These constants are just to make it easier to know which
    652      // frame we're referring to without having to remember the
    653      // exact indices.
    654      const TOP = 0;
    655      const MIDDLE = 1;
    656      const BOTTOM = 2;
    657 
    658      // Test the simple case on the top-level window by adding a single
    659      // beforeunload event listener on the outer window of the iframe
    660      // in the TOP document.
    661      await addOuterBeforeUnloadListeners(browser);
    662      assertHasBeforeUnload(browser, true);
    663 
    664      await removeOuterBeforeUnloadListeners(browser);
    665      assertHasBeforeUnload(browser, false);
    666 
    667      // Now let's add several beforeunload listeners, and
    668      // ensure that we only set hasBeforeUnload to false once
    669      // the last listener is removed.
    670      await addOuterBeforeUnloadListeners(browser, 3);
    671      assertHasBeforeUnload(browser, true);
    672      await removeOuterBeforeUnloadListeners(browser); // 2 left...
    673      assertHasBeforeUnload(browser, true);
    674      await removeOuterBeforeUnloadListeners(browser); // 1 left...
    675      assertHasBeforeUnload(browser, true);
    676      await removeOuterBeforeUnloadListeners(browser); // None left!
    677 
    678      assertHasBeforeUnload(browser, false);
    679 
    680      // Now let's have the top-level content window navigate away
    681      // with a beforeunload listener set on the outer window of the
    682      // iframe inside it, and ensure that we clear the hasBeforeUnload
    683      // value.
    684      await addOuterBeforeUnloadListeners(browser, 5);
    685      await navigateSubframe(browser, "http://example.com", TOP);
    686      assertHasBeforeUnload(browser, false);
    687 
    688      // Now send the page back to the test page for
    689      // the next few tests.
    690      BrowserTestUtils.startLoadingURIString(browser, PAGE_URL);
    691      await BrowserTestUtils.browserLoaded(browser);
    692 
    693      // We should initially start with hasBeforeUnload set to false.
    694      assertHasBeforeUnload(browser, false);
    695 
    696      await prepareSubframes(browser, [
    697        { sandboxAttributes: null },
    698        { sandboxAttributes: null },
    699      ]);
    700 
    701      // Tests that if there are beforeunload event listeners on
    702      // all levels of our window structure, that we only set
    703      // hasBeforeUnload to false once the last beforeunload
    704      // listener has been unset.
    705      await addOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    706      assertHasBeforeUnload(browser, true);
    707      await addOuterBeforeUnloadListeners(browser, 7, BOTTOM);
    708      assertHasBeforeUnload(browser, true);
    709 
    710      await removeOuterBeforeUnloadListeners(browser, 7, BOTTOM);
    711      assertHasBeforeUnload(browser, true);
    712      await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    713      assertHasBeforeUnload(browser, false);
    714 
    715      // Tests that if a beforeunload event listener is set on
    716      // an iframe that navigates away to a page without a
    717      // beforeunload listener, that hasBeforeUnload is set
    718      // to false. We're setting the event listener on the
    719      // outer window on the <iframe> in the MIDDLE, which
    720      // itself contains the BOTTOM frame it our structure.
    721      await addOuterBeforeUnloadListeners(browser, 5, BOTTOM);
    722      assertHasBeforeUnload(browser, true);
    723 
    724      // Now navigate that BOTTOM frame.
    725      await navigateSubframe(browser, "http://example.com", BOTTOM);
    726      assertHasBeforeUnload(browser, false);
    727 
    728      // Reset our window structure now.
    729      await prepareSubframes(browser, [
    730        { sandboxAttributes: null },
    731        { sandboxAttributes: null },
    732      ]);
    733 
    734      // This time, add beforeunload event listeners to the outer
    735      // windows for MIDDLE and BOTTOM. Then navigate the MIDDLE
    736      // frame. This should set hasBeforeUnload to false.
    737      await addOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    738      await addOuterBeforeUnloadListeners(browser, 1, BOTTOM);
    739      assertHasBeforeUnload(browser, true);
    740      await navigateSubframe(browser, "http://example.com", MIDDLE);
    741      assertHasBeforeUnload(browser, false);
    742 
    743      // Adds beforeunload event listeners to the outer windows of
    744      // MIDDLE and BOTOTM, and then removes those iframes. Removing
    745      // both iframes should set hasBeforeUnload to false.
    746      await prepareSubframes(browser, [
    747        { sandboxAttributes: null },
    748        { sandboxAttributes: null },
    749      ]);
    750      await addOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    751      await addOuterBeforeUnloadListeners(browser, 1, BOTTOM);
    752      assertHasBeforeUnload(browser, true);
    753      await removeSubframeFrom(browser, BOTTOM);
    754      assertHasBeforeUnload(browser, true);
    755      await removeSubframeFrom(browser, MIDDLE);
    756      assertHasBeforeUnload(browser, false);
    757 
    758      // Adds beforeunload event listeners to the outer windows of MIDDLE
    759      // and BOTTOM, and then removes just the MIDDLE iframe (which will
    760      // take the bottom one with it). This should set hasBeforeUnload to
    761      // false.
    762      await prepareSubframes(browser, [
    763        { sandboxAttributes: null },
    764        { sandboxAttributes: null },
    765      ]);
    766      await addOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    767      await addOuterBeforeUnloadListeners(browser, 1, BOTTOM);
    768      assertHasBeforeUnload(browser, true);
    769      await removeSubframeFrom(browser, TOP);
    770      assertHasBeforeUnload(browser, false);
    771 
    772      // Test that two sandboxed iframes, _without_ the allow-modals
    773      // permission, do not result in the hasBeforeUnload attribute
    774      // being set to true when beforeunload event listeners are added
    775      // to the outer windows. Note that this requires the
    776      // allow-same-origin permission, otherwise a cross-origin
    777      // security exception is thrown.
    778      await prepareSubframes(browser, [
    779        { sandboxAttributes: "allow-same-origin allow-scripts" },
    780        { sandboxAttributes: "allow-same-origin allow-scripts" },
    781      ]);
    782 
    783      await addOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    784      await addOuterBeforeUnloadListeners(browser, 1, BOTTOM);
    785      assertHasBeforeUnload(browser, false);
    786 
    787      await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    788      await removeOuterBeforeUnloadListeners(browser, 1, BOTTOM);
    789      assertHasBeforeUnload(browser, false);
    790 
    791      // Test that two sandboxed iframes, both with the allow-modals
    792      // permission, cause the hasBeforeUnload attribute to be set
    793      // to true when beforeunload event listeners are added. Note
    794      // that this requires the allow-same-origin permission,
    795      // otherwise a cross-origin security exception is thrown.
    796      await prepareSubframes(browser, [
    797        { sandboxAttributes: "allow-same-origin allow-scripts allow-modals" },
    798        { sandboxAttributes: "allow-same-origin allow-scripts allow-modals" },
    799      ]);
    800 
    801      await addOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    802      await addOuterBeforeUnloadListeners(browser, 1, BOTTOM);
    803      assertHasBeforeUnload(browser, true);
    804 
    805      await removeOuterBeforeUnloadListeners(browser, 1, BOTTOM);
    806      assertHasBeforeUnload(browser, true);
    807      await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    808      assertHasBeforeUnload(browser, false);
    809    }
    810  );
    811 });
    812 
    813 /**
    814 * Tests hasBeforeUnload behaviour when beforeunload event listeners
    815 * are added on both inner and outer windows.
    816 */
    817 add_task(async function test_mixed_inner_and_outer_window_scenarios() {
    818  // Turn this off because the test expects the page to be not bfcached.
    819  await SpecialPowers.pushPrefEnv({
    820    set: [
    821      ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", false],
    822    ],
    823  });
    824  await BrowserTestUtils.withNewTab(
    825    {
    826      gBrowser,
    827      url: PAGE_URL,
    828    },
    829    async function (browser) {
    830      Assert.ok(
    831        browser.isRemoteBrowser,
    832        "This test only makes sense with out of process browsers."
    833      );
    834      assertHasBeforeUnload(browser, false);
    835 
    836      // We want to test hasBeforeUnload works properly with
    837      // beforeunload event listeners in <iframe> elements.
    838      // We prepare a structure like this with 3 content windows
    839      // to exercise:
    840      //
    841      // <top-level content window at PAGE_URL> (TOP)
    842      // |
    843      // |--> <iframe at PAGE_URL> (MIDDLE)
    844      //      |
    845      //      |--> <iframe at PAGE_URL> (BOTTOM)
    846      //
    847      await prepareSubframes(browser, [
    848        { sandboxAttributes: null },
    849        { sandboxAttributes: null },
    850      ]);
    851 
    852      // These constants are just to make it easier to know which
    853      // frame we're referring to without having to remember the
    854      // exact indices.
    855      const TOP = 0;
    856      const MIDDLE = 1;
    857      const BOTTOM = 2;
    858 
    859      await addBeforeUnloadListeners(browser, 1, TOP);
    860      assertHasBeforeUnload(browser, true);
    861      await addBeforeUnloadListeners(browser, 2, MIDDLE);
    862      assertHasBeforeUnload(browser, true);
    863      await addBeforeUnloadListeners(browser, 5, BOTTOM);
    864      assertHasBeforeUnload(browser, true);
    865 
    866      await addOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    867      assertHasBeforeUnload(browser, true);
    868      await addOuterBeforeUnloadListeners(browser, 7, BOTTOM);
    869      assertHasBeforeUnload(browser, true);
    870 
    871      await removeBeforeUnloadListeners(browser, 5, BOTTOM);
    872      assertHasBeforeUnload(browser, true);
    873 
    874      await removeBeforeUnloadListeners(browser, 2, MIDDLE);
    875      assertHasBeforeUnload(browser, true);
    876 
    877      await removeOuterBeforeUnloadListeners(browser, 3, MIDDLE);
    878      assertHasBeforeUnload(browser, true);
    879 
    880      await removeBeforeUnloadListeners(browser, 1, TOP);
    881      assertHasBeforeUnload(browser, true);
    882 
    883      await removeOuterBeforeUnloadListeners(browser, 7, BOTTOM);
    884      assertHasBeforeUnload(browser, false);
    885    }
    886  );
    887 });