summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/forms/browser_selectpopup.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/test/forms/browser_selectpopup.js')
-rw-r--r--browser/base/content/test/forms/browser_selectpopup.js1337
1 files changed, 1337 insertions, 0 deletions
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..89cbaea664
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup.js
@@ -0,0 +1,1337 @@
+/* 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.
+
+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 openSelectPopup(
+ selectPopup,
+ mode = "key",
+ selector = "select",
+ win = window
+) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popupshown"
+ );
+
+ 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
+ );
+ }
+
+ return Promise.all([popupShownPromise, mousePromise]);
+ }
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win);
+ return popupShownPromise;
+}
+
+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 menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ await openSelectPopup(selectPopup);
+
+ 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(selectPopup);
+
+ 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(selectPopup, "click");
+ await hideSelectPopup(selectPopup, "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(selectPopup, "click");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await hideSelectPopup(selectPopup, "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_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.select_popup_in_parent.enabled", true],
+ ["dom.forms.select.customstyling", true],
+ ],
+ });
+});
+
+add_task(async function() {
+ await doSelectTests("text/html", PAGECONTENT);
+});
+
+add_task(async function() {
+ await doSelectTests("application/xhtml+xml", XHTML_DTD + "\n" + PAGECONTENT);
+});
+
+add_task(async function() {
+ await doSelectTests("application/xml", XHTML_DTD + "\n" + PAGECONTENT_XSLT);
+});
+
+// This test opens a select popup and removes the content node of a popup while
+// The popup should close if its node is removed.
+add_task(async function() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // First, try it when a different <select> element than the one that is open is removed
+ await openSelectPopup(selectPopup, "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(selectPopup);
+
+ // Next, try it when the same <select> element than the one that is open is removed
+ await openSelectPopup(selectPopup, "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(selectPopup, "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;
+ });
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // First, get the position of the select popup when no translations have been applied.
+ await openSelectPopup(selectPopup);
+
+ let rect = selectPopup.getBoundingClientRect();
+ let expectedX = rect.left;
+ let expectedY = rect.top;
+
+ await hideSelectPopup(selectPopup);
+
+ // 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(selectPopup);
+
+ 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(selectPopup);
+ }
+
+ 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) {
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // 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: false,
+ },
+ {
+ 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: false,
+ },
+ {
+ 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(
+ selectPopup,
+ "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(selectPopup, mode);
+ await eventsPromise;
+ }
+ }
+ );
+});
+
+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();
+ });
+
+ let selectPopup = win.document.getElementById("ContentSelectDropdown")
+ .menupopup;
+ let browserRect = browser.getBoundingClientRect();
+
+ // Check if a drag-select works and scrolls the list.
+ await openSelectPopup(selectPopup, "mousedown", "select", win);
+
+ 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" },
+ win
+ );
+ is(
+ getScrollPos(),
+ scrollPos,
+ "scroll position after mousemove over button should not change"
+ );
+
+ EventUtils.synthesizeMouseAtPoint(
+ popupRect.left + 20,
+ popupRect.top + 10,
+ { type: "mousemove" },
+ 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" },
+ 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" },
+ 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" },
+ 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" },
+ 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(selectPopup, "escape", win);
+
+ let positions = [
+ "margin-top: 300px;",
+ "position: fixed; bottom: 200px;",
+ "width: 100%; height: 9999px;",
+ ];
+
+ let position;
+ while (positions.length) {
+ await openSelectPopup(selectPopup, "key", "select", win);
+
+ let rect = selectPopup.getBoundingClientRect();
+ ok(
+ rect.top >= browserRect.top,
+ "Popup top position in within browser area"
+ );
+ ok(
+ rect.bottom <= browserRect.bottom,
+ "Popup bottom position in within browser area"
+ );
+
+ // Don't check the scroll position for the last step as the popup will be cut off.
+ if (positions.length) {
+ let cs = win.getComputedStyle(selectPopup);
+ let bpBottom =
+ parseFloat(cs.paddingBottom) + parseFloat(cs.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.
+ SimpleTest.isfuzzy(
+ selectPopup.children[selectedOption].getBoundingClientRect().bottom,
+ selectPopup.getBoundingClientRect().bottom - bpBottom,
+ 1,
+ "Popup scroll at correct position " + bpBottom
+ );
+ }
+
+ await hideSelectPopup(selectPopup, "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(selectPopup, "key", "select", win);
+
+ ok(
+ selectPopup.getBoundingClientRect().top >
+ browser.getBoundingClientRect().top,
+ "select popup appears over selected item"
+ );
+
+ await hideSelectPopup(selectPopup, "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 <= 400;
+ }
+ );
+ newWin.resizeTo(600, 400);
+ await resizePromise;
+
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ newWin.gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.loadURI(newWin.gBrowser.selectedBrowser, pageUrl);
+ await browserLoadedPromise;
+
+ newWin.gBrowser.selectedBrowser.focus();
+
+ await performLargePopupTests(newWin);
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
+
+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 = win.document.getElementById("ContentSelectDropdown")
+ .menupopup;
+ await openSelectPopup(selectPopup, 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(selectPopup, "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);
+
+ let selectPopup = document.getElementById("ContentSelectDropdown").menupopup;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#one",
+ { type: "mousedown" },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ 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",
+ });
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#one",
+ { type: "mouseup" },
+ gBrowser.selectedBrowser
+ );
+
+ await hideSelectPopup(selectPopup);
+
+ // The popup should be closed when fullscreen mode is entered or exited.
+ for (let steps = 0; steps < 2; steps++) {
+ await openSelectPopup(selectPopup, "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 = document.getElementById("ContentSelectDropdown").menupopup;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#one",
+ { type: "mousedown" },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ // The exact number is not needed; just ensure the height is larger than 4 items to accomodate 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 ? "-moz-box" : "none",
+ "Item " + idx++ + " is visible"
+ );
+ child = child.nextElementSibling;
+ }
+
+ await hideSelectPopup(selectPopup, "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 menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ await openSelectPopup(selectPopup);
+
+ 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);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ info("Opening the popup");
+ await openSelectPopup(selectPopup, "click");
+
+ info("Opened the popup");
+ let nonZoomedFontSize = parseFloat(
+ getComputedStyle(selectPopup.querySelector("menuitem")).fontSize,
+ 10
+ );
+
+ info("font-size is " + nonZoomedFontSize);
+ await hideSelectPopup(selectPopup);
+
+ info("Hid 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(selectPopup, "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(selectPopup);
+ info("Hid the popup again");
+
+ ZoomManager.toggleZoom();
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+function getIsHandlingUserInput(browser, elementId, eventName) {
+ return SpecialPowers.spawn(browser, [[elementId, eventName]], async function([
+ contentElementId,
+ contentEventName,
+ ]) {
+ let element = content.document.getElementById(contentElementId);
+ let isHandlingUserInput = false;
+ await ContentTaskUtils.waitForEvent(element, contentEventName, false, e => {
+ isHandlingUserInput = content.window.windowUtils.isHandlingUserInput;
+ return true;
+ });
+
+ return isHandlingUserInput;
+ });
+}
+
+// This test checks if the change/click event is considered as user input event.
+add_task(async function test_handling_user_input() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // Test onchange event when changing value via keyboard.
+ await openSelectPopup(selectPopup, "click", "#one");
+ let getPromise = getIsHandlingUserInput(tab.linkedBrowser, "one", "change");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await hideSelectPopup(selectPopup);
+ is(await getPromise, true, "isHandlingUserInput should be true");
+
+ // Test onchange event when changing value via mouse click
+ await openSelectPopup(selectPopup, "click", "#two");
+ getPromise = getIsHandlingUserInput(tab.linkedBrowser, "two", "change");
+ EventUtils.synthesizeMouseAtCenter(selectPopup.lastElementChild, {});
+ is(await getPromise, true, "isHandlingUserInput should be true");
+
+ // Test onclick event fired from clicking select popup.
+ await openSelectPopup(selectPopup, "click", "#three");
+ getPromise = getIsHandlingUserInput(tab.linkedBrowser, "three", "click");
+ EventUtils.synthesizeMouseAtCenter(selectPopup.firstElementChild, {});
+ is(await getPromise, true, "isHandlingUserInput should be true");
+
+ 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);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // Test change and input events get handled consistently
+ await openSelectPopup(selectPopup, "click");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await hideSelectPopup(selectPopup);
+
+ 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);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ await openSelectPopup(selectPopup, "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);
+});