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 });