browser_selectpopup.js (28039B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 /* eslint-disable mozilla/no-arbitrary-setTimeout */ 4 5 // This test tests <select> in a child process. This is different than 6 // single-process as a <menulist> is used to implement the dropdown list. 7 8 // FIXME(bug 1774835): This test should be split. 9 requestLongerTimeout(2); 10 11 const XHTML_DTD = 12 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'; 13 14 const PAGECONTENT = 15 "<html xmlns='http://www.w3.org/1999/xhtml'>" + 16 "<body onload='gChangeEvents = 0;gInputEvents = 0; gClickEvents = 0; document.getElementById(\"select\").focus();'>" + 17 "<select id='select' oninput='gInputEvents++' onchange='gChangeEvents++' onclick='if (event.target == this) gClickEvents++'>" + 18 " <optgroup label='First Group'>" + 19 " <option value='One'>One</option>" + 20 " <option value='Two'>Two</option>" + 21 " </optgroup>" + 22 " <option value='Three'>Three</option>" + 23 " <optgroup label='Second Group' disabled='true'>" + 24 " <option value='Four'>Four</option>" + 25 " <option value='Five'>Five</option>" + 26 " </optgroup>" + 27 " <option value='Six' disabled='true'>Six</option>" + 28 " <optgroup label='Third Group'>" + 29 " <option value='Seven'> Seven </option>" + 30 " <option value='Eight'> Eight </option>" + 31 " </optgroup></select><input />Text" + 32 "</body></html>"; 33 34 const PAGECONTENT_XSLT = 35 "<?xml-stylesheet type='text/xml' href='#style1'?>" + 36 "<xsl:stylesheet id='style1'" + 37 " version='1.0'" + 38 " xmlns:xsl='http://www.w3.org/1999/XSL/Transform'" + 39 " xmlns:html='http://www.w3.org/1999/xhtml'>" + 40 "<xsl:template match='xsl:stylesheet'>" + 41 PAGECONTENT + 42 "</xsl:template>" + 43 "</xsl:stylesheet>"; 44 45 const PAGECONTENT_SMALL = 46 "<html>" + 47 "<body><select id='one'>" + 48 " <option value='One'>One</option>" + 49 " <option value='Two'>Two</option>" + 50 "</select><select id='two'>" + 51 " <option value='Three'>Three</option>" + 52 " <option value='Four'>Four</option>" + 53 "</select><select id='three'>" + 54 " <option value='Five'>Five</option>" + 55 " <option value='Six'>Six</option>" + 56 "</select></body></html>"; 57 58 const PAGECONTENT_GROUPS = 59 "<html>" + 60 "<body><select id='one'>" + 61 " <optgroup label='Group 1'>" + 62 " <option value='G1 O1'>G1 O1</option>" + 63 " <option value='G1 O2'>G1 O2</option>" + 64 " <option value='G1 O3'>G1 O3</option>" + 65 " </optgroup>" + 66 " <optgroup label='Group 2'>" + 67 " <option value='G2 O1'>G2 O4</option>" + 68 " <option value='G2 O2'>G2 O5</option>" + 69 " <option value='Hidden' style='display: none;'>Hidden</option>" + 70 " </optgroup>" + 71 "</select></body></html>"; 72 73 const PAGECONTENT_SOMEHIDDEN = 74 "<html><head><style>.hidden { display: none; }</style></head>" + 75 "<body><select id='one'>" + 76 " <option value='One' style='display: none;'>OneHidden</option>" + 77 " <option value='Two' class='hidden'>TwoHidden</option>" + 78 " <option value='Three'>ThreeVisible</option>" + 79 " <option value='Four'style='display: table;'>FourVisible</option>" + 80 " <option value='Five'>FiveVisible</option>" + 81 " <optgroup label='GroupHidden' class='hidden'>" + 82 " <option value='Four'>Six.OneHidden</option>" + 83 " <option value='Five' style='display: block;'>Six.TwoHidden</option>" + 84 " </optgroup>" + 85 " <option value='Six' class='hidden' style='display: block;'>SevenVisible</option>" + 86 "</select></body></html>"; 87 88 const PAGECONTENT_TRANSLATED = 89 "<html><body>" + 90 "<div id='div'>" + 91 "<iframe id='frame' width='320' height='295' style='border: none;'" + 92 " src='data:text/html,<select id=select><option>he he he</option><option>boo boo</option><option>baz baz</option></select>'>" + 93 "</iframe>" + 94 "</div></body></html>"; 95 96 function getInputEvents() { 97 return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { 98 return content.wrappedJSObject.gInputEvents; 99 }); 100 } 101 102 function getChangeEvents() { 103 return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { 104 return content.wrappedJSObject.gChangeEvents; 105 }); 106 } 107 108 function getClickEvents() { 109 return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { 110 return content.wrappedJSObject.gClickEvents; 111 }); 112 } 113 114 async function doSelectTests(contentType, content) { 115 const pageUrl = "data:" + contentType + "," + encodeURIComponent(content); 116 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); 117 118 let selectPopup = await openSelectPopup(); 119 let menulist = selectPopup.parentNode; 120 121 let isWindows = navigator.platform.includes("Win"); 122 123 is(menulist.selectedIndex, 1, "Initial selection"); 124 is( 125 selectPopup.firstElementChild.localName, 126 "menucaption", 127 "optgroup is caption" 128 ); 129 is( 130 selectPopup.firstElementChild.getAttribute("label"), 131 "First Group", 132 "optgroup label" 133 ); 134 is(selectPopup.children[1].localName, "menuitem", "option is menuitem"); 135 is(selectPopup.children[1].getAttribute("label"), "One", "option label"); 136 137 EventUtils.synthesizeKey("KEY_ArrowDown"); 138 is(menulist.activeChild, menulist.getItemAtIndex(2), "Select item 2"); 139 is(menulist.selectedIndex, isWindows ? 2 : 1, "Select item 2 selectedIndex"); 140 141 EventUtils.synthesizeKey("KEY_ArrowDown"); 142 is(menulist.activeChild, menulist.getItemAtIndex(3), "Select item 3"); 143 is(menulist.selectedIndex, isWindows ? 3 : 1, "Select item 3 selectedIndex"); 144 145 EventUtils.synthesizeKey("KEY_ArrowDown"); 146 147 // On Windows, one can navigate on disabled menuitems 148 is( 149 menulist.activeChild, 150 menulist.getItemAtIndex(9), 151 "Skip optgroup header and disabled items select item 7" 152 ); 153 is( 154 menulist.selectedIndex, 155 isWindows ? 9 : 1, 156 "Select or skip disabled item selectedIndex" 157 ); 158 159 for (let i = 0; i < 10; i++) { 160 is( 161 menulist.getItemAtIndex(i).disabled, 162 i >= 4 && i <= 7, 163 "item " + i + " disabled" 164 ); 165 } 166 167 EventUtils.synthesizeKey("KEY_ArrowUp"); 168 is(menulist.activeChild, menulist.getItemAtIndex(3), "Select item 3 again"); 169 is(menulist.selectedIndex, isWindows ? 3 : 1, "Select item 3 selectedIndex"); 170 171 is(await getInputEvents(), 0, "Before closed - number of input events"); 172 is(await getChangeEvents(), 0, "Before closed - number of change events"); 173 is(await getClickEvents(), 0, "Before closed - number of click events"); 174 175 EventUtils.synthesizeKey("a", { accelKey: true }); 176 await SpecialPowers.spawn( 177 gBrowser.selectedBrowser, 178 [{ isWindows }], 179 function (args) { 180 Assert.equal( 181 String(content.getSelection()).trim(), 182 args.isWindows ? "Text" : "", 183 "Select all while popup is open" 184 ); 185 } 186 ); 187 188 // Backspace should not go back 189 let handleKeyPress = function () { 190 ok(false, "Should not get keypress event"); 191 }; 192 window.addEventListener("keypress", handleKeyPress); 193 EventUtils.synthesizeKey("KEY_Backspace"); 194 window.removeEventListener("keypress", handleKeyPress); 195 196 await hideSelectPopup(); 197 198 is(menulist.selectedIndex, 3, "Item 3 still selected"); 199 is(await getInputEvents(), 1, "After closed - number of input events"); 200 is(await getChangeEvents(), 1, "After closed - number of change events"); 201 is(await getClickEvents(), 0, "After closed - number of click events"); 202 203 // Opening and closing the popup without changing the value should not fire a change event. 204 await openSelectPopup("click"); 205 await hideSelectPopup("escape"); 206 is( 207 await getInputEvents(), 208 1, 209 "Open and close with no change - number of input events" 210 ); 211 is( 212 await getChangeEvents(), 213 1, 214 "Open and close with no change - number of change events" 215 ); 216 is( 217 await getClickEvents(), 218 1, 219 "Open and close with no change - number of click events" 220 ); 221 EventUtils.synthesizeKey("KEY_Tab"); 222 EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); 223 is( 224 await getInputEvents(), 225 1, 226 "Tab away from select with no change - number of input events" 227 ); 228 is( 229 await getChangeEvents(), 230 1, 231 "Tab away from select with no change - number of change events" 232 ); 233 is( 234 await getClickEvents(), 235 1, 236 "Tab away from select with no change - number of click events" 237 ); 238 239 await openSelectPopup("click"); 240 EventUtils.synthesizeKey("KEY_ArrowDown"); 241 await hideSelectPopup("escape"); 242 is( 243 await getInputEvents(), 244 isWindows ? 2 : 1, 245 "Open and close with change - number of input events" 246 ); 247 is( 248 await getChangeEvents(), 249 isWindows ? 2 : 1, 250 "Open and close with change - number of change events" 251 ); 252 is( 253 await getClickEvents(), 254 2, 255 "Open and close with change - number of click events" 256 ); 257 EventUtils.synthesizeKey("KEY_Tab"); 258 EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); 259 is( 260 await getInputEvents(), 261 isWindows ? 2 : 1, 262 "Tab away from select with change - number of input events" 263 ); 264 is( 265 await getChangeEvents(), 266 isWindows ? 2 : 1, 267 "Tab away from select with change - number of change events" 268 ); 269 is( 270 await getClickEvents(), 271 2, 272 "Tab away from select with change - number of click events" 273 ); 274 275 is( 276 selectPopup.lastElementChild.previousElementSibling.label, 277 "Seven", 278 "Spaces collapsed" 279 ); 280 is( 281 selectPopup.lastElementChild.label, 282 "\xA0\xA0Eight\xA0\xA0", 283 "Non-breaking spaces not collapsed" 284 ); 285 286 BrowserTestUtils.removeTab(tab); 287 } 288 289 add_setup(async function () { 290 await SpecialPowers.pushPrefEnv({ 291 set: [ 292 ["test.wait300msAfterTabSwitch", true], 293 ["dom.forms.select.customstyling", true], 294 ], 295 }); 296 }); 297 298 add_task(async function () { 299 await doSelectTests("text/html", PAGECONTENT); 300 }); 301 302 add_task(async function () { 303 await doSelectTests("application/xhtml+xml", XHTML_DTD + "\n" + PAGECONTENT); 304 }); 305 306 add_task(async function () { 307 await doSelectTests("application/xml", XHTML_DTD + "\n" + PAGECONTENT_XSLT); 308 }); 309 310 // This test opens a select popup and removes the content node of a popup while 311 // The popup should close if its node is removed. 312 add_task(async function () { 313 const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL); 314 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); 315 316 // First, try it when a different <select> element than the one that is open is removed 317 const selectPopup = await openSelectPopup("click", "#one"); 318 319 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { 320 content.document.body.removeChild(content.document.getElementById("two")); 321 }); 322 323 // Wait a bit just to make sure the popup won't close. 324 await new Promise(resolve => setTimeout(resolve, 1000)); 325 326 is(selectPopup.state, "open", "Different popup did not affect open popup"); 327 328 await hideSelectPopup(); 329 330 // Next, try it when the same <select> element than the one that is open is removed 331 await openSelectPopup("click", "#three"); 332 333 let popupHiddenPromise = BrowserTestUtils.waitForEvent( 334 selectPopup, 335 "popuphidden" 336 ); 337 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { 338 content.document.body.removeChild(content.document.getElementById("three")); 339 }); 340 await popupHiddenPromise; 341 342 ok(true, "Popup hidden when select is removed"); 343 344 // Finally, try it when the tab is closed while the select popup is open. 345 await openSelectPopup("click", "#one"); 346 347 popupHiddenPromise = BrowserTestUtils.waitForEvent( 348 selectPopup, 349 "popuphidden" 350 ); 351 BrowserTestUtils.removeTab(tab); 352 await popupHiddenPromise; 353 354 ok(true, "Popup hidden when tab is closed"); 355 }); 356 357 // This test opens a select popup that is isn't a frame and has some translations applied. 358 add_task(async function () { 359 const pageUrl = "data:text/html," + escape(PAGECONTENT_TRANSLATED); 360 info(`pageUrl: data:text/html,${PAGECONTENT_TRANSLATED}`); 361 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); 362 363 // We need to explicitly call Element.focus() since dataURL is treated as 364 // cross-origin, thus autofocus doesn't work there. 365 const iframe = await SpecialPowers.spawn(tab.linkedBrowser, [], () => { 366 return content.document.querySelector("iframe").browsingContext; 367 }); 368 await SpecialPowers.spawn(iframe, [], async () => { 369 const input = content.document.getElementById("select"); 370 const focusPromise = new Promise(resolve => { 371 input.addEventListener("focus", resolve, { once: true }); 372 }); 373 input.focus(); 374 await focusPromise; 375 }); 376 377 // First, get the position of the select popup when no translations have been applied. 378 const selectPopup = await openSelectPopup(); 379 380 let rect = selectPopup.getBoundingClientRect(); 381 let expectedX = rect.left; 382 let expectedY = rect.top; 383 384 await hideSelectPopup(); 385 386 // Iterate through a set of steps which each add more translation to the select's expected position. 387 let steps = [ 388 ["div", "transform: translateX(7px) translateY(13px);", 7, 13], 389 [ 390 "frame", 391 "border-top: 5px solid green; border-left: 10px solid red; border-right: 35px solid blue;", 392 10, 393 5, 394 ], 395 [ 396 "frame", 397 "border: none; padding-left: 6px; padding-right: 12px; padding-top: 2px;", 398 -4, 399 -3, 400 ], 401 ["select", "margin: 9px; transform: translateY(-3px);", 9, 6], 402 ]; 403 404 for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { 405 let step = steps[stepIndex]; 406 407 await SpecialPowers.spawn( 408 gBrowser.selectedBrowser, 409 [step], 410 async function (contentStep) { 411 return new Promise(resolve => { 412 let changedWin = content; 413 414 let elem; 415 if (contentStep[0] == "select") { 416 changedWin = content.document.getElementById("frame").contentWindow; 417 elem = changedWin.document.getElementById("select"); 418 } else { 419 elem = content.document.getElementById(contentStep[0]); 420 } 421 422 changedWin.addEventListener( 423 "MozAfterPaint", 424 function () { 425 resolve(); 426 }, 427 { once: true } 428 ); 429 430 elem.style = contentStep[1]; 431 elem.getBoundingClientRect(); 432 }); 433 } 434 ); 435 436 await openSelectPopup(); 437 438 expectedX += step[2]; 439 expectedY += step[3]; 440 441 // FIXME: These expectations are not aware of HiDPI environment. 442 let popupRect = selectPopup.getBoundingClientRect(); 443 is(popupRect.left, expectedX, "step " + (stepIndex + 1) + " x"); 444 is(popupRect.top, expectedY, "step " + (stepIndex + 1) + " y"); 445 446 await hideSelectPopup(); 447 } 448 449 BrowserTestUtils.removeTab(tab); 450 }); 451 452 // Test that we get the right events when a select popup is changed. 453 add_task(async function test_event_order() { 454 const URL = "data:text/html," + escape(PAGECONTENT_SMALL); 455 await BrowserTestUtils.withNewTab( 456 { 457 gBrowser, 458 url: URL, 459 }, 460 async function (browser) { 461 // According to https://html.spec.whatwg.org/#the-select-element, 462 // we want to fire input, change, and then click events on the 463 // <select> (in that order) when it has changed. 464 let expectedEnter = [ 465 { 466 type: "input", 467 cancelable: false, 468 targetIsOption: false, 469 composed: true, 470 }, 471 { 472 type: "change", 473 cancelable: false, 474 targetIsOption: false, 475 composed: false, 476 }, 477 ]; 478 479 let expectedClick = [ 480 { 481 type: "mousedown", 482 cancelable: true, 483 targetIsOption: true, 484 composed: true, 485 }, 486 { 487 type: "mouseup", 488 cancelable: true, 489 targetIsOption: true, 490 composed: true, 491 }, 492 { 493 type: "input", 494 cancelable: false, 495 targetIsOption: false, 496 composed: true, 497 }, 498 { 499 type: "change", 500 cancelable: false, 501 targetIsOption: false, 502 composed: false, 503 }, 504 { 505 type: "click", 506 cancelable: true, 507 targetIsOption: true, 508 composed: true, 509 }, 510 ]; 511 512 for (let mode of ["enter", "click"]) { 513 let expected = mode == "enter" ? expectedEnter : expectedClick; 514 await openSelectPopup("click", mode == "enter" ? "#one" : "#two"); 515 516 let eventsPromise = SpecialPowers.spawn( 517 browser, 518 [[mode, expected]], 519 async function ([contentMode, contentExpected]) { 520 return new Promise(resolve => { 521 function onEvent(event) { 522 select.removeEventListener(event.type, onEvent); 523 Assert.ok( 524 contentExpected.length, 525 "Unexpected event " + event.type 526 ); 527 let expectation = contentExpected.shift(); 528 Assert.equal( 529 event.type, 530 expectation.type, 531 "Expected the right event order" 532 ); 533 Assert.ok(event.bubbles, "All of these events should bubble"); 534 Assert.equal( 535 event.cancelable, 536 expectation.cancelable, 537 "Cancellation property should match" 538 ); 539 Assert.equal( 540 event.target.localName, 541 expectation.targetIsOption ? "option" : "select", 542 "Target matches" 543 ); 544 Assert.equal( 545 event.composed, 546 expectation.composed, 547 "Composed property should match" 548 ); 549 if (!contentExpected.length) { 550 resolve(); 551 } 552 } 553 554 let select = content.document.getElementById( 555 contentMode == "enter" ? "one" : "two" 556 ); 557 for (let event of [ 558 "input", 559 "change", 560 "mousedown", 561 "mouseup", 562 "click", 563 ]) { 564 select.addEventListener(event, onEvent); 565 } 566 }); 567 } 568 ); 569 570 EventUtils.synthesizeKey("KEY_ArrowDown"); 571 await hideSelectPopup(mode); 572 await eventsPromise; 573 } 574 } 575 ); 576 }); 577 578 async function performSelectSearchTests(win) { 579 let browser = win.gBrowser.selectedBrowser; 580 await SpecialPowers.spawn(browser, [], async function () { 581 let doc = content.document; 582 let select = doc.getElementById("one"); 583 584 for (var i = 0; i < 40; i++) { 585 select.add(new content.Option("Test" + i)); 586 } 587 588 select.options[1].selected = true; 589 select.focus(); 590 }); 591 592 let selectPopup = await openSelectPopup(false, "select", win); 593 594 let searchElement = selectPopup.querySelector( 595 ".contentSelectDropdown-searchbox" 596 ); 597 searchElement.focus(); 598 599 EventUtils.synthesizeKey("O", {}, win); 600 is(selectPopup.children[2].hidden, false, "First option should be visible"); 601 is(selectPopup.children[3].hidden, false, "Second option should be visible"); 602 603 EventUtils.synthesizeKey("3", {}, win); 604 is(selectPopup.children[2].hidden, true, "First option should be hidden"); 605 is(selectPopup.children[3].hidden, true, "Second option should be hidden"); 606 is(selectPopup.children[4].hidden, false, "Third option should be visible"); 607 608 EventUtils.synthesizeKey("Z", {}, win); 609 is(selectPopup.children[4].hidden, true, "Third option should be hidden"); 610 is( 611 selectPopup.children[1].hidden, 612 true, 613 "First group header should be hidden" 614 ); 615 616 EventUtils.synthesizeKey("KEY_Backspace", {}, win); 617 is(selectPopup.children[4].hidden, false, "Third option should be visible"); 618 619 EventUtils.synthesizeKey("KEY_Backspace", {}, win); 620 is( 621 selectPopup.children[5].hidden, 622 false, 623 "Second group header should be visible" 624 ); 625 626 EventUtils.synthesizeKey("KEY_Backspace", {}, win); 627 EventUtils.synthesizeKey("O", {}, win); 628 EventUtils.synthesizeKey("5", {}, win); 629 is( 630 selectPopup.children[5].hidden, 631 false, 632 "Second group header should be visible" 633 ); 634 is( 635 selectPopup.children[1].hidden, 636 true, 637 "First group header should be hidden" 638 ); 639 640 EventUtils.synthesizeKey("KEY_Backspace", {}, win); 641 is( 642 selectPopup.children[1].hidden, 643 false, 644 "First group header should be shown" 645 ); 646 647 EventUtils.synthesizeKey("KEY_Backspace", {}, win); 648 is( 649 selectPopup.children[8].hidden, 650 true, 651 "Option hidden by content should remain hidden" 652 ); 653 654 await hideSelectPopup("escape", win); 655 } 656 657 // This test checks the functionality of search in select elements with groups 658 // and a large number of options. 659 add_task(async function test_select_search() { 660 await SpecialPowers.pushPrefEnv({ 661 set: [["dom.forms.selectSearch", true]], 662 }); 663 const pageUrl = "data:text/html," + escape(PAGECONTENT_GROUPS); 664 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); 665 666 await performSelectSearchTests(window); 667 668 BrowserTestUtils.removeTab(tab); 669 670 await SpecialPowers.popPrefEnv(); 671 }); 672 673 // This test checks that a mousemove event is fired correctly at the menu and 674 // not at the browser, ensuring that any mouse capture has been cleared. 675 add_task(async function test_mousemove_correcttarget() { 676 const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL); 677 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); 678 679 const selectPopup = await openSelectPopup("mousedown"); 680 681 await new Promise(resolve => { 682 window.addEventListener( 683 "mousemove", 684 function (event) { 685 is(event.target.localName.indexOf("menu"), 0, "mouse over menu"); 686 resolve(); 687 }, 688 { capture: true, once: true } 689 ); 690 691 EventUtils.synthesizeMouseAtCenter(selectPopup.firstElementChild, { 692 type: "mousemove", 693 buttons: 1, 694 }); 695 }); 696 697 await BrowserTestUtils.synthesizeMouseAtCenter( 698 "#one", 699 { type: "mouseup" }, 700 gBrowser.selectedBrowser 701 ); 702 703 await hideSelectPopup(); 704 705 // The popup should be closed when fullscreen mode is entered or exited. 706 for (let steps = 0; steps < 2; steps++) { 707 await openSelectPopup("click"); 708 let popupHiddenPromise = BrowserTestUtils.waitForEvent( 709 selectPopup, 710 "popuphidden" 711 ); 712 let sizeModeChanged = BrowserTestUtils.waitForEvent( 713 window, 714 "sizemodechange" 715 ); 716 BrowserCommands.fullScreen(); 717 await sizeModeChanged; 718 await popupHiddenPromise; 719 } 720 721 BrowserTestUtils.removeTab(tab); 722 }); 723 724 // This test checks when a <select> element has some options with altered display values. 725 add_task(async function test_somehidden() { 726 const pageUrl = "data:text/html," + escape(PAGECONTENT_SOMEHIDDEN); 727 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); 728 729 let selectPopup = await openSelectPopup("click"); 730 731 // The exact number is not needed; just ensure the height is larger than 4 items to accommodate any popup borders. 732 Assert.greaterOrEqual( 733 selectPopup.getBoundingClientRect().height, 734 selectPopup.lastElementChild.getBoundingClientRect().height * 4, 735 "Height contains at least 4 items" 736 ); 737 Assert.less( 738 selectPopup.getBoundingClientRect().height, 739 selectPopup.lastElementChild.getBoundingClientRect().height * 5, 740 "Height doesn't contain 5 items" 741 ); 742 743 // The label contains the substring 'Visible' for items that are visible. 744 // Otherwise, it is expected to be display: none. 745 is(selectPopup.parentNode.itemCount, 9, "Correct number of items"); 746 let child = selectPopup.firstElementChild; 747 let idx = 1; 748 while (child) { 749 is( 750 getComputedStyle(child).display, 751 child.label.indexOf("Visible") > 0 ? "flex" : "none", 752 "Item " + idx++ + " is visible" 753 ); 754 child = child.nextElementSibling; 755 } 756 757 await hideSelectPopup("escape"); 758 BrowserTestUtils.removeTab(tab); 759 }); 760 761 // This test checks that the popup is closed when the select element is blurred. 762 add_task(async function test_blur_hides_popup() { 763 const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL); 764 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); 765 766 await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { 767 content.addEventListener( 768 "blur", 769 function (event) { 770 event.preventDefault(); 771 event.stopPropagation(); 772 }, 773 true 774 ); 775 776 content.document.getElementById("one").focus(); 777 }); 778 779 let selectPopup = await openSelectPopup(); 780 781 let popupHiddenPromise = BrowserTestUtils.waitForEvent( 782 selectPopup, 783 "popuphidden" 784 ); 785 786 await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { 787 content.document.getElementById("one").blur(); 788 }); 789 790 await popupHiddenPromise; 791 792 ok(true, "Blur closed popup"); 793 794 BrowserTestUtils.removeTab(tab); 795 }); 796 797 // Test zoom handling. 798 add_task(async function test_zoom() { 799 const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL); 800 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); 801 802 info("Opening the popup"); 803 const selectPopup = await openSelectPopup("click"); 804 805 info("Opened the popup"); 806 let nonZoomedFontSize = parseFloat( 807 getComputedStyle(selectPopup.querySelector("menuitem")).fontSize, 808 10 809 ); 810 811 info("font-size is " + nonZoomedFontSize); 812 await hideSelectPopup(); 813 814 info("Hide the popup"); 815 816 for (let i = 0; i < 2; ++i) { 817 info("Testing with full zoom: " + ZoomManager.useFullZoom); 818 819 // This is confusing, but does the right thing. 820 FullZoom.setZoom(2.0, tab.linkedBrowser); 821 822 info("Opening popup again"); 823 await openSelectPopup("click"); 824 825 let zoomedFontSize = parseFloat( 826 getComputedStyle(selectPopup.querySelector("menuitem")).fontSize, 827 10 828 ); 829 info("Zoomed font-size is " + zoomedFontSize); 830 831 Assert.less( 832 Math.abs(zoomedFontSize - nonZoomedFontSize * 2.0), 833 0.01, 834 `Zoom should affect menu popup size, got ${zoomedFontSize}, ` + 835 `expected ${nonZoomedFontSize * 2.0}` 836 ); 837 838 await hideSelectPopup(); 839 info("Hid the popup again"); 840 841 ZoomManager.toggleZoom(); 842 } 843 844 FullZoom.setZoom(1.0, tab.linkedBrowser); // make sure the zoom level is reset 845 BrowserTestUtils.removeTab(tab); 846 }); 847 848 // Test that input and change events are dispatched consistently (bug 1561882). 849 add_task(async function test_event_destroys_popup() { 850 const PAGE_CONTENT = ` 851 <!doctype html> 852 <select> 853 <option>a</option> 854 <option>b</option> 855 </select> 856 <script> 857 gChangeEvents = 0; 858 gInputEvents = 0; 859 let select = document.querySelector("select"); 860 select.addEventListener("input", function() { 861 gInputEvents++; 862 this.style.display = "none"; 863 this.getBoundingClientRect(); 864 }) 865 select.addEventListener("change", function() { 866 gChangeEvents++; 867 }) 868 </script>`; 869 870 const pageUrl = "data:text/html," + escape(PAGE_CONTENT); 871 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); 872 873 // Test change and input events get handled consistently 874 await openSelectPopup("click"); 875 EventUtils.synthesizeKey("KEY_ArrowDown"); 876 await hideSelectPopup(); 877 878 is( 879 await getChangeEvents(), 880 1, 881 "Should get change and input events consistently" 882 ); 883 is( 884 await getInputEvents(), 885 1, 886 "Should get change and input events consistently (input)" 887 ); 888 889 BrowserTestUtils.removeTab(tab); 890 }); 891 892 add_task(async function test_label_not_text() { 893 const PAGE_CONTENT = ` 894 <!doctype html> 895 <select> 896 <option label="Some nifty Label">Some Element Text Instead</option> 897 <option label="">Element Text</option> 898 </select> 899 `; 900 901 const pageUrl = "data:text/html," + escape(PAGE_CONTENT); 902 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); 903 904 const selectPopup = await openSelectPopup("click"); 905 906 is( 907 selectPopup.children[0].label, 908 "Some nifty Label", 909 "Use the label not the text." 910 ); 911 912 is( 913 selectPopup.children[1].label, 914 "Element Text", 915 "Uses the text if the label is empty, like HTMLOptionElement::GetRenderedLabel." 916 ); 917 918 BrowserTestUtils.removeTab(tab); 919 });