summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/forms
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/test/forms')
-rw-r--r--browser/base/content/test/forms/browser.ini21
-rw-r--r--browser/base/content/test/forms/browser_selectpopup.js913
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_colors.js867
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_dir.js21
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_large.js338
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_searchfocus.js36
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_text_transform.js40
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_toplevel.js19
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_user_input.js90
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_width.js49
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_xhtml.js36
-rw-r--r--browser/base/content/test/forms/head.js51
12 files changed, 2481 insertions, 0 deletions
diff --git a/browser/base/content/test/forms/browser.ini b/browser/base/content/test/forms/browser.ini
new file mode 100644
index 0000000000..00c6cfe951
--- /dev/null
+++ b/browser/base/content/test/forms/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+prefs =
+ gfx.font_loader.delay=0
+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_large.js]
+[browser_selectpopup_searchfocus.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..7645f89b4a
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup.js
@@ -0,0 +1,913 @@
+/* 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'>&nbsp;&nbsp;Eight&nbsp;&nbsp;</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.
+ ok(
+ selectPopup.getBoundingClientRect().height >=
+ selectPopup.lastElementChild.getBoundingClientRect().height * 4,
+ "Height contains at least 4 items"
+ );
+ ok(
+ 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);
+
+ ok(
+ 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..00b399c672
--- /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.isPlatformAndVersionAtLeast("win", "10")) {
+ 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_large.js b/browser/base/content/test/forms/browser_selectpopup_large.js
new file mode 100644
index 0000000000..0c88755b27
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_large.js
@@ -0,0 +1,338 @@
+/* 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();
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.bottom + 25,
+ { type: "mouseup" },
+ win
+ );
+ 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);
+ ok(
+ rect.top - marginTop >= browserRect.top,
+ "Popup top position in within browser area"
+ );
+ ok(
+ 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.
+ //
+ // FIXME(emilio): In win7 scroll position is off by 20px more, but that's
+ // not reproducible in win10 even with the win7 "native" menus enabled.
+ const fuzzFactor = matchMedia("(-moz-platform: windows-win7)").matches
+ ? 21
+ : 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;
+ }
+
+ if (navigator.platform.indexOf("Mac") == 0) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ doc.body.style = "padding-top: 400px;";
+
+ let select = doc.getElementById("one");
+ select.options[41].selected = true;
+ select.focus();
+ });
+
+ await openSelectPopup("key", "select", win);
+
+ ok(
+ selectPopup.getBoundingClientRect().top >
+ browser.getBoundingClientRect().top,
+ "select popup appears over selected item"
+ );
+
+ await hideSelectPopup("escape", win);
+ }
+}
+
+// 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.loadURIString(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_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..85a77ea676
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_toplevel.js
@@ -0,0 +1,19 @@
+/* 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);
+ EventUtils.synthesizeMouseAtCenter(select, {});
+
+ 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;
+}