diff options
Diffstat (limited to '')
14 files changed, 2601 insertions, 0 deletions
diff --git a/browser/base/content/test/forms/browser.toml b/browser/base/content/test/forms/browser.toml new file mode 100644 index 0000000000..33d73ba8bf --- /dev/null +++ b/browser/base/content/test/forms/browser.toml @@ -0,0 +1,34 @@ +[DEFAULT] +prefs = ["gfx.font_loader.delay=0", "dom.select.showPicker.enabled=true"] +support-files = ["head.js"] + +["browser_selectpopup.js"] +skip-if = [ + "os == 'linux'", # Bug 1329991 + "os == 'mac'", # Bug 1661132, 1775896 + "verify && os == 'win'", +] + +["browser_selectpopup_colors.js"] +skip-if = ["os == 'linux'"] # Bug 1329991 - test fails intermittently on Linux builds + +["browser_selectpopup_dir.js"] + +["browser_selectpopup_hr.js"] + +["browser_selectpopup_large.js"] + +["browser_selectpopup_searchfocus.js"] +fail-if = ["a11y_checks"] # Bug 1854233 input may not be labeled + +["browser_selectpopup_showPicker.js"] + +["browser_selectpopup_text_transform.js"] + +["browser_selectpopup_toplevel.js"] + +["browser_selectpopup_user_input.js"] + +["browser_selectpopup_width.js"] + +["browser_selectpopup_xhtml.js"] diff --git a/browser/base/content/test/forms/browser_selectpopup.js b/browser/base/content/test/forms/browser_selectpopup.js new file mode 100644 index 0000000000..abcdee486f --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup.js @@ -0,0 +1,914 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +// This test tests <select> in a child process. This is different than +// single-process as a <menulist> is used to implement the dropdown list. + +// FIXME(bug 1774835): This test should be split. +requestLongerTimeout(2); + +const XHTML_DTD = + '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'; + +const PAGECONTENT = + "<html xmlns='http://www.w3.org/1999/xhtml'>" + + "<body onload='gChangeEvents = 0;gInputEvents = 0; gClickEvents = 0; document.getElementById(\"select\").focus();'>" + + "<select id='select' oninput='gInputEvents++' onchange='gChangeEvents++' onclick='if (event.target == this) gClickEvents++'>" + + " <optgroup label='First Group'>" + + " <option value='One'>One</option>" + + " <option value='Two'>Two</option>" + + " </optgroup>" + + " <option value='Three'>Three</option>" + + " <optgroup label='Second Group' disabled='true'>" + + " <option value='Four'>Four</option>" + + " <option value='Five'>Five</option>" + + " </optgroup>" + + " <option value='Six' disabled='true'>Six</option>" + + " <optgroup label='Third Group'>" + + " <option value='Seven'> Seven </option>" + + " <option value='Eight'> Eight </option>" + + " </optgroup></select><input />Text" + + "</body></html>"; + +const PAGECONTENT_XSLT = + "<?xml-stylesheet type='text/xml' href='#style1'?>" + + "<xsl:stylesheet id='style1'" + + " version='1.0'" + + " xmlns:xsl='http://www.w3.org/1999/XSL/Transform'" + + " xmlns:html='http://www.w3.org/1999/xhtml'>" + + "<xsl:template match='xsl:stylesheet'>" + + PAGECONTENT + + "</xsl:template>" + + "</xsl:stylesheet>"; + +const PAGECONTENT_SMALL = + "<html>" + + "<body><select id='one'>" + + " <option value='One'>One</option>" + + " <option value='Two'>Two</option>" + + "</select><select id='two'>" + + " <option value='Three'>Three</option>" + + " <option value='Four'>Four</option>" + + "</select><select id='three'>" + + " <option value='Five'>Five</option>" + + " <option value='Six'>Six</option>" + + "</select></body></html>"; + +const PAGECONTENT_GROUPS = + "<html>" + + "<body><select id='one'>" + + " <optgroup label='Group 1'>" + + " <option value='G1 O1'>G1 O1</option>" + + " <option value='G1 O2'>G1 O2</option>" + + " <option value='G1 O3'>G1 O3</option>" + + " </optgroup>" + + " <optgroup label='Group 2'>" + + " <option value='G2 O1'>G2 O4</option>" + + " <option value='G2 O2'>G2 O5</option>" + + " <option value='Hidden' style='display: none;'>Hidden</option>" + + " </optgroup>" + + "</select></body></html>"; + +const PAGECONTENT_SOMEHIDDEN = + "<html><head><style>.hidden { display: none; }</style></head>" + + "<body><select id='one'>" + + " <option value='One' style='display: none;'>OneHidden</option>" + + " <option value='Two' class='hidden'>TwoHidden</option>" + + " <option value='Three'>ThreeVisible</option>" + + " <option value='Four'style='display: table;'>FourVisible</option>" + + " <option value='Five'>FiveVisible</option>" + + " <optgroup label='GroupHidden' class='hidden'>" + + " <option value='Four'>Six.OneHidden</option>" + + " <option value='Five' style='display: block;'>Six.TwoHidden</option>" + + " </optgroup>" + + " <option value='Six' class='hidden' style='display: block;'>SevenVisible</option>" + + "</select></body></html>"; + +const PAGECONTENT_TRANSLATED = + "<html><body>" + + "<div id='div'>" + + "<iframe id='frame' width='320' height='295' style='border: none;'" + + " src='data:text/html,<select id=select><option>he he he</option><option>boo boo</option><option>baz baz</option></select>'" + + "</iframe>" + + "</div></body></html>"; + +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()), + args.isWindows ? "Text" : "", + "Select all while popup is open" + ); + } + ); + + // Backspace should not go back + let handleKeyPress = function (event) { + 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: [["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 <select> element than the one that is open is removed + const selectPopup = await openSelectPopup("click", "#one"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + content.document.body.removeChild(content.document.getElementById("two")); + }); + + // Wait a bit just to make sure the popup won't close. + await new Promise(resolve => setTimeout(resolve, 1000)); + + is(selectPopup.state, "open", "Different popup did not affect open popup"); + + await hideSelectPopup(); + + // Next, try it when the same <select> 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); + 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]; + + 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 + // <select> (in that order) when it has changed. + let expectedEnter = [ + { + type: "input", + cancelable: false, + targetIsOption: false, + composed: true, + }, + { + type: "change", + cancelable: false, + targetIsOption: false, + composed: false, + }, + ]; + + let expectedClick = [ + { + type: "mousedown", + cancelable: true, + targetIsOption: true, + composed: true, + }, + { + type: "mouseup", + cancelable: true, + targetIsOption: true, + composed: true, + }, + { + type: "input", + cancelable: false, + targetIsOption: false, + composed: true, + }, + { + type: "change", + cancelable: false, + targetIsOption: false, + composed: false, + }, + { + type: "click", + cancelable: true, + targetIsOption: true, + composed: true, + }, + ]; + + for (let mode of ["enter", "click"]) { + let expected = mode == "enter" ? expectedEnter : expectedClick; + await openSelectPopup("click", mode == "enter" ? "#one" : "#two"); + + let eventsPromise = SpecialPowers.spawn( + browser, + [[mode, expected]], + async function ([contentMode, contentExpected]) { + return new Promise(resolve => { + function onEvent(event) { + select.removeEventListener(event.type, onEvent); + Assert.ok( + contentExpected.length, + "Unexpected event " + event.type + ); + let expectation = contentExpected.shift(); + Assert.equal( + event.type, + expectation.type, + "Expected the right event order" + ); + Assert.ok(event.bubbles, "All of these events should bubble"); + Assert.equal( + event.cancelable, + expectation.cancelable, + "Cancellation property should match" + ); + Assert.equal( + event.target.localName, + expectation.targetIsOption ? "option" : "select", + "Target matches" + ); + Assert.equal( + event.composed, + expectation.composed, + "Composed property should match" + ); + if (!contentExpected.length) { + resolve(); + } + } + + let select = content.document.getElementById( + contentMode == "enter" ? "one" : "two" + ); + for (let event of [ + "input", + "change", + "mousedown", + "mouseup", + "click", + ]) { + select.addEventListener(event, onEvent); + } + }); + } + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + await hideSelectPopup(mode); + await eventsPromise; + } + } + ); +}); + +async function performSelectSearchTests(win) { + let browser = win.gBrowser.selectedBrowser; + await SpecialPowers.spawn(browser, [], async function () { + let doc = content.document; + let select = doc.getElementById("one"); + + for (var i = 0; i < 40; i++) { + select.add(new content.Option("Test" + i)); + } + + select.options[1].selected = true; + select.focus(); + }); + + let selectPopup = await openSelectPopup(false, "select", win); + + let searchElement = selectPopup.querySelector( + ".contentSelectDropdown-searchbox" + ); + searchElement.focus(); + + EventUtils.synthesizeKey("O", {}, win); + is(selectPopup.children[2].hidden, false, "First option should be visible"); + is(selectPopup.children[3].hidden, false, "Second option should be visible"); + + EventUtils.synthesizeKey("3", {}, win); + is(selectPopup.children[2].hidden, true, "First option should be hidden"); + is(selectPopup.children[3].hidden, true, "Second option should be hidden"); + is(selectPopup.children[4].hidden, false, "Third option should be visible"); + + EventUtils.synthesizeKey("Z", {}, win); + is(selectPopup.children[4].hidden, true, "Third option should be hidden"); + is( + selectPopup.children[1].hidden, + true, + "First group header should be hidden" + ); + + EventUtils.synthesizeKey("KEY_Backspace", {}, win); + is(selectPopup.children[4].hidden, false, "Third option should be visible"); + + EventUtils.synthesizeKey("KEY_Backspace", {}, win); + is( + selectPopup.children[5].hidden, + false, + "Second group header should be visible" + ); + + EventUtils.synthesizeKey("KEY_Backspace", {}, win); + EventUtils.synthesizeKey("O", {}, win); + EventUtils.synthesizeKey("5", {}, win); + is( + selectPopup.children[5].hidden, + false, + "Second group header should be visible" + ); + is( + selectPopup.children[1].hidden, + true, + "First group header should be hidden" + ); + + EventUtils.synthesizeKey("KEY_Backspace", {}, win); + is( + selectPopup.children[1].hidden, + false, + "First group header should be shown" + ); + + EventUtils.synthesizeKey("KEY_Backspace", {}, win); + is( + selectPopup.children[8].hidden, + true, + "Option hidden by content should remain hidden" + ); + + await hideSelectPopup("escape", win); +} + +// This test checks the functionality of search in select elements with groups +// and a large number of options. +add_task(async function test_select_search() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.forms.selectSearch", true]], + }); + const pageUrl = "data:text/html," + escape(PAGECONTENT_GROUPS); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + + await performSelectSearchTests(window); + + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); +}); + +// This test checks that a mousemove event is fired correctly at the menu and +// not at the browser, ensuring that any mouse capture has been cleared. +add_task(async function test_mousemove_correcttarget() { + const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + + const selectPopup = await openSelectPopup("mousedown"); + + await new Promise(resolve => { + window.addEventListener( + "mousemove", + function (event) { + is(event.target.localName.indexOf("menu"), 0, "mouse over menu"); + resolve(); + }, + { capture: true, once: true } + ); + + EventUtils.synthesizeMouseAtCenter(selectPopup.firstElementChild, { + type: "mousemove", + buttons: 1, + }); + }); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#one", + { type: "mouseup" }, + gBrowser.selectedBrowser + ); + + await hideSelectPopup(); + + // The popup should be closed when fullscreen mode is entered or exited. + for (let steps = 0; steps < 2; steps++) { + await openSelectPopup("click"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + selectPopup, + "popuphidden" + ); + let sizeModeChanged = BrowserTestUtils.waitForEvent( + window, + "sizemodechange" + ); + BrowserFullScreen(); + await sizeModeChanged; + await popupHiddenPromise; + } + + BrowserTestUtils.removeTab(tab); +}); + +// This test checks when a <select> 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 = ` +<!doctype html> +<select> + <option>a</option> + <option>b</option> +</select> +<script> +gChangeEvents = 0; +gInputEvents = 0; +let select = document.querySelector("select"); + select.addEventListener("input", function() { + gInputEvents++; + this.style.display = "none"; + this.getBoundingClientRect(); + }) + select.addEventListener("change", function() { + gChangeEvents++; + }) +</script>`; + + 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 = ` +<!doctype html> +<select> + <option label="Some nifty Label">Some Element Text Instead</option> + <option label="">Element Text</option> +</select> +`; + + 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); +}); diff --git a/browser/base/content/test/forms/browser_selectpopup_colors.js b/browser/base/content/test/forms/browser_selectpopup_colors.js new file mode 100644 index 0000000000..63cece0ce5 --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_colors.js @@ -0,0 +1,867 @@ +const gSelects = { + PAGECONTENT_COLORS: + "<html><head><style>" + + " .blue { color: #fff; background-color: #00f; }" + + " .green { color: #800080; background-color: green; }" + + " .defaultColor { color: -moz-ComboboxText; }" + + " .defaultBackground { background-color: -moz-Combobox; }" + + "</style>" + + "<body><select id='one'>" + + ' <option value="One" style="color: #fff; background-color: #f00;">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgb(255, 0, 0)"}</option>' + + ' <option value="Two" class="blue">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgb(0, 0, 255)"}</option>' + + ' <option value="Three" class="green">{"color": "rgb(128, 0, 128)", "backgroundColor": "rgb(0, 128, 0)"}</option>' + + ' <option value="Four" class="defaultColor defaultBackground">{"color": "-moz-ComboboxText", "backgroundColor": "-moz-Combobox"}</option>' + + ' <option value="Five" class="defaultColor">{"color": "-moz-ComboboxText", "backgroundColor": "rgba(0, 0, 0, 0)", "unstyled": "true"}</option>' + + ' <option value="Six" class="defaultBackground">{"color": "-moz-ComboboxText", "backgroundColor": "-moz-Combobox"}</option>' + + ' <option value="Seven" selected="true">{"unstyled": "true"}</option>' + + "</select></body></html>", + + PAGECONTENT_COLORS_ON_SELECT: + "<html><head><style>" + + " #one { background-color: #7E3A3A; color: #fff }" + + "</style>" + + "<body><select id='one'>" + + ' <option value="One">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option value="Two">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option value="Three">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option value="Four" selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + TRANSPARENT_SELECT: + "<html><head><style>" + + " #one { background-color: transparent; }" + + "</style>" + + "<body><select id='one'>" + + ' <option value="One">{"unstyled": "true"}</option>' + + ' <option value="Two" selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + OPTION_COLOR_EQUAL_TO_UABACKGROUND_COLOR_SELECT: + "<html><head><style>" + + " #one { background-color: black; color: white; }" + + "</style>" + + "<body><select id='one'>" + + ' <option value="One" style="background-color: white; color: black;">{"color": "rgb(0, 0, 0)", "backgroundColor": "rgb(255, 255, 255)"}</option>' + + ' <option value="Two" selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + GENERIC_OPTION_STYLED_AS_IMPORTANT: + "<html><head><style>" + + " option { background-color: black !important; color: white !important; }" + + "</style>" + + "<body><select id='one'>" + + ' <option value="One">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgb(0, 0, 0)"}</option>' + + ' <option value="Two" selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + TRANSLUCENT_SELECT_BECOMES_OPAQUE: + "<html><head>" + + "<body><select id='one' style='background-color: rgba(255,255,255,.55);'>" + + ' <option value="One">{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option value="Two" selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + TRANSLUCENT_SELECT_APPLIES_ON_BASE_COLOR: + "<html><head>" + + "<body><select id='one' style='background-color: rgba(255,0,0,.55);'>" + + ' <option value="One">{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option value="Two" selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + DISABLED_OPTGROUP_AND_OPTIONS: + "<html><head>" + + "<body><select id='one'>" + + " <optgroup label='{\"unstyled\": true}'>" + + ' <option disabled="">{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' + + ' <option>{"unstyled": true}</option>' + + ' <option disabled="">{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' + + ' <option>{"unstyled": true}</option>' + + ' <option>{"unstyled": true}</option>' + + ' <option>{"unstyled": true}</option>' + + ' <option>{"unstyled": true}</option>' + + " </optgroup>" + + ' <optgroup label=\'{"color": "GrayText", "backgroundColor": "-moz-Combobox"}\' disabled=\'\'>' + + ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "-moz-Combobox"}</option>' + + " </optgroup>" + + ' <option value="Two" selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + SELECT_CHANGES_COLOR_ON_FOCUS: + "<html><head><style>" + + " select:focus { background-color: orange; color: black; }" + + "</style></head>" + + "<body><select id='one'>" + + ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + SELECT_BGCOLOR_ON_SELECT_COLOR_ON_OPTIONS: + "<html><head><style>" + + " select { background-color: black; }" + + " option { color: white; }" + + "</style></head>" + + "<body><select id='one'>" + + ' <option>{"colorScheme": "dark", "color": "rgb(255, 255, 255)", "backgroundColor": "rgb(0, 0, 0)"}</option>' + + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + SELECT_STYLE_OF_OPTION_IS_BASED_ON_FOCUS_OF_SELECT: + "<html><head><style>" + + " select:focus { background-color: #3a96dd; }" + + " select:focus option { background-color: #fff; }" + + "</style></head>" + + "<body><select id='one'>" + + ' <option>{"color": "-moz-ComboboxText", "backgroundColor": "rgb(255, 255, 255)"}</option>' + + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + SELECT_STYLE_OF_OPTION_CHANGES_AFTER_FOCUS_EVENT: + "<html><body><select id='one'>" + + ' <option>{"color": "rgb(255, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option selected="true">{"end": "true"}</option>' + + "</select></body><scr" + + "ipt>" + + " var select = document.getElementById('one');" + + " select.addEventListener('focus', () => select.style.color = 'red');" + + "</script></html>", + + SELECT_COLOR_OF_OPTION_CHANGES_AFTER_TRANSITIONEND: + "<html><head><style>" + + " select { transition: all .1s; }" + + " select:focus { background-color: orange; }" + + "</style></head><body><select id='one'>" + + ' <option>{"color": "-moz-ComboboxText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + SELECT_TEXTSHADOW_OF_OPTION_CHANGES_AFTER_TRANSITIONEND: + "<html><head><style>" + + " select { transition: all .1s; }" + + " select:focus { text-shadow: 0 0 0 #303030; }" + + " option { color: red; /* It gets the default otherwise, which is fine but we don't have a good way to test for */ }" + + "</style></head><body><select id='one'>" + + ' <option>{"color": "rgb(255, 0, 0)", "backgroundColor": "-moz-Combobox", "textShadow": "rgb(48, 48, 48) 0px 0px 0px"}</option>' + + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + SELECT_TRANSPARENT_COLOR_WITH_TEXT_SHADOW: + "<html><head><style>" + + " select { color: transparent; text-shadow: 0 0 0 #303030; }" + + "</style></head><body><select id='one'>" + + ' <option>{"color": "rgba(0, 0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)", "textShadow": "rgb(48, 48, 48) 0px 0px 0px"}</option>' + + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>", + + SELECT_LONG_WITH_TRANSITION: + "<html><head><style>" + + " select { transition: all .2s linear; }" + + " select:focus { color: purple; }" + + "</style></head><body><select id='one'>" + + (function () { + let rv = ""; + for (let i = 0; i < 75; i++) { + rv += + ' <option>{"color": "rgb(128, 0, 128)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>'; + } + rv += + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>"; + return rv; + })(), + + SELECT_INHERITED_COLORS_ON_OPTIONS_DONT_GET_UNIQUE_RULES_IF_RULE_SET_ON_SELECT: ` + <html><head><style> + select { color: blue; text-shadow: 1px 1px 2px blue; } + .redColor { color: red; } + .textShadow { text-shadow: 1px 1px 2px black; } + </style></head><body><select id='one'> + <option>{"color": "rgb(0, 0, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option> + <option>{"color": "rgb(0, 0, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option> + <option>{"color": "rgb(0, 0, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option> + <option class="redColor">{"color": "rgb(255, 0, 0)", "backgroundColor": "-moz-Combobox"}</option> + <option class="textShadow">{"color": "rgb(0, 0, 255)", "textShadow": "rgb(0, 0, 0) 1px 1px 2px", "backgroundColor": "rgba(0, 0, 0, 0)"}</option> + <option selected="true">{"end": "true"}</option> + </select></body></html> +`, + + SELECT_FONT_INHERITS_TO_OPTION: ` + <html><head><style> + select { font-family: monospace } + </style></head><body><select id='one'> + <option>One</option> + <option style="font-family: sans-serif">Two</option> + </select></body></html> +`, + + SELECT_SCROLLBAR_PROPS: ` + <html><head><style> + select { scrollbar-width: thin; scrollbar-color: red blue } + </style></head><body><select id='one'> + <option>One</option> + <option style="font-family: sans-serif">Two</option> + </select></body></html> +`, + DEFAULT_DARKMODE: ` + <html><body><select id='one'> + <option>{"unstyled": "true"}</option> + <option>{"unstyled": "true"}</option> + <option selected="true">{"end": "true"}</option> + </select></body></html> +`, + + DEFAULT_DARKMODE_DARK: ` + <meta name=color-scheme content=dark> + <select id='one'> + <option>{"color": "MenuText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option> + <option>{"color": "MenuText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option> + <option selected="true">{"end": "true"}</option> + </select> +`, + + SPLIT_FG_BG_OPTION_DARKMODE: ` + <html><head><style> + select { background-color: #fff; } + option { color: #2b2b2b; } + </style></head><body><select id='one'> + <option>{"color": "rgb(43, 43, 43)", "backgroundColor": "rgb(255, 255, 255)"}</option> + <option>{"color": "rgb(43, 43, 43)", "backgroundColor": "rgb(255, 255, 255)"}</option> + <option selected="true">{"end": "true"}</option> + </select></body></html> +`, + + IDENTICAL_BG_DIFF_FG_OPTION_DARKMODE: ` + <html><head><style> + select { background-color: #fff; } + option { color: #2b2b2b; background-color: #fff; } + </style></head><body><select id='one'> + <option>{"colorScheme": "light", "color": "rgb(43, 43, 43)", "backgroundColor": "rgb(255, 255, 255)"}</option> + <option>{"colorScheme": "light", "color": "rgb(43, 43, 43)", "backgroundColor": "rgb(255, 255, 255)"}</option> + <option selected="true">{"end": "true"}</option> + </select></body></html> +`, +}; + +function rgbaToString(parsedColor) { + let { r, g, b, a } = parsedColor; + if (a == 1) { + return `rgb(${r}, ${g}, ${b})`; + } + return `rgba(${r}, ${g}, ${b}, ${a})`; +} + +function testOptionColors(test, index, item, menulist) { + // The label contains a JSON string of the expected colors for + // `color` and `background-color`. + let expected = JSON.parse(item.label); + + // Press Down to move the selected item to the next item in the + // list and check the colors of this item when it's not selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + + if (expected.end) { + return; + } + + if (expected.unstyled) { + ok( + !item.hasAttribute("customoptionstyling"), + `${test}: Item ${index} should not have any custom option styling: ${item.outerHTML}` + ); + } else { + is( + getComputedStyle(item).color, + expected.color, + `${test}: Item ${index} has correct foreground color` + ); + is( + getComputedStyle(item).backgroundColor, + expected.backgroundColor, + `${test}: Item ${index} has correct background color` + ); + if (expected.textShadow) { + is( + getComputedStyle(item).textShadow, + expected.textShadow, + `${test}: Item ${index} has correct text-shadow color` + ); + } + } +} + +function computeLabels(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], function () { + function _rgbaToString(parsedColor) { + let { r, g, b, a } = parsedColor; + if (a == 1) { + return `rgb(${r}, ${g}, ${b})`; + } + return `rgba(${r}, ${g}, ${b}, ${a})`; + } + function computeColors(expected) { + let any = false; + for (let color of Object.keys(expected)) { + if ( + color != "colorScheme" && + color.toLowerCase().includes("color") && + !expected[color].startsWith("rgb") + ) { + any = true; + expected[color] = _rgbaToString( + InspectorUtils.colorToRGBA(expected[color], content.document) + ); + } + } + return any; + } + for (let option of content.document.querySelectorAll("option,optgroup")) { + if (!option.label) { + continue; + } + let expected; + try { + expected = JSON.parse(option.label); + } catch (ex) { + continue; + } + if (computeColors(expected)) { + option.label = JSON.stringify(expected); + } + } + }); +} + +async function openSelectPopup(select) { + const pageUrl = "data:text/html," + escape(select); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + + await computeLabels(tab); + + let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#one", + { type: "mousedown" }, + gBrowser.selectedBrowser + ); + let selectPopup = await popupShownPromise; + let menulist = selectPopup.parentNode; + return { tab, menulist, selectPopup }; +} + +async function testSelectColors(selectID, itemCount, options) { + let select = gSelects[selectID]; + let { tab, menulist, selectPopup } = await openSelectPopup(select); + if (options.unstyled) { + ok( + !selectPopup.hasAttribute("customoptionstyling"), + `Shouldn't have custom option styling for ${selectID}` + ); + } + let arrowSB = selectPopup.shadowRoot.querySelector( + ".menupopup-arrowscrollbox" + ); + if (options.waitForComputedStyle) { + let property = options.waitForComputedStyle.property; + let expectedValue = options.waitForComputedStyle.value; + await TestUtils.waitForCondition(() => { + let node = ["background-image", "background-color"].includes(property) + ? arrowSB + : selectPopup; + let value = getComputedStyle(node).getPropertyValue(property); + info(`<${node.localName}> has ${property}: ${value}`); + return value == expectedValue; + }, `${selectID} - Waiting for <select> to have ${property}: ${expectedValue}`); + } + + is(selectPopup.parentNode.itemCount, itemCount, "Correct number of items"); + let child = selectPopup.firstElementChild; + let idx = 1; + + if (typeof options.skipSelectColorTest != "object") { + let skip = !!options.skipSelectColorTest; + options.skipSelectColorTest = { + color: skip, + background: skip, + }; + } + if (!options.skipSelectColorTest.color) { + is( + getComputedStyle(arrowSB).color, + options.selectColor, + selectID + " popup has expected foreground color" + ); + } + + if (options.selectTextShadow) { + is( + getComputedStyle(selectPopup).textShadow, + options.selectTextShadow, + selectID + " popup has expected text-shadow color" + ); + } + + if (!options.skipSelectColorTest.background) { + // Combine the select popup's backgroundColor and the + // backgroundImage color to get the color that is seen by + // the user. + let base = getComputedStyle(arrowSB).backgroundColor; + if (base == "rgba(0, 0, 0, 0)") { + base = getComputedStyle(selectPopup).backgroundColor; + } + info("Parsing background color: " + base); + let [, /* unused */ bR, bG, bB] = base.match(/rgb\((\d+), (\d+), (\d+)\)/); + bR = parseInt(bR, 10); + bG = parseInt(bG, 10); + bB = parseInt(bB, 10); + let topCoat = getComputedStyle(arrowSB).backgroundImage; + if (topCoat == "none") { + is( + `rgb(${bR}, ${bG}, ${bB})`, + options.selectBgColor, + selectID + " popup has expected background color (top coat)" + ); + } else { + let [, , /* unused */ /* unused */ tR, tG, tB, tA] = topCoat.match( + /(rgba?\((\d+), (\d+), (\d+)(?:, (0\.\d+))?\)), \1/ + ); + tR = parseInt(tR, 10); + tG = parseInt(tG, 10); + tB = parseInt(tB, 10); + tA = parseFloat(tA) || 1; + let actualR = Math.round(tR * tA + bR * (1 - tA)); + let actualG = Math.round(tG * tA + bG * (1 - tA)); + let actualB = Math.round(tB * tA + bB * (1 - tA)); + is( + `rgb(${actualR}, ${actualG}, ${actualB})`, + options.selectBgColor, + selectID + " popup has expected background color (no top coat)" + ); + } + } + + ok(!child.selected, "The first child should not be selected"); + while (child) { + testOptionColors(selectID, idx, child, menulist); + idx++; + child = child.nextElementSibling; + } + + if (!options.leaveOpen) { + await hideSelectPopup("escape"); + BrowserTestUtils.removeTab(tab); + } +} + +// System colors may be different in content pages and chrome pages. +let kDefaultSelectStyles = {}; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.forms.select.customstyling", true]], + }); + kDefaultSelectStyles = await BrowserTestUtils.withNewTab( + `data:text/html,<select>`, + function (browser) { + return SpecialPowers.spawn(browser, [], function () { + let cs = content.getComputedStyle( + content.document.querySelector("select") + ); + return { + backgroundColor: cs.backgroundColor, + }; + }); + } + ); +}); + +// This test checks when a <select> element has styles applied to <option>s within it. +add_task(async function test_colors_applied_to_popup_items() { + await testSelectColors("PAGECONTENT_COLORS", 7, { + skipSelectColorTest: true, + }); +}); + +// This test checks when a <select> element has styles applied to itself. +add_task(async function test_colors_applied_to_popup() { + let options = { + selectColor: "rgb(255, 255, 255)", + selectBgColor: "rgb(126, 58, 58)", + }; + await testSelectColors("PAGECONTENT_COLORS_ON_SELECT", 4, options); +}); + +// This test checks when a <select> element has a transparent background applied to itself. +add_task(async function test_transparent_applied_to_popup() { + let options = { + unstyled: true, + skipSelectColorTest: true, + }; + await testSelectColors("TRANSPARENT_SELECT", 2, options); +}); + +// This test checks when a <select> element has a background set, and the +// options have their own background set which is equal to the default +// user-agent background color, but should be used because the select +// background color has been changed. +add_task(async function test_options_inverted_from_select_background() { + // The popup has a black background and white text, but the + // options inside of it have flipped the colors. + let options = { + selectColor: "rgb(255, 255, 255)", + selectBgColor: "rgb(0, 0, 0)", + }; + await testSelectColors( + "OPTION_COLOR_EQUAL_TO_UABACKGROUND_COLOR_SELECT", + 2, + options + ); +}); + +// This test checks when a <select> element has a background set using !important, +// which was affecting how we calculated the user-agent styling. +add_task(async function test_select_background_using_important() { + await testSelectColors("GENERIC_OPTION_STYLED_AS_IMPORTANT", 2, { + skipSelectColorTest: true, + }); +}); + +// This test checks when a <select> element has a background set, and the +// options have their own background set which is equal to the default +// user-agent background color, but should be used because the select +// background color has been changed. +add_task(async function test_translucent_select_becomes_opaque() { + // The popup is requested to show a translucent background + // but we apply the requested background color on the system's base color. + let options = { + selectColor: "rgb(0, 0, 0)", + selectBgColor: "rgb(255, 255, 255)", + }; + await testSelectColors("TRANSLUCENT_SELECT_BECOMES_OPAQUE", 2, options); +}); + +// This test checks when a popup has a translucent background color, +// and that the color painted to the screen of the translucent background +// matches what the user expects. +add_task(async function test_translucent_select_applies_on_base_color() { + // The popup is requested to show a translucent background + // but we apply the requested background color on the system's base color. + let options = { + selectColor: "rgb(0, 0, 0)", + selectBgColor: "rgb(255, 115, 115)", + }; + await testSelectColors( + "TRANSLUCENT_SELECT_APPLIES_ON_BASE_COLOR", + 2, + options + ); +}); + +add_task(async function test_disabled_optgroup_and_options() { + await testSelectColors("DISABLED_OPTGROUP_AND_OPTIONS", 17, { + skipSelectColorTest: true, + }); +}); + +add_task(async function test_disabled_optgroup_and_options() { + let options = { + selectColor: "rgb(0, 0, 0)", + selectBgColor: "rgb(255, 165, 0)", + }; + + await testSelectColors("SELECT_CHANGES_COLOR_ON_FOCUS", 2, options); +}); + +add_task(async function test_bgcolor_on_select_color_on_options() { + let options = { + selectColor: "rgb(0, 0, 0)", + selectBgColor: "rgb(0, 0, 0)", + }; + + await testSelectColors( + "SELECT_BGCOLOR_ON_SELECT_COLOR_ON_OPTIONS", + 2, + options + ); +}); + +add_task( + async function test_style_of_options_is_dependent_on_focus_of_select() { + let options = { + selectColor: "rgb(0, 0, 0)", + selectBgColor: "rgb(58, 150, 221)", + }; + + await testSelectColors( + "SELECT_STYLE_OF_OPTION_IS_BASED_ON_FOCUS_OF_SELECT", + 2, + options + ); + } +); + +add_task( + async function test_style_of_options_is_dependent_on_focus_of_select_after_event() { + let options = { + skipSelectColorTest: true, + waitForComputedStyle: { + property: "--panel-color", + value: "rgb(255, 0, 0)", + }, + }; + await testSelectColors( + "SELECT_STYLE_OF_OPTION_CHANGES_AFTER_FOCUS_EVENT", + 2, + options + ); + } +); + +add_task(async function test_color_of_options_is_dependent_on_transitionend() { + let options = { + selectColor: "rgb(0, 0, 0)", + selectBgColor: "rgb(255, 165, 0)", + waitForComputedStyle: { + property: "background-image", + value: "linear-gradient(rgb(255, 165, 0), rgb(255, 165, 0))", + }, + }; + + await testSelectColors( + "SELECT_COLOR_OF_OPTION_CHANGES_AFTER_TRANSITIONEND", + 2, + options + ); +}); + +add_task( + async function test_textshadow_of_options_is_dependent_on_transitionend() { + let options = { + skipSelectColorTest: true, + waitForComputedStyle: { + property: "text-shadow", + value: "rgb(48, 48, 48) 0px 0px 0px", + }, + }; + + await testSelectColors( + "SELECT_TEXTSHADOW_OF_OPTION_CHANGES_AFTER_TRANSITIONEND", + 2, + options + ); + } +); + +add_task(async function test_transparent_color_with_text_shadow() { + let options = { + selectColor: "rgba(0, 0, 0, 0)", + selectTextShadow: "rgb(48, 48, 48) 0px 0px 0px", + selectBgColor: kDefaultSelectStyles.backgroundColor, + }; + + await testSelectColors( + "SELECT_TRANSPARENT_COLOR_WITH_TEXT_SHADOW", + 2, + options + ); +}); + +add_task( + async function test_select_with_transition_doesnt_lose_scroll_position() { + let options = { + selectColor: "rgb(128, 0, 128)", + selectBgColor: kDefaultSelectStyles.backgroundColor, + waitForComputedStyle: { + property: "--panel-color", + value: "rgb(128, 0, 128)", + }, + leaveOpen: true, + }; + + await testSelectColors("SELECT_LONG_WITH_TRANSITION", 76, options); + + let selectPopup = document.getElementById( + "ContentSelectDropdown" + ).menupopup; + let scrollBox = selectPopup.scrollBox; + is( + scrollBox.scrollTop, + scrollBox.scrollTopMax, + "The popup should be scrolled to the bottom of the list (where the selected item is)" + ); + + await hideSelectPopup("escape"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +); + +add_task( + async function test_select_inherited_colors_on_options_dont_get_unique_rules_if_rule_set_on_select() { + let options = { + selectColor: "rgb(0, 0, 255)", + selectTextShadow: "rgb(0, 0, 255) 1px 1px 2px", + selectBgColor: kDefaultSelectStyles.backgroundColor, + leaveOpen: true, + }; + + await testSelectColors( + "SELECT_INHERITED_COLORS_ON_OPTIONS_DONT_GET_UNIQUE_RULES_IF_RULE_SET_ON_SELECT", + 6, + options + ); + + let stylesheetEl = document.getElementById( + "ContentSelectDropdownStylesheet" + ); + + let sheet = stylesheetEl.sheet; + /* Check that the rules are what we expect: There are three different option styles (even though there are 6 options, plus the select rules). */ + let expectedSelectors = [ + "#ContentSelectDropdown .ContentSelectDropdown-item-0", + "#ContentSelectDropdown .ContentSelectDropdown-item-1", + '#ContentSelectDropdown .ContentSelectDropdown-item-1:not([_moz-menuactive="true"])', + "#ContentSelectDropdown .ContentSelectDropdown-item-2", + '#ContentSelectDropdown .ContentSelectDropdown-item-2:not([_moz-menuactive="true"])', + '#ContentSelectDropdown > menupopup > :is(menuitem, menucaption):not([_moz-menuactive="true"])', + '#ContentSelectDropdown > menupopup > :is(menuitem, menucaption)[_moz-menuactive="true"]', + ].sort(); + + let actualSelectors = [...sheet.cssRules].map(r => r.selectorText).sort(); + is( + actualSelectors.length, + expectedSelectors.length, + "Should have the expected number of rules" + ); + for (let i = 0; i < expectedSelectors.length; ++i) { + is( + actualSelectors[i], + expectedSelectors[i], + `Selector ${i} should match` + ); + } + + await hideSelectPopup("escape"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +); + +add_task(async function test_select_font_inherits_to_option() { + let { tab, menulist, selectPopup } = await openSelectPopup( + gSelects.SELECT_FONT_INHERITS_TO_OPTION + ); + + let popupFont = getComputedStyle(selectPopup).fontFamily; + let items = menulist.querySelectorAll("menuitem"); + is(items.length, 2, "Should have two options"); + let firstItemFont = getComputedStyle(items[0]).fontFamily; + let secondItemFont = getComputedStyle(items[1]).fontFamily; + + is( + popupFont, + firstItemFont, + "First menuitem's font should be inherited from the select" + ); + isnot( + popupFont, + secondItemFont, + "Second menuitem's font should be the author specified one" + ); + + await hideSelectPopup("escape"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_scrollbar_props() { + let { tab, selectPopup } = await openSelectPopup( + gSelects.SELECT_SCROLLBAR_PROPS + ); + + let popupStyle = getComputedStyle(selectPopup); + is(popupStyle.getPropertyValue("--content-select-scrollbar-width"), "thin"); + is(popupStyle.scrollbarColor, "rgb(255, 0, 0) rgb(0, 0, 255)"); + + let scrollBoxStyle = getComputedStyle(selectPopup.scrollBox.scrollbox); + is(scrollBoxStyle.overflow, "auto", "Should be the scrollable box"); + is(scrollBoxStyle.scrollbarWidth, "thin"); + is(scrollBoxStyle.scrollbarColor, "rgb(255, 0, 0) rgb(0, 0, 255)"); + + await hideSelectPopup("escape"); + BrowserTestUtils.removeTab(tab); +}); + +if (AppConstants.platform == "win") { + add_task(async function test_darkmode() { + let lightSelectColor = rgbaToString( + InspectorUtils.colorToRGBA("MenuText", document) + ); + let lightSelectBgColor = rgbaToString( + InspectorUtils.colorToRGBA("Menu", document) + ); + + // Force dark mode: + let darkModeQuery = matchMedia("(prefers-color-scheme: dark)"); + let darkModeChange = BrowserTestUtils.waitForEvent(darkModeQuery, "change"); + await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 1]] }); + await darkModeChange; + + // Determine colours from the main context menu: + let darkSelectColor = rgbaToString( + InspectorUtils.colorToRGBA("MenuText", document) + ); + let darkSelectBgColor = rgbaToString( + InspectorUtils.colorToRGBA("Menu", document) + ); + + isnot(lightSelectColor, darkSelectColor); + isnot(lightSelectBgColor, darkSelectBgColor); + + let { tab } = await openSelectPopup(gSelects.DEFAULT_DARKMODE); + + await testSelectColors("DEFAULT_DARKMODE", 3, { + selectColor: lightSelectColor, + selectBgColor: lightSelectBgColor, + }); + + await hideSelectPopup("escape"); + + await testSelectColors("DEFAULT_DARKMODE_DARK", 3, { + selectColor: darkSelectColor, + selectBgColor: darkSelectBgColor, + }); + + await hideSelectPopup("escape"); + BrowserTestUtils.removeTab(tab); + + ({ tab } = await openSelectPopup( + gSelects.IDENTICAL_BG_DIFF_FG_OPTION_DARKMODE + )); + + // Custom styling on the options enforces using the select styling, too, + // even if it matched the UA style. They'll be overridden on individual + // options where necessary. + await testSelectColors("IDENTICAL_BG_DIFF_FG_OPTION_DARKMODE", 3, { + selectColor: "rgb(0, 0, 0)", + selectBgColor: "rgb(255, 255, 255)", + }); + + await hideSelectPopup("escape"); + BrowserTestUtils.removeTab(tab); + + ({ tab } = await openSelectPopup(gSelects.SPLIT_FG_BG_OPTION_DARKMODE)); + + // Like the previous case, but here the bg colour is defined on the + // select, and the fg colour on the option. The behaviour should be the + // same. + await testSelectColors("SPLIT_FG_BG_OPTION_DARKMODE", 3, { + selectColor: "rgb(0, 0, 0)", + selectBgColor: "rgb(255, 255, 255)", + }); + + await hideSelectPopup("escape"); + BrowserTestUtils.removeTab(tab); + }); +} diff --git a/browser/base/content/test/forms/browser_selectpopup_dir.js b/browser/base/content/test/forms/browser_selectpopup_dir.js new file mode 100644 index 0000000000..aaf4a61fc2 --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_dir.js @@ -0,0 +1,21 @@ +const PAGE = ` +<!doctype html> +<select style="direction: rtl"> + <option>ABC</option> + <option>DEFG</option> +</select> +`; + +add_task(async function () { + const url = "data:text/html," + encodeURI(PAGE); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async function (browser) { + let popup = await openSelectPopup("click"); + is(popup.style.direction, "rtl", "Should be the right dir"); + } + ); +}); diff --git a/browser/base/content/test/forms/browser_selectpopup_hr.js b/browser/base/content/test/forms/browser_selectpopup_hr.js new file mode 100644 index 0000000000..85a44be66c --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_hr.js @@ -0,0 +1,55 @@ +add_task(async function test_hr() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.forms.select.customstyling", true]], + }); + + const PAGE_CONTENT = ` +<!doctype html> +<select> +<option>One</option> +<hr style="color: red; background-color: blue"> +<option>Two</option> +</select>`; + + const pageUrl = "data:text/html," + encodeURIComponent(PAGE_CONTENT); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + + const selectPopup = await openSelectPopup("click"); + const menulist = selectPopup.parentNode; + + const optionOne = selectPopup.children[0]; + const separator = selectPopup.children[1]; + const optionTwo = selectPopup.children[2]; + + is(optionOne.textContent, "One", "First option has expected text content"); + + is(separator.tagName, "menuseparator", "Separator is menuseparator"); + + const separatorStyle = getComputedStyle(separator); + + is( + separatorStyle.color, + "rgb(255, 0, 0)", + "Separator color is specified CSS color" + ); + + is( + separatorStyle.backgroundColor, + "rgba(0, 0, 0, 0)", + "Separator background-color is not set to specified CSS color" + ); + + is(optionTwo.textContent, "Two", "Second option has expected text content"); + + is(menulist.activeChild, optionOne, "First option is selected to start"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + + is( + menulist.activeChild, + optionTwo, + "Second option is selected after arrow down" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/base/content/test/forms/browser_selectpopup_large.js b/browser/base/content/test/forms/browser_selectpopup_large.js new file mode 100644 index 0000000000..722e0d9588 --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_large.js @@ -0,0 +1,323 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PAGECONTENT_SMALL = ` + <!doctype html> + <html> + <body><select id='one'> + <option value='One'>One</option> + <option value='Two'>Two</option> + </select><select id='two'> + <option value='Three'>Three</option> + <option value='Four'>Four</option> + </select><select id='three'> + <option value='Five'>Five</option> + <option value='Six'>Six</option> + </select></body></html> +`; + +async function performLargePopupTests(win) { + let browser = win.gBrowser.selectedBrowser; + + await SpecialPowers.spawn(browser, [], async function () { + let doc = content.document; + let select = doc.getElementById("one"); + for (var i = 0; i < 180; i++) { + select.add(new content.Option("Test" + i)); + } + + select.options[60].selected = true; + select.focus(); + }); + + // Check if a drag-select works and scrolls the list. + const selectPopup = await openSelectPopup("mousedown", "select", win); + const browserRect = browser.getBoundingClientRect(); + + let getScrollPos = () => selectPopup.scrollBox.scrollbox.scrollTop; + let scrollPos = getScrollPos(); + let popupRect = selectPopup.getBoundingClientRect(); + + // First, check that scrolling does not occur when the mouse is moved over the + // anchor button but not the popup yet. + EventUtils.synthesizeMouseAtPoint( + popupRect.left + 5, + popupRect.top - 10, + { + type: "mousemove", + buttons: 1, + }, + win + ); + is( + getScrollPos(), + scrollPos, + "scroll position after mousemove over button should not change" + ); + + EventUtils.synthesizeMouseAtPoint( + popupRect.left + 20, + popupRect.top + 10, + { + type: "mousemove", + buttons: 1, + }, + win + ); + + // Dragging above the popup scrolls it up. + let scrolledPromise = BrowserTestUtils.waitForEvent( + selectPopup, + "scroll", + false, + () => getScrollPos() < scrollPos - 5 + ); + EventUtils.synthesizeMouseAtPoint( + popupRect.left + 20, + popupRect.top - 20, + { + type: "mousemove", + buttons: 1, + }, + win + ); + await scrolledPromise; + ok(true, "scroll position at drag up"); + + // Dragging below the popup scrolls it down. + scrollPos = getScrollPos(); + scrolledPromise = BrowserTestUtils.waitForEvent( + selectPopup, + "scroll", + false, + () => getScrollPos() > scrollPos + 5 + ); + EventUtils.synthesizeMouseAtPoint( + popupRect.left + 20, + popupRect.bottom + 20, + { + type: "mousemove", + buttons: 1, + }, + win + ); + await scrolledPromise; + ok(true, "scroll position at drag down"); + + // Releasing the mouse button and moving the mouse does not change the scroll position. + scrollPos = getScrollPos(); + EventUtils.synthesizeMouseAtPoint( + popupRect.left + 20, + popupRect.bottom + 25, + { type: "mouseup" }, + win + ); + is(getScrollPos(), scrollPos, "scroll position at mouseup should not change"); + + EventUtils.synthesizeMouseAtPoint( + popupRect.left + 20, + popupRect.bottom + 20, + { type: "mousemove" }, + win + ); + is( + getScrollPos(), + scrollPos, + "scroll position at mousemove after mouseup should not change" + ); + + // Now check dragging with a mousedown on an item + let menuRect = selectPopup.children[51].getBoundingClientRect(); + EventUtils.synthesizeMouseAtPoint( + menuRect.left + 5, + menuRect.top + 5, + { type: "mousedown" }, + win + ); + + // Dragging below the popup scrolls it down. + scrolledPromise = BrowserTestUtils.waitForEvent( + selectPopup, + "scroll", + false, + () => getScrollPos() > scrollPos + 5 + ); + EventUtils.synthesizeMouseAtPoint( + popupRect.left + 20, + popupRect.bottom + 20, + { + type: "mousemove", + buttons: 1, + }, + win + ); + await scrolledPromise; + ok(true, "scroll position at drag down from option"); + + // Dragging above the popup scrolls it up. + scrolledPromise = BrowserTestUtils.waitForEvent( + selectPopup, + "scroll", + false, + () => getScrollPos() < scrollPos - 5 + ); + EventUtils.synthesizeMouseAtPoint( + popupRect.left + 20, + popupRect.top - 20, + { + type: "mousemove", + buttons: 1, + }, + win + ); + await scrolledPromise; + ok(true, "scroll position at drag up from option"); + + scrollPos = getScrollPos(); + // We intentionally turn off this a11y check, because the following click + // is sent on an arbitrary web content that is not expected to be tested + // by itself with the browser mochitests, therefore this rule check shall + // be ignored by a11y-checks suite. + AccessibilityUtils.setEnv({ labelRule: false }); + EventUtils.synthesizeMouseAtPoint( + popupRect.left + 20, + popupRect.bottom + 25, + { type: "mouseup" }, + win + ); + AccessibilityUtils.resetEnv(); + is( + getScrollPos(), + scrollPos, + "scroll position at mouseup from option should not change" + ); + + EventUtils.synthesizeMouseAtPoint( + popupRect.left + 20, + popupRect.bottom + 20, + { type: "mousemove" }, + win + ); + is( + getScrollPos(), + scrollPos, + "scroll position at mousemove after mouseup should not change" + ); + + await hideSelectPopup("escape", win); + + let positions = [ + "margin-top: 300px;", + "position: fixed; bottom: 200px;", + "width: 100%; height: 9999px;", + ]; + + let position; + while (positions.length) { + await openSelectPopup("key", "select", win); + + let rect = selectPopup.getBoundingClientRect(); + let marginBottom = parseFloat(getComputedStyle(selectPopup).marginBottom); + let marginTop = parseFloat(getComputedStyle(selectPopup).marginTop); + Assert.greaterOrEqual( + rect.top - marginTop, + browserRect.top, + "Popup top position in within browser area" + ); + Assert.lessOrEqual( + rect.bottom + marginBottom, + browserRect.bottom, + "Popup bottom position in within browser area" + ); + + let cs = win.getComputedStyle(selectPopup); + let csArrow = win.getComputedStyle(selectPopup.scrollBox); + let bpBottom = + parseFloat(cs.paddingBottom) + + parseFloat(cs.borderBottomWidth) + + parseFloat(csArrow.paddingBottom) + + parseFloat(csArrow.borderBottomWidth); + let selectedOption = 60; + + if (Services.prefs.getBoolPref("dom.forms.selectSearch")) { + // Use option 61 instead of 60, as the 60th option element is actually the + // 61st child, since the first child is now the search input field. + selectedOption = 61; + } + // Some of the styles applied to the menuitems are percentages, meaning + // that the final layout calculations returned by getBoundingClientRect() + // might return floating point values. We don't care about sub-pixel + // accuracy, and only care about the final pixel value, so we add a + // fuzz-factor of 1. + const fuzzFactor = 1; + SimpleTest.isfuzzy( + selectPopup.children[selectedOption].getBoundingClientRect().bottom, + selectPopup.getBoundingClientRect().bottom - bpBottom + marginBottom, + fuzzFactor, + "Popup scroll at correct position " + bpBottom + ); + + await hideSelectPopup("enter", win); + + position = positions.shift(); + + let contentPainted = BrowserTestUtils.waitForContentEvent( + browser, + "MozAfterPaint" + ); + await SpecialPowers.spawn( + browser, + [position], + async function (contentPosition) { + let select = content.document.getElementById("one"); + select.setAttribute("style", contentPosition || ""); + select.getBoundingClientRect(); + } + ); + await contentPainted; + } +} + +// This test checks select elements with a large number of options to ensure that +// the popup appears within the browser area. +add_task(async function test_large_popup() { + const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + + await performLargePopupTests(window); + + BrowserTestUtils.removeTab(tab); +}); + +// This test checks the same as the previous test but in a new, vertically smaller window. +add_task(async function test_large_popup_in_small_window() { + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + let resizePromise = BrowserTestUtils.waitForEvent( + newWin, + "resize", + false, + e => { + info(`Got resize event (innerHeight: ${newWin.innerHeight})`); + return newWin.innerHeight <= 450; + } + ); + newWin.resizeTo(600, 450); + await resizePromise; + + const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + newWin.gBrowser.selectedBrowser + ); + BrowserTestUtils.startLoadingURIString( + newWin.gBrowser.selectedBrowser, + pageUrl + ); + await browserLoadedPromise; + + newWin.gBrowser.selectedBrowser.focus(); + + await performLargePopupTests(newWin); + + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/browser/base/content/test/forms/browser_selectpopup_searchfocus.js b/browser/base/content/test/forms/browser_selectpopup_searchfocus.js new file mode 100644 index 0000000000..caae828668 --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_searchfocus.js @@ -0,0 +1,36 @@ +let SELECT = "<html><body><select id='one'>"; +for (let i = 0; i < 75; i++) { + SELECT += ` <option>${i}${i}${i}${i}${i}</option>`; +} +SELECT += + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.forms.selectSearch", true]], + }); +}); + +add_task(async function test_focus_on_search_shouldnt_close_popup() { + const pageUrl = "data:text/html," + escape(SELECT); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + let selectPopup = await openSelectPopup("mousedown"); + + let searchInput = selectPopup.querySelector( + ".contentSelectDropdown-searchbox" + ); + searchInput.scrollIntoView(); + let searchFocused = BrowserTestUtils.waitForEvent(searchInput, "focus", true); + await EventUtils.synthesizeMouseAtCenter(searchInput, {}, window); + await searchFocused; + + is( + selectPopup.state, + "open", + "select popup should still be open after clicking on the search field" + ); + + await hideSelectPopup("escape"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/base/content/test/forms/browser_selectpopup_showPicker.js b/browser/base/content/test/forms/browser_selectpopup_showPicker.js new file mode 100644 index 0000000000..9c978cb411 --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_showPicker.js @@ -0,0 +1,60 @@ +const PAGE = ` +<!doctype html> +<select> + <option>ABC</option> + <option>DEFG</option> +</select> +`; + +add_task(async function test_showPicker() { + const url = "data:text/html," + encodeURI(PAGE); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async function (browser) { + let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window); + + await SpecialPowers.spawn(browser, [], async function () { + content.document.notifyUserGestureActivation(); + content.document.querySelector("select").showPicker(); + }); + + let selectPopup = await popupShownPromise; + is( + selectPopup.state, + "open", + "select popup is open after calling showPicker" + ); + } + ); +}); + +add_task(async function test_showPicker_alreadyOpen() { + const url = "data:text/html," + encodeURI(PAGE); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async function (browser) { + let selectPopup = await openSelectPopup("click"); + + await SpecialPowers.spawn(browser, [], async function () { + content.document.notifyUserGestureActivation(); + content.document.querySelector("select").showPicker(); + }); + + // Wait some time for potential (unwanted) closing. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 100)); + + is( + selectPopup.state, + "open", + "select popup is still open after calling showPicker" + ); + } + ); +}); diff --git a/browser/base/content/test/forms/browser_selectpopup_text_transform.js b/browser/base/content/test/forms/browser_selectpopup_text_transform.js new file mode 100644 index 0000000000..671f39e2a6 --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_text_transform.js @@ -0,0 +1,40 @@ +const PAGE = ` +<!doctype html> +<select style="text-transform: uppercase"> + <option>abc</option> + <option>defg</option> +</select> +`; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.forms.select.customstyling", true]], + }); + const url = "data:text/html," + encodeURI(PAGE); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async function (browser) { + let popup = await openSelectPopup("click"); + let menuitems = popup.querySelectorAll("menuitem"); + is(menuitems[0].textContent, "abc", "Option text should be lowercase"); + is(menuitems[1].textContent, "defg", "Option text should be lowercase"); + + let optionStyle = getComputedStyle(menuitems[0]); + is( + optionStyle.textTransform, + "uppercase", + "Option text should be transformed to uppercase" + ); + + optionStyle = getComputedStyle(menuitems[1]); + is( + optionStyle.textTransform, + "uppercase", + "Option text should be transformed to uppercase" + ); + } + ); +}); diff --git a/browser/base/content/test/forms/browser_selectpopup_toplevel.js b/browser/base/content/test/forms/browser_selectpopup_toplevel.js new file mode 100644 index 0000000000..9b4fcd860a --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_toplevel.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + let select = document.createElement("select"); + select.appendChild(new Option("abc")); + select.appendChild(new Option("defg")); + registerCleanupFunction(() => select.remove()); + document.body.appendChild(select); + let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window); + // We intentionally turn off this a11y check, because the following click + // is sent on an arbitrary web content that is not expected to be tested + // by itself with the browser mochitests, therefore this rule check shall + // be ignored by a11y-checks suite. + AccessibilityUtils.setEnv({ labelRule: false }); + EventUtils.synthesizeMouseAtCenter(select, {}); + AccessibilityUtils.resetEnv(); + + let popup = await popupShownPromise; + ok(!!popup, "Should've shown the popup"); + let items = popup.querySelectorAll("menuitem"); + is(items.length, 2, "Should have two options"); + is(items[0].textContent, "abc", "First option should be correct"); + is(items[1].textContent, "defg", "First option should be correct"); +}); diff --git a/browser/base/content/test/forms/browser_selectpopup_user_input.js b/browser/base/content/test/forms/browser_selectpopup_user_input.js new file mode 100644 index 0000000000..b3cdeaf7e6 --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_user_input.js @@ -0,0 +1,90 @@ +const PAGE = ` +<!doctype html> +<select> + <option>ABC</option> + <option>DEFG</option> +</select> +`; + +function promiseChangeHandlingUserInput(browser) { + return SpecialPowers.spawn(browser, [], async function () { + content.document.clearUserGestureActivation(); + let element = content.document.querySelector("select"); + let reply = {}; + function getUserInputState() { + return { + isHandlingUserInput: content.window.windowUtils.isHandlingUserInput, + hasValidTransientUserGestureActivation: + content.document.hasValidTransientUserGestureActivation, + }; + } + reply.before = getUserInputState(); + await ContentTaskUtils.waitForEvent(element, "change", false, () => { + reply.during = getUserInputState(); + return true; + }); + await new Promise(r => content.window.setTimeout(r)); + reply.after = getUserInputState(); + return reply; + }); +} + +async function testHandlingUserInputOnChange(aTriggerFn) { + const url = "data:text/html," + encodeURI(PAGE); + return BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async function (browser) { + let popup = await openSelectPopup("click"); + let userInputOnChange = promiseChangeHandlingUserInput(browser); + await aTriggerFn(popup); + let userInput = await userInputOnChange; + ok( + !userInput.before.isHandlingUserInput, + "Shouldn't be handling user input before test" + ); + ok( + !userInput.before.hasValidTransientUserGestureActivation, + "transient activation should be cleared before test" + ); + ok( + userInput.during.hasValidTransientUserGestureActivation, + "should provide transient activation during event" + ); + ok( + userInput.during.isHandlingUserInput, + "isHandlingUserInput should be true during event" + ); + ok( + userInput.after.hasValidTransientUserGestureActivation, + "should provide transient activation after event" + ); + ok( + !userInput.after.isHandlingUserInput, + "isHandlingUserInput should be false after event" + ); + } + ); +} + +// This test checks if the change/click event is considered as user input event. +add_task(async function test_handling_user_input_key() { + return testHandlingUserInputOnChange(async function (popup) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + await hideSelectPopup(); + }); +}); + +add_task(async function test_handling_user_input_click() { + return testHandlingUserInputOnChange(async function (popup) { + EventUtils.synthesizeMouseAtCenter(popup.lastElementChild, {}); + }); +}); + +add_task(async function test_handling_user_input_click() { + return testHandlingUserInputOnChange(async function (popup) { + EventUtils.synthesizeMouseAtCenter(popup.lastElementChild, {}); + }); +}); diff --git a/browser/base/content/test/forms/browser_selectpopup_width.js b/browser/base/content/test/forms/browser_selectpopup_width.js new file mode 100644 index 0000000000..d8f748fb18 --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_width.js @@ -0,0 +1,49 @@ +const PAGE = ` +<!doctype html> +<select style="width: 600px"> + <option>ABC</option> + <option>DEFG</option> +</select> +`; + +function tick() { + return new Promise(r => + requestAnimationFrame(() => requestAnimationFrame(r)) + ); +} + +add_task(async function () { + const url = "data:text/html," + encodeURI(PAGE); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async function (browser) { + let popup = await openSelectPopup("click"); + let arrowSB = popup.shadowRoot.querySelector(".menupopup-arrowscrollbox"); + is( + arrowSB.getBoundingClientRect().width, + 600, + "Should be the right size" + ); + + // Trigger a layout change that would cause us to layout the popup again, + // and change our menulist to be zero-size so that the anchor rect + // codepath is used. We should still use the anchor rect to expand our + // size. + await tick(); + + popup.closest("menulist").style.width = "0"; + popup.style.minWidth = "2px"; + + await tick(); + + is( + arrowSB.getBoundingClientRect().width, + 600, + "Should be the right size" + ); + } + ); +}); diff --git a/browser/base/content/test/forms/browser_selectpopup_xhtml.js b/browser/base/content/test/forms/browser_selectpopup_xhtml.js new file mode 100644 index 0000000000..091649be89 --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_xhtml.js @@ -0,0 +1,36 @@ +const PAGE = `<?xml version="1.0"?> +<html id="main-window" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.w3.org/1999/xhtml"> +<head/> +<body> + <html:select> + <html:option>abc</html:option> + <html:optgroup> + <html:option>defg</html:option> + </html:optgroup> + </html:select> +</body> +</html> +`; + +add_task(async function () { + const url = "data:application/xhtml+xml," + encodeURI(PAGE); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async function (browser) { + let popup = await openSelectPopup("click"); + let menuitems = popup.querySelectorAll("menuitem"); + is(menuitems.length, 2, "Should've properly detected two menu items"); + is(menuitems[0].textContent, "abc", "Option text should be correct"); + is(menuitems[1].textContent, "defg", "Second text should be correct"); + ok( + !!popup.querySelector("menucaption"), + "Should've created a caption for the optgroup" + ); + } + ); +}); diff --git a/browser/base/content/test/forms/head.js b/browser/base/content/test/forms/head.js new file mode 100644 index 0000000000..1629c6a57c --- /dev/null +++ b/browser/base/content/test/forms/head.js @@ -0,0 +1,51 @@ +async function openSelectPopup( + mode = "key", + selector = "select", + win = window +) { + info("Opening select popup"); + let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(win); + if (mode == "click" || mode == "mousedown") { + let mousePromise; + if (mode == "click") { + mousePromise = BrowserTestUtils.synthesizeMouseAtCenter( + selector, + {}, + win.gBrowser.selectedBrowser + ); + } else { + mousePromise = BrowserTestUtils.synthesizeMouse( + selector, + 5, + 5, + { type: "mousedown" }, + win.gBrowser.selectedBrowser + ); + } + await mousePromise; + } else { + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win); + } + return popupShownPromise; +} + +function hideSelectPopup(mode = "enter", win = window) { + let browser = win.gBrowser.selectedBrowser; + let selectClosedPromise = SpecialPowers.spawn(browser, [], async function () { + let { SelectContentHelper } = ChromeUtils.importESModule( + "resource://gre/actors/SelectChild.sys.mjs" + ); + return ContentTaskUtils.waitForCondition(() => !SelectContentHelper.open); + }); + + if (mode == "escape") { + EventUtils.synthesizeKey("KEY_Escape", {}, win); + } else if (mode == "enter") { + EventUtils.synthesizeKey("KEY_Enter", {}, win); + } else if (mode == "click") { + let popup = win.document.getElementById("ContentSelectDropdown").menupopup; + EventUtils.synthesizeMouseAtCenter(popup.lastElementChild, {}, win); + } + + return selectClosedPromise; +} |