browser_subdialogs.js (21089B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 /** 7 * Tests for the sub-dialog infrastructure, not for actual sub-dialog functionality. 8 */ 9 10 const gDialogURL = getRootDirectory(gTestPath) + "subdialog.xhtml"; 11 const gDialogURL2 = getRootDirectory(gTestPath) + "subdialog2.xhtml"; 12 13 function open_subdialog_and_test_generic_start_state( 14 browser, 15 domcontentloadedFn, 16 url = gDialogURL 17 ) { 18 let domcontentloadedFnStr = domcontentloadedFn 19 ? "(" + domcontentloadedFn.toString() + ")()" 20 : ""; 21 return SpecialPowers.spawn( 22 browser, 23 [{ url, domcontentloadedFnStr }], 24 async function (args) { 25 let rv = { acceptCount: 0 }; 26 let win = content.window; 27 content.gSubDialog.open(args.url, undefined, rv); 28 let subdialog = content.gSubDialog._topDialog; 29 30 info("waiting for subdialog DOMFrameContentLoaded"); 31 let dialogOpenPromise; 32 await new Promise(resolve => { 33 win.addEventListener( 34 "DOMFrameContentLoaded", 35 function frameContentLoaded(ev) { 36 // We can get events for loads in other frames, so we have to filter 37 // those out. 38 if (ev.target != subdialog._frame) { 39 return; 40 } 41 win.removeEventListener( 42 "DOMFrameContentLoaded", 43 frameContentLoaded 44 ); 45 dialogOpenPromise = ContentTaskUtils.waitForEvent( 46 subdialog._overlay, 47 "dialogopen" 48 ); 49 resolve(); 50 }, 51 { capture: true } 52 ); 53 }); 54 let result; 55 if (args.domcontentloadedFnStr) { 56 // eslint-disable-next-line no-eval 57 result = eval(args.domcontentloadedFnStr); 58 } 59 60 info("waiting for subdialog load"); 61 await dialogOpenPromise; 62 info("subdialog window is loaded"); 63 64 let expectedStyleSheetURLs = subdialog._injectedStyleSheets; 65 let foundStyleSheetURLs = Array.from( 66 subdialog._frame.contentDocument.styleSheets, 67 s => s.href 68 ); 69 70 Assert.greaterOrEqual( 71 expectedStyleSheetURLs.length, 72 0, 73 "Should be at least one injected stylesheet." 74 ); 75 // We can't test that the injected stylesheets come later if those are 76 // the only ones. If this test fails it indicates the test needs to be 77 // changed somehow. 78 Assert.greater( 79 foundStyleSheetURLs.length, 80 expectedStyleSheetURLs.length, 81 "Should see at least all one additional stylesheet from the document." 82 ); 83 84 // The expected stylesheets should be at the end of the list of loaded 85 // stylesheets. 86 foundStyleSheetURLs = foundStyleSheetURLs.slice( 87 foundStyleSheetURLs.length - expectedStyleSheetURLs.length 88 ); 89 90 Assert.deepEqual( 91 foundStyleSheetURLs, 92 expectedStyleSheetURLs, 93 "Should have seen the injected stylesheets in the correct order" 94 ); 95 96 Assert.ok( 97 !!subdialog._frame.contentWindow, 98 "The dialog should be non-null" 99 ); 100 Assert.notEqual( 101 subdialog._frame.contentWindow.location.toString(), 102 "about:blank", 103 "Subdialog URL should not be about:blank" 104 ); 105 Assert.equal( 106 win.getComputedStyle(subdialog._overlay).visibility, 107 "visible", 108 "Overlay should be visible" 109 ); 110 111 return result; 112 } 113 ); 114 } 115 116 async function close_subdialog_and_test_generic_end_state( 117 browser, 118 closingFn, 119 closingButton, 120 acceptCount, 121 options 122 ) { 123 let getDialogsCount = () => { 124 return SpecialPowers.spawn( 125 browser, 126 [], 127 () => content.window.gSubDialog._dialogs.length 128 ); 129 }; 130 let getStackChildrenCount = () => { 131 return SpecialPowers.spawn( 132 browser, 133 [], 134 () => content.window.gSubDialog._dialogStack.children.length 135 ); 136 }; 137 let dialogclosingPromise = SpecialPowers.spawn( 138 browser, 139 [{ closingButton, acceptCount }], 140 async function (expectations) { 141 let win = content.window; 142 let subdialog = win.gSubDialog._topDialog; 143 let frame = subdialog._frame; 144 145 let frameWinUnload = ContentTaskUtils.waitForEvent( 146 frame.contentWindow, 147 "unload", 148 true 149 ); 150 151 let actualAcceptCount; 152 info("waiting for dialogclosing"); 153 info("URI " + frame.currentURI?.spec); 154 let closingEvent = await ContentTaskUtils.waitForEvent( 155 frame.contentWindow, 156 "dialogclosing", 157 true, 158 () => { 159 actualAcceptCount = frame.contentWindow?.arguments[0].acceptCount; 160 return true; 161 } 162 ); 163 164 info("Waiting for subdialog unload"); 165 await frameWinUnload; 166 167 let contentClosingButton = closingEvent.detail.button; 168 169 Assert.notEqual( 170 win.getComputedStyle(subdialog._overlay).visibility, 171 "visible", 172 "overlay is not visible" 173 ); 174 Assert.equal( 175 frame.getAttribute("style"), 176 null, 177 "inline styles should be cleared" 178 ); 179 Assert.equal( 180 contentClosingButton, 181 expectations.closingButton, 182 "closing event should indicate button was '" + 183 expectations.closingButton + 184 "'" 185 ); 186 Assert.equal( 187 actualAcceptCount, 188 expectations.acceptCount, 189 "should be 1 if accepted, 0 if canceled, undefined if closed w/out button" 190 ); 191 } 192 ); 193 let initialDialogsCount = await getDialogsCount(); 194 let initialStackChildrenCount = await getStackChildrenCount(); 195 if (options && options.runClosingFnOutsideOfContentTask) { 196 await closingFn(); 197 } else { 198 SpecialPowers.spawn(browser, [], closingFn); 199 } 200 201 await dialogclosingPromise; 202 let endDialogsCount = await getDialogsCount(); 203 let endStackChildrenCount = await getStackChildrenCount(); 204 Assert.equal( 205 initialDialogsCount - 1, 206 endDialogsCount, 207 "dialog count should decrease by 1" 208 ); 209 Assert.equal( 210 initialStackChildrenCount - 1, 211 endStackChildrenCount, 212 "stack children count should decrease by 1" 213 ); 214 } 215 216 let tab; 217 218 add_setup(async function () { 219 await SpecialPowers.pushPrefEnv({ 220 set: [["security.allow_eval_with_system_principal", true]], 221 }); 222 223 tab = await BrowserTestUtils.openNewForegroundTab( 224 gBrowser, 225 "about:preferences" 226 ); 227 228 registerCleanupFunction(() => { 229 gBrowser.removeTab(tab); 230 }); 231 }); 232 233 add_task( 234 async function check_titlebar_focus_returnval_titlechanges_accepting() { 235 await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); 236 237 let domtitlechangedPromise = BrowserTestUtils.waitForEvent( 238 tab.linkedBrowser, 239 "DOMTitleChanged" 240 ); 241 await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { 242 let dialog = content.window.gSubDialog._topDialog; 243 let dialogWin = dialog._frame.contentWindow; 244 let dialogTitleElement = dialog._titleElement; 245 Assert.equal( 246 dialogTitleElement.textContent, 247 "Sample sub-dialog", 248 "Title should be correct initially" 249 ); 250 Assert.equal( 251 dialogWin.document.activeElement.value, 252 "Default text", 253 "Textbox with correct text is focused" 254 ); 255 dialogWin.document.title = "Updated title"; 256 }); 257 258 info("waiting for DOMTitleChanged event"); 259 await domtitlechangedPromise; 260 261 SpecialPowers.spawn(tab.linkedBrowser, [], async function () { 262 let dialogTitleElement = 263 content.window.gSubDialog._topDialog._titleElement; 264 Assert.equal( 265 dialogTitleElement.textContent, 266 "Updated title", 267 "subdialog should have updated title" 268 ); 269 }); 270 271 // Accept the dialog 272 await close_subdialog_and_test_generic_end_state( 273 tab.linkedBrowser, 274 function () { 275 content.window.gSubDialog._topDialog._frame.contentDocument 276 .getElementById("subDialog") 277 .acceptDialog(); 278 }, 279 "accept", 280 1 281 ); 282 } 283 ); 284 285 add_task(async function check_canceling_dialog() { 286 await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); 287 288 info("canceling the dialog"); 289 await close_subdialog_and_test_generic_end_state( 290 tab.linkedBrowser, 291 function () { 292 content.window.gSubDialog._topDialog._frame.contentDocument 293 .getElementById("subDialog") 294 .cancelDialog(); 295 }, 296 "cancel", 297 0 298 ); 299 }); 300 301 add_task(async function check_reopening_dialog() { 302 await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); 303 info("opening another dialog which will close the first"); 304 await open_subdialog_and_test_generic_start_state( 305 tab.linkedBrowser, 306 "", 307 gDialogURL2 308 ); 309 310 SpecialPowers.spawn(tab.linkedBrowser, [], async function () { 311 let win = content.window; 312 let dialogs = win.gSubDialog._dialogs; 313 let lowerDialog = dialogs[0]; 314 let topDialog = dialogs[1]; 315 Assert.equal(dialogs.length, 2, "There should be two visible dialogs"); 316 Assert.equal( 317 win.getComputedStyle(topDialog._overlay).visibility, 318 "visible", 319 "The top dialog should be visible" 320 ); 321 Assert.equal( 322 win.getComputedStyle(lowerDialog._overlay).visibility, 323 "visible", 324 "The lower dialog should be visible" 325 ); 326 Assert.equal( 327 win.getComputedStyle(topDialog._overlay).backgroundColor, 328 "oklch(0 0 0 / 0.5)", 329 "The top dialog should have a semi-transparent overlay" 330 ); 331 Assert.equal( 332 win.getComputedStyle(lowerDialog._overlay).backgroundColor, 333 "rgba(0, 0, 0, 0)", 334 "The lower dialog should not have an overlay" 335 ); 336 }); 337 338 info("closing two dialogs"); 339 await close_subdialog_and_test_generic_end_state( 340 tab.linkedBrowser, 341 function () { 342 content.window.gSubDialog._topDialog._frame.contentDocument 343 .getElementById("subDialog") 344 .acceptDialog(); 345 }, 346 "accept", 347 1 348 ); 349 await close_subdialog_and_test_generic_end_state( 350 tab.linkedBrowser, 351 function () { 352 content.window.gSubDialog._topDialog._frame.contentDocument 353 .getElementById("subDialog") 354 .acceptDialog(); 355 }, 356 "accept", 357 1 358 ); 359 }); 360 361 add_task(async function check_opening_while_closing() { 362 await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); 363 info("closing"); 364 content.window.gSubDialog._topDialog.close(); 365 info("reopening immediately after calling .close()"); 366 await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); 367 await close_subdialog_and_test_generic_end_state( 368 tab.linkedBrowser, 369 function () { 370 content.window.gSubDialog._topDialog._frame.contentDocument 371 .getElementById("subDialog") 372 .acceptDialog(); 373 }, 374 "accept", 375 1 376 ); 377 }); 378 379 add_task(async function window_close_on_dialog() { 380 await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); 381 382 info("canceling the dialog"); 383 await close_subdialog_and_test_generic_end_state( 384 tab.linkedBrowser, 385 function () { 386 content.window.gSubDialog._topDialog._frame.contentWindow.close(); 387 }, 388 null, 389 0 390 ); 391 }); 392 393 add_task(async function click_close_button_on_dialog() { 394 await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); 395 396 info("canceling the dialog"); 397 await close_subdialog_and_test_generic_end_state( 398 tab.linkedBrowser, 399 function () { 400 return BrowserTestUtils.synthesizeMouseAtCenter( 401 ".dialogClose", 402 {}, 403 tab.linkedBrowser 404 ); 405 }, 406 null, 407 0, 408 { runClosingFnOutsideOfContentTask: true } 409 ); 410 }); 411 412 add_task(async function background_click_should_close_dialog() { 413 await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); 414 415 // Clicking on an inactive part of dialog itself should not close the dialog. 416 // Click the dialog title bar here to make sure nothing happens. 417 info("clicking the dialog title bar"); 418 // We intentionally turn off this a11y check, because the following click 419 // is purposefully targeting a non-interactive element to confirm the opened 420 // dialog won't be dismissed. It is not meant to be interactive and is not 421 // expected to be accessible, therefore this rule check shall be ignored by 422 // a11y_checks suite. 423 AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); 424 BrowserTestUtils.synthesizeMouseAtCenter( 425 ".dialogTitle", 426 {}, 427 tab.linkedBrowser 428 ); 429 AccessibilityUtils.resetEnv(); 430 431 // Close the dialog by clicking on the overlay background. Simulate a click 432 // at point (2,2) instead of (0,0) so we are sure we're clicking on the 433 // overlay background instead of some boundary condition that a real user 434 // would never click. 435 info("clicking the overlay background"); 436 // We intentionally turn off this a11y check, because the following click 437 // is purposefully targeting a non-interactive element to dismiss the opened 438 // dialog with a mouse which can be done by assistive technology and keyboard 439 // by pressing `Esc` key, this rule check shall be ignored by a11y_checks. 440 AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); 441 await close_subdialog_and_test_generic_end_state( 442 tab.linkedBrowser, 443 function () { 444 return BrowserTestUtils.synthesizeMouseAtPoint( 445 2, 446 2, 447 {}, 448 tab.linkedBrowser 449 ); 450 }, 451 null, 452 0, 453 { runClosingFnOutsideOfContentTask: true } 454 ); 455 AccessibilityUtils.resetEnv(); 456 }); 457 458 add_task(async function escape_should_close_dialog() { 459 await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); 460 461 info("canceling the dialog"); 462 await close_subdialog_and_test_generic_end_state( 463 tab.linkedBrowser, 464 function () { 465 return BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, tab.linkedBrowser); 466 }, 467 "cancel", 468 0, 469 { runClosingFnOutsideOfContentTask: true } 470 ); 471 }); 472 473 add_task(async function correct_width_and_height_should_be_used_for_dialog() { 474 await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); 475 476 await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { 477 function fuzzyEqual(value, expectedValue, fuzz, msg) { 478 Assert.greaterOrEqual(expectedValue + fuzz, value, msg); 479 Assert.lessOrEqual(expectedValue - fuzz, value, msg); 480 } 481 let topDialog = content.gSubDialog._topDialog; 482 let frameStyle = content.getComputedStyle(topDialog._frame); 483 let dialogStyle = topDialog.frameContentWindow.getComputedStyle( 484 topDialog.frameContentWindow.document.documentElement 485 ); 486 let fontSize = parseFloat(dialogStyle.fontSize); 487 let height = parseFloat(frameStyle.height); 488 let width = parseFloat(frameStyle.width); 489 490 fuzzyEqual( 491 width, 492 fontSize * 32, 493 2, 494 "Width should be set on the frame from the dialog" 495 ); 496 fuzzyEqual( 497 height, 498 fontSize * 5, 499 2, 500 "Height should be set on the frame from the dialog" 501 ); 502 }); 503 504 await close_subdialog_and_test_generic_end_state( 505 tab.linkedBrowser, 506 function () { 507 content.window.gSubDialog._topDialog._frame.contentWindow.close(); 508 }, 509 null, 510 0 511 ); 512 }); 513 514 add_task( 515 async function wrapped_text_in_dialog_should_have_expected_scrollHeight() { 516 let oldHeight = await open_subdialog_and_test_generic_start_state( 517 tab.linkedBrowser, 518 function domcontentloadedFn() { 519 let frame = content.window.gSubDialog._topDialog._frame; 520 let doc = frame.contentDocument; 521 let scrollHeight = doc.documentElement.scrollHeight; 522 doc.documentElement.style.removeProperty("height"); 523 doc.getElementById("desc").textContent = ` 524 Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque 525 laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi 526 architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas 527 sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione 528 laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi 529 architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas 530 sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione 531 laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi 532 architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas 533 sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione 534 voluptatem sequi nesciunt.`; 535 return scrollHeight; 536 } 537 ); 538 539 await SpecialPowers.spawn( 540 tab.linkedBrowser, 541 [oldHeight], 542 async function (contentOldHeight) { 543 function fuzzyEqual(value, expectedValue, fuzz, msg) { 544 Assert.greaterOrEqual(expectedValue + fuzz, value, msg); 545 Assert.lessOrEqual(expectedValue - fuzz, value, msg); 546 } 547 let topDialog = content.gSubDialog._topDialog; 548 let frame = topDialog._frame; 549 let frameStyle = content.getComputedStyle(frame); 550 let docEl = frame.contentDocument.documentElement; 551 let dialogStyle = topDialog.frameContentWindow.getComputedStyle(docEl); 552 let fontSize = parseFloat(dialogStyle.fontSize); 553 let height = parseFloat(frameStyle.height); 554 let width = parseFloat(frameStyle.width); 555 556 fuzzyEqual( 557 width, 558 32 * fontSize, 559 2, 560 "Width should be set on the frame from the dialog" 561 ); 562 Assert.greater( 563 docEl.scrollHeight, 564 contentOldHeight, 565 "Content height increased (from " + 566 contentOldHeight + 567 " to " + 568 docEl.scrollHeight + 569 ")." 570 ); 571 fuzzyEqual( 572 height, 573 docEl.scrollHeight, 574 2, 575 "Height on the frame should be higher now. " + 576 "This test may fail on certain screen resoluition. " + 577 "See bug 1420576 and bug 1205717." 578 ); 579 } 580 ); 581 582 await close_subdialog_and_test_generic_end_state( 583 tab.linkedBrowser, 584 function () { 585 content.window.gSubDialog._topDialog._frame.contentWindow.window.close(); 586 }, 587 null, 588 0 589 ); 590 } 591 ); 592 593 add_task(async function dialog_too_tall_should_get_reduced_in_height() { 594 await open_subdialog_and_test_generic_start_state( 595 tab.linkedBrowser, 596 function domcontentloadedFn() { 597 let frame = content.window.gSubDialog._topDialog._frame; 598 frame.contentDocument.documentElement.style.height = "100000px"; 599 } 600 ); 601 602 await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { 603 function fuzzyEqual(value, expectedValue, fuzz, msg) { 604 Assert.greaterOrEqual(expectedValue + fuzz, value, msg); 605 Assert.lessOrEqual(expectedValue - fuzz, value, msg); 606 } 607 let topDialog = content.gSubDialog._topDialog; 608 let frame = topDialog._frame; 609 let frameStyle = content.getComputedStyle(frame); 610 let dialogStyle = topDialog.frameContentWindow.getComputedStyle( 611 frame.contentDocument.documentElement 612 ); 613 let fontSize = parseFloat(dialogStyle.fontSize); 614 let height = parseFloat(frameStyle.height); 615 let width = parseFloat(frameStyle.width); 616 fuzzyEqual( 617 width, 618 32 * fontSize, 619 2, 620 "Width should be set on the frame from the dialog" 621 ); 622 Assert.less( 623 height, 624 content.window.innerHeight, 625 "Height on the frame should be smaller than window's innerHeight" 626 ); 627 }); 628 629 await close_subdialog_and_test_generic_end_state( 630 tab.linkedBrowser, 631 function () { 632 content.window.gSubDialog._topDialog._frame.contentWindow.window.close(); 633 }, 634 null, 635 0 636 ); 637 }); 638 639 add_task( 640 async function scrollWidth_and_scrollHeight_from_subdialog_should_size_the_browser() { 641 await open_subdialog_and_test_generic_start_state( 642 tab.linkedBrowser, 643 function domcontentloadedFn() { 644 let frame = content.window.gSubDialog._topDialog._frame; 645 frame.contentDocument.documentElement.style.removeProperty("height"); 646 frame.contentDocument.documentElement.style.removeProperty("width"); 647 } 648 ); 649 650 await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { 651 let frame = content.window.gSubDialog._topDialog._frame; 652 Assert.ok( 653 frame.style.width.endsWith("px"), 654 "Width (" + 655 frame.style.width + 656 ") should be set to a px value of the scrollWidth from the dialog" 657 ); 658 let cs = content.getComputedStyle(frame); 659 Assert.stringMatches( 660 cs.getPropertyValue("--subdialog-inner-height"), 661 /px$/, 662 "Height (" + 663 cs.getPropertyValue("--subdialog-inner-height") + 664 ") should be set to a px value of the scrollHeight from the dialog" 665 ); 666 }); 667 668 await close_subdialog_and_test_generic_end_state( 669 tab.linkedBrowser, 670 function () { 671 content.window.gSubDialog._topDialog._frame.contentWindow.window.close(); 672 }, 673 null, 674 0 675 ); 676 } 677 );