/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /* eslint-disable mozilla/no-arbitrary-setTimeout */ // This test tests " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " Text" + ""; const PAGECONTENT_XSLT = "" + "" + "" + PAGECONTENT + "" + ""; const PAGECONTENT_SMALL = "" + ""; const PAGECONTENT_GROUPS = "" + ""; const PAGECONTENT_SOMEHIDDEN = "" + ""; const PAGECONTENT_TRANSLATED = "" + "
" + "" + "
"; function getInputEvents() { return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { return content.wrappedJSObject.gInputEvents; }); } function getChangeEvents() { return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { return content.wrappedJSObject.gChangeEvents; }); } function getClickEvents() { return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { return content.wrappedJSObject.gClickEvents; }); } async function doSelectTests(contentType, content) { const pageUrl = "data:" + contentType + "," + encodeURIComponent(content); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); let selectPopup = await openSelectPopup(); let menulist = selectPopup.parentNode; let isWindows = navigator.platform.includes("Win"); is(menulist.selectedIndex, 1, "Initial selection"); is( selectPopup.firstElementChild.localName, "menucaption", "optgroup is caption" ); is( selectPopup.firstElementChild.getAttribute("label"), "First Group", "optgroup label" ); is(selectPopup.children[1].localName, "menuitem", "option is menuitem"); is(selectPopup.children[1].getAttribute("label"), "One", "option label"); EventUtils.synthesizeKey("KEY_ArrowDown"); is(menulist.activeChild, menulist.getItemAtIndex(2), "Select item 2"); is(menulist.selectedIndex, isWindows ? 2 : 1, "Select item 2 selectedIndex"); EventUtils.synthesizeKey("KEY_ArrowDown"); is(menulist.activeChild, menulist.getItemAtIndex(3), "Select item 3"); is(menulist.selectedIndex, isWindows ? 3 : 1, "Select item 3 selectedIndex"); EventUtils.synthesizeKey("KEY_ArrowDown"); // On Windows, one can navigate on disabled menuitems is( menulist.activeChild, menulist.getItemAtIndex(9), "Skip optgroup header and disabled items select item 7" ); is( menulist.selectedIndex, isWindows ? 9 : 1, "Select or skip disabled item selectedIndex" ); for (let i = 0; i < 10; i++) { is( menulist.getItemAtIndex(i).disabled, i >= 4 && i <= 7, "item " + i + " disabled" ); } EventUtils.synthesizeKey("KEY_ArrowUp"); is(menulist.activeChild, menulist.getItemAtIndex(3), "Select item 3 again"); is(menulist.selectedIndex, isWindows ? 3 : 1, "Select item 3 selectedIndex"); is(await getInputEvents(), 0, "Before closed - number of input events"); is(await getChangeEvents(), 0, "Before closed - number of change events"); is(await getClickEvents(), 0, "Before closed - number of click events"); EventUtils.synthesizeKey("a", { accelKey: true }); await SpecialPowers.spawn( gBrowser.selectedBrowser, [{ isWindows }], function (args) { Assert.equal( String(content.getSelection()).trim(), args.isWindows ? "Text" : "", "Select all while popup is open" ); } ); // Backspace should not go back let handleKeyPress = function () { ok(false, "Should not get keypress event"); }; window.addEventListener("keypress", handleKeyPress); EventUtils.synthesizeKey("KEY_Backspace"); window.removeEventListener("keypress", handleKeyPress); await hideSelectPopup(); is(menulist.selectedIndex, 3, "Item 3 still selected"); is(await getInputEvents(), 1, "After closed - number of input events"); is(await getChangeEvents(), 1, "After closed - number of change events"); is(await getClickEvents(), 0, "After closed - number of click events"); // Opening and closing the popup without changing the value should not fire a change event. await openSelectPopup("click"); await hideSelectPopup("escape"); is( await getInputEvents(), 1, "Open and close with no change - number of input events" ); is( await getChangeEvents(), 1, "Open and close with no change - number of change events" ); is( await getClickEvents(), 1, "Open and close with no change - number of click events" ); EventUtils.synthesizeKey("KEY_Tab"); EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); is( await getInputEvents(), 1, "Tab away from select with no change - number of input events" ); is( await getChangeEvents(), 1, "Tab away from select with no change - number of change events" ); is( await getClickEvents(), 1, "Tab away from select with no change - number of click events" ); await openSelectPopup("click"); EventUtils.synthesizeKey("KEY_ArrowDown"); await hideSelectPopup("escape"); is( await getInputEvents(), isWindows ? 2 : 1, "Open and close with change - number of input events" ); is( await getChangeEvents(), isWindows ? 2 : 1, "Open and close with change - number of change events" ); is( await getClickEvents(), 2, "Open and close with change - number of click events" ); EventUtils.synthesizeKey("KEY_Tab"); EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); is( await getInputEvents(), isWindows ? 2 : 1, "Tab away from select with change - number of input events" ); is( await getChangeEvents(), isWindows ? 2 : 1, "Tab away from select with change - number of change events" ); is( await getClickEvents(), 2, "Tab away from select with change - number of click events" ); is( selectPopup.lastElementChild.previousElementSibling.label, "Seven", "Spaces collapsed" ); is( selectPopup.lastElementChild.label, "\xA0\xA0Eight\xA0\xA0", "Non-breaking spaces not collapsed" ); BrowserTestUtils.removeTab(tab); } add_setup(async function () { await SpecialPowers.pushPrefEnv({ set: [ ["test.wait300msAfterTabSwitch", true], ["dom.forms.select.customstyling", true], ], }); }); add_task(async function () { await doSelectTests("text/html", PAGECONTENT); }); add_task(async function () { await doSelectTests("application/xhtml+xml", XHTML_DTD + "\n" + PAGECONTENT); }); add_task(async function () { await doSelectTests("application/xml", XHTML_DTD + "\n" + PAGECONTENT_XSLT); }); // This test opens a select popup and removes the content node of a popup while // The popup should close if its node is removed. add_task(async function () { const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); // First, try it when a different element than the one that is open is removed await openSelectPopup("click", "#three"); let popupHiddenPromise = BrowserTestUtils.waitForEvent( selectPopup, "popuphidden" ); await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { content.document.body.removeChild(content.document.getElementById("three")); }); await popupHiddenPromise; ok(true, "Popup hidden when select is removed"); // Finally, try it when the tab is closed while the select popup is open. await openSelectPopup("click", "#one"); popupHiddenPromise = BrowserTestUtils.waitForEvent( selectPopup, "popuphidden" ); BrowserTestUtils.removeTab(tab); await popupHiddenPromise; ok(true, "Popup hidden when tab is closed"); }); // This test opens a select popup that is isn't a frame and has some translations applied. add_task(async function () { const pageUrl = "data:text/html," + escape(PAGECONTENT_TRANSLATED); info(`pageUrl: data:text/html,${PAGECONTENT_TRANSLATED}`); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); // We need to explicitly call Element.focus() since dataURL is treated as // cross-origin, thus autofocus doesn't work there. const iframe = await SpecialPowers.spawn(tab.linkedBrowser, [], () => { return content.document.querySelector("iframe").browsingContext; }); await SpecialPowers.spawn(iframe, [], async () => { const input = content.document.getElementById("select"); const focusPromise = new Promise(resolve => { input.addEventListener("focus", resolve, { once: true }); }); input.focus(); await focusPromise; }); // First, get the position of the select popup when no translations have been applied. const selectPopup = await openSelectPopup(); let rect = selectPopup.getBoundingClientRect(); let expectedX = rect.left; let expectedY = rect.top; await hideSelectPopup(); // Iterate through a set of steps which each add more translation to the select's expected position. let steps = [ ["div", "transform: translateX(7px) translateY(13px);", 7, 13], [ "frame", "border-top: 5px solid green; border-left: 10px solid red; border-right: 35px solid blue;", 10, 5, ], [ "frame", "border: none; padding-left: 6px; padding-right: 12px; padding-top: 2px;", -4, -3, ], ["select", "margin: 9px; transform: translateY(-3px);", 9, 6], ]; for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) { let step = steps[stepIndex]; await SpecialPowers.spawn( gBrowser.selectedBrowser, [step], async function (contentStep) { return new Promise(resolve => { let changedWin = content; let elem; if (contentStep[0] == "select") { changedWin = content.document.getElementById("frame").contentWindow; elem = changedWin.document.getElementById("select"); } else { elem = content.document.getElementById(contentStep[0]); } changedWin.addEventListener( "MozAfterPaint", function () { resolve(); }, { once: true } ); elem.style = contentStep[1]; elem.getBoundingClientRect(); }); } ); await openSelectPopup(); expectedX += step[2]; expectedY += step[3]; // FIXME: These expectations are not aware of HiDPI environment. let popupRect = selectPopup.getBoundingClientRect(); is(popupRect.left, expectedX, "step " + (stepIndex + 1) + " x"); is(popupRect.top, expectedY, "step " + (stepIndex + 1) + " y"); await hideSelectPopup(); } BrowserTestUtils.removeTab(tab); }); // Test that we get the right events when a select popup is changed. add_task(async function test_event_order() { const URL = "data:text/html," + escape(PAGECONTENT_SMALL); await BrowserTestUtils.withNewTab( { gBrowser, url: URL, }, async function (browser) { // According to https://html.spec.whatwg.org/#the-select-element, // we want to fire input, change, and then click events on the // element has some options with altered display values. add_task(async function test_somehidden() { const pageUrl = "data:text/html," + escape(PAGECONTENT_SOMEHIDDEN); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); let selectPopup = await openSelectPopup("click"); // The exact number is not needed; just ensure the height is larger than 4 items to accommodate any popup borders. Assert.greaterOrEqual( selectPopup.getBoundingClientRect().height, selectPopup.lastElementChild.getBoundingClientRect().height * 4, "Height contains at least 4 items" ); Assert.less( selectPopup.getBoundingClientRect().height, selectPopup.lastElementChild.getBoundingClientRect().height * 5, "Height doesn't contain 5 items" ); // The label contains the substring 'Visible' for items that are visible. // Otherwise, it is expected to be display: none. is(selectPopup.parentNode.itemCount, 9, "Correct number of items"); let child = selectPopup.firstElementChild; let idx = 1; while (child) { is( getComputedStyle(child).display, child.label.indexOf("Visible") > 0 ? "flex" : "none", "Item " + idx++ + " is visible" ); child = child.nextElementSibling; } await hideSelectPopup("escape"); BrowserTestUtils.removeTab(tab); }); // This test checks that the popup is closed when the select element is blurred. add_task(async function test_blur_hides_popup() { const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { content.addEventListener( "blur", function (event) { event.preventDefault(); event.stopPropagation(); }, true ); content.document.getElementById("one").focus(); }); let selectPopup = await openSelectPopup(); let popupHiddenPromise = BrowserTestUtils.waitForEvent( selectPopup, "popuphidden" ); await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { content.document.getElementById("one").blur(); }); await popupHiddenPromise; ok(true, "Blur closed popup"); BrowserTestUtils.removeTab(tab); }); // Test zoom handling. add_task(async function test_zoom() { const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); info("Opening the popup"); const selectPopup = await openSelectPopup("click"); info("Opened the popup"); let nonZoomedFontSize = parseFloat( getComputedStyle(selectPopup.querySelector("menuitem")).fontSize, 10 ); info("font-size is " + nonZoomedFontSize); await hideSelectPopup(); info("Hide the popup"); for (let i = 0; i < 2; ++i) { info("Testing with full zoom: " + ZoomManager.useFullZoom); // This is confusing, but does the right thing. FullZoom.setZoom(2.0, tab.linkedBrowser); info("Opening popup again"); await openSelectPopup("click"); let zoomedFontSize = parseFloat( getComputedStyle(selectPopup.querySelector("menuitem")).fontSize, 10 ); info("Zoomed font-size is " + zoomedFontSize); Assert.less( Math.abs(zoomedFontSize - nonZoomedFontSize * 2.0), 0.01, `Zoom should affect menu popup size, got ${zoomedFontSize}, ` + `expected ${nonZoomedFontSize * 2.0}` ); await hideSelectPopup(); info("Hid the popup again"); ZoomManager.toggleZoom(); } FullZoom.setZoom(1.0, tab.linkedBrowser); // make sure the zoom level is reset BrowserTestUtils.removeTab(tab); }); // Test that input and change events are dispatched consistently (bug 1561882). add_task(async function test_event_destroys_popup() { const PAGE_CONTENT = ` `; const pageUrl = "data:text/html," + escape(PAGE_CONTENT); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); // Test change and input events get handled consistently await openSelectPopup("click"); EventUtils.synthesizeKey("KEY_ArrowDown"); await hideSelectPopup(); is( await getChangeEvents(), 1, "Should get change and input events consistently" ); is( await getInputEvents(), 1, "Should get change and input events consistently (input)" ); BrowserTestUtils.removeTab(tab); }); add_task(async function test_label_not_text() { const PAGE_CONTENT = ` `; const pageUrl = "data:text/html," + escape(PAGE_CONTENT); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); const selectPopup = await openSelectPopup("click"); is( selectPopup.children[0].label, "Some nifty Label", "Use the label not the text." ); is( selectPopup.children[1].label, "Element Text", "Uses the text if the label is empty, like HTMLOptionElement::GetRenderedLabel." ); BrowserTestUtils.removeTab(tab); });