diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /browser/base/content/test/forms | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/base/content/test/forms')
-rw-r--r-- | browser/base/content/test/forms/.eslintrc.js | 5 | ||||
-rw-r--r-- | browser/base/content/test/forms/browser.ini | 12 | ||||
-rw-r--r-- | browser/base/content/test/forms/browser_selectpopup.js | 1337 | ||||
-rw-r--r-- | browser/base/content/test/forms/browser_selectpopup_colors.js | 683 | ||||
-rw-r--r-- | browser/base/content/test/forms/browser_selectpopup_searchfocus.js | 52 | ||||
-rw-r--r-- | browser/base/content/test/forms/head.js | 20 |
6 files changed, 2109 insertions, 0 deletions
diff --git a/browser/base/content/test/forms/.eslintrc.js b/browser/base/content/test/forms/.eslintrc.js new file mode 100644 index 0000000000..1779fd7f1c --- /dev/null +++ b/browser/base/content/test/forms/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/browser-test"], +}; diff --git a/browser/base/content/test/forms/browser.ini b/browser/base/content/test/forms/browser.ini new file mode 100644 index 0000000000..e9a474a10e --- /dev/null +++ b/browser/base/content/test/forms/browser.ini @@ -0,0 +1,12 @@ +[DEFAULT] +prefs = + gfx.font_loader.delay=0 + gfx.font_loader.interval=0 +support-files = + head.js + +[browser_selectpopup.js] +skip-if = os == "linux" || (os == "mac" && !debug) || (verify && (os == 'win')) # Bug 1329991 - linux, Bug 1661132 - osx +[browser_selectpopup_colors.js] +skip-if = os == "linux" # Bug 1329991 - test fails intermittently on Linux builds +[browser_selectpopup_searchfocus.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..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'> Eight </option>" + + " </optgroup></select><input />Text" + + "</body></html>"; + +const PAGECONTENT_XSLT = + "<?xml-stylesheet type='text/xml' href='#style1'?>" + + "<xsl:stylesheet id='style1'" + + " version='1.0'" + + " xmlns:xsl='http://www.w3.org/1999/XSL/Transform'" + + " xmlns:html='http://www.w3.org/1999/xhtml'>" + + "<xsl:template match='xsl:stylesheet'>" + + PAGECONTENT + + "</xsl:template>" + + "</xsl:stylesheet>"; + +const PAGECONTENT_SMALL = + "<html>" + + "<body><select id='one'>" + + " <option value='One'>One</option>" + + " <option value='Two'>Two</option>" + + "</select><select id='two'>" + + " <option value='Three'>Three</option>" + + " <option value='Four'>Four</option>" + + "</select><select id='three'>" + + " <option value='Five'>Five</option>" + + " <option value='Six'>Six</option>" + + "</select></body></html>"; + +const PAGECONTENT_GROUPS = + "<html>" + + "<body><select id='one'>" + + " <optgroup label='Group 1'>" + + " <option value='G1 O1'>G1 O1</option>" + + " <option value='G1 O2'>G1 O2</option>" + + " <option value='G1 O3'>G1 O3</option>" + + " </optgroup>" + + " <optgroup label='Group 2'>" + + " <option value='G2 O1'>G2 O4</option>" + + " <option value='G2 O2'>G2 O5</option>" + + " <option value='Hidden' style='display: none;'>Hidden</option>" + + " </optgroup>" + + "</select></body></html>"; + +const PAGECONTENT_SOMEHIDDEN = + "<html><head><style>.hidden { display: none; }</style></head>" + + "<body><select id='one'>" + + " <option value='One' style='display: none;'>OneHidden</option>" + + " <option value='Two' class='hidden'>TwoHidden</option>" + + " <option value='Three'>ThreeVisible</option>" + + " <option value='Four'style='display: table;'>FourVisible</option>" + + " <option value='Five'>FiveVisible</option>" + + " <optgroup label='GroupHidden' class='hidden'>" + + " <option value='Four'>Six.OneHidden</option>" + + " <option value='Five' style='display: block;'>Six.TwoHidden</option>" + + " </optgroup>" + + " <option value='Six' class='hidden' style='display: block;'>SevenVisible</option>" + + "</select></body></html>"; + +const PAGECONTENT_TRANSLATED = + "<html><body>" + + "<div id='div'>" + + "<iframe id='frame' width='320' height='295' style='border: none;'" + + " src='data:text/html,<select id=select><option>he he he</option><option>boo boo</option><option>baz baz</option></select>'" + + "</iframe>" + + "</div></body></html>"; + +function 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); +}); 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..2f60c80f7c --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_colors.js @@ -0,0 +1,683 @@ +const 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": "rgba(0, 0, 0, 0)", "unstyled": "true"}</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": "rgba(0, 0, 0, 0)", "unstyled": "true"}</option>' + + ' <option value="Seven" selected="true">{"unstyled": "true"}</option>' + + "</select></body></html>"; + +const 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>"; + +const 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>"; + +const 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>"; + +const 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>"; + +const 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>"; + +const 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>"; + +const DISABLED_OPTGROUP_AND_OPTIONS = + "<html><head>" + + "<body><select id='one'>" + + ' <optgroup label=\'{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}\'>' + + ' <option disabled="">{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option disabled="">{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + " </optgroup>" + + ' <optgroup label=\'{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}\' disabled=\'\'>' + + ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option>{"color": "GrayText", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + " </optgroup>" + + ' <option value="Two" selected="true">{"end": "true"}</option>' + + "</select></body></html>"; + +const 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>"; + +const SELECT_BGCOLOR_ON_SELECT_COLOR_ON_OPTIONS = + "<html><head><style>" + + " select { background-color: black; }" + + " option { color: white; }" + + "</style></head>" + + "<body><select id='one'>" + + ' <option>{"color": "rgb(255, 255, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>"; + +const 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": "rgb(0, 0, 0)", "backgroundColor": "rgb(255, 255, 255)"}</option>' + + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>"; + +const 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>"; + +const 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": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' + + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>"; + +const SELECT_TEXTSHADOW_OF_OPTION_CHANGES_AFTER_TRANSITIONEND = + "<html><head><style>" + + " select { transition: all .1s; }" + + " select:focus { text-shadow: 0 0 0 #303030; }" + + "</style></head><body><select id='one'>" + + ' <option>{"color": "rgb(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>"; + +const 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>"; + +let SELECT_LONG_WITH_TRANSITION = + "<html><head><style>" + + " select { transition: all .2s linear; }" + + " select:focus { color: purple; }" + + "</style></head><body><select id='one'>"; +for (let i = 0; i < 75; i++) { + SELECT_LONG_WITH_TRANSITION += + ' <option>{"color": "rgb(128, 0, 128)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>'; +} +SELECT_LONG_WITH_TRANSITION += + ' <option selected="true">{"end": "true"}</option>' + + "</select></body></html>"; + +const 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 class="redColor">{"color": "rgb(255, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</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> +`; + +const 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> +`; + +const 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> +`; + +function getSystemColor(color) { + // Need to convert system color to RGB color. + let textarea = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "textarea" + ); + textarea.style.display = "none"; + textarea.style.color = color; + document.documentElement.appendChild(textarea); + let computed = getComputedStyle(textarea).color; + textarea.remove(); + return computed; +} + +function testOptionColors(index, item, menulist) { + // The label contains a JSON string of the expected colors for + // `color` and `background-color`. + let expected = JSON.parse(item.label); + + for (let color of Object.keys(expected)) { + if ( + color.toLowerCase().includes("color") && + !expected[color].startsWith("rgb") + ) { + expected[color] = getSystemColor(expected[color]); + } + } + + // 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"), + `Item ${index} should not have any custom option styling: ${item.outerHTML}` + ); + } else { + is( + getComputedStyle(item).color, + expected.color, + "Item " + index + " has correct foreground color" + ); + is( + getComputedStyle(item).backgroundColor, + expected.backgroundColor, + "Item " + index + " has correct background color" + ); + if (expected.textShadow) { + is( + getComputedStyle(item).textShadow, + expected.textShadow, + "Item " + index + " has correct text-shadow color" + ); + } + } +} + +async function openSelectPopup(select) { + const pageUrl = "data:text/html," + escape(select); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + + let menulist = document.getElementById("ContentSelectDropdown"); + let selectPopup = menulist.menupopup; + + let popupShownPromise = BrowserTestUtils.waitForEvent( + selectPopup, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#one", + { type: "mousedown" }, + gBrowser.selectedBrowser + ); + await popupShownPromise; + return { tab, menulist, selectPopup }; +} + +async function testSelectColors(select, itemCount, options) { + let { tab, menulist, selectPopup } = await openSelectPopup(select); + if (options.waitForComputedStyle) { + let property = options.waitForComputedStyle.property; + let value = options.waitForComputedStyle.value; + await TestUtils.waitForCondition(() => { + info( + `<select> has ${property}: ${getComputedStyle(selectPopup)[property]}` + ); + return getComputedStyle(selectPopup)[property] == value; + }, `Waiting for <select> to have ${property}: ${value}`); + } + + is(selectPopup.parentNode.itemCount, itemCount, "Correct number of items"); + let child = selectPopup.firstElementChild; + let idx = 1; + + if (!options.skipSelectColorTest) { + is( + getComputedStyle(selectPopup).color, + options.selectColor, + "popup has expected foreground color" + ); + + if (options.selectTextShadow) { + is( + getComputedStyle(selectPopup).textShadow, + options.selectTextShadow, + "popup has expected text-shadow color" + ); + } + + // Combine the select popup's backgroundColor and the + // backgroundImage color to get the color that is seen by + // the user. + let base = getComputedStyle(selectPopup).backgroundColor; + 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(selectPopup).backgroundImage; + if (topCoat == "none") { + is( + `rgb(${bR}, ${bG}, ${bB})`, + options.selectBgColor, + "popup has expected background color" + ); + } 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, + "popup has expected background color" + ); + } + } + + ok(!child.selected, "The first child should not be selected"); + while (child) { + testOptionColors(idx, child, menulist); + idx++; + child = child.nextElementSibling; + } + + if (!options.leaveOpen) { + await hideSelectPopup(selectPopup, "escape"); + BrowserTestUtils.removeTab(tab); + } +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.select_popup_in_parent.enabled", true], + ["dom.forms.select.customstyling", true], + ], + }); +}); + +// 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 = { + selectColor: getSystemColor("-moz-ComboboxText"), + selectBgColor: getSystemColor("-moz-Combobox"), + }; + 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: "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: "rgb(255, 255, 255)", + }; + + 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: "rgb(255, 255, 255)", + waitForComputedStyle: { + property: "color", + value: "rgb(128, 0, 128)", + }, + leaveOpen: true, + }; + + await testSelectColors(SELECT_LONG_WITH_TRANSITION, 76, options); + + let menulist = document.getElementById("ContentSelectDropdown"); + let selectPopup = menulist.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(selectPopup, "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)", + selectBgColor: "rgb(255, 255, 255)", + selectTextShadow: "rgb(0, 0, 255) 1px 1px 2px", + leaveOpen: true, + }; + + await testSelectColors( + SELECT_INHERITED_COLORS_ON_OPTIONS_DONT_GET_UNIQUE_RULES_IF_RULE_SET_ON_SELECT, + 4, + options + ); + + let stylesheetEl = document.getElementById( + "ContentSelectDropdownStylesheet" + ); + let sheet = stylesheetEl.sheet; + /* Check that there are no rulesets for the first option, but that + one exists for the second option and sets the color of that + option to "rgb(255, 0, 0)" */ + + function hasMatchingRuleForOption(cssRules, index, styles = {}) { + for (let rule of cssRules) { + if (rule.selectorText.includes(`:nth-child(${index})`)) { + if ( + Object.keys(styles).some(key => rule.style[key] !== styles[key]) + ) { + continue; + } + return true; + } + } + return false; + } + + is( + hasMatchingRuleForOption(sheet.cssRules, 1), + false, + "There should be no rules specific to option1" + ); + is( + hasMatchingRuleForOption(sheet.cssRules, 2, { + color: "rgb(255, 0, 0)", + }), + true, + "There should be a rule specific to option2 and it should have color: red" + ); + is( + hasMatchingRuleForOption(sheet.cssRules, 3, { + "text-shadow": "rgb(0, 0, 0) 1px 1px 2px", + }), + true, + "There should be a rule specific to option3 and it should have text-shadow: rgb(0, 0, 0) 1px 1px 2px" + ); + + let menulist = document.getElementById("ContentSelectDropdown"); + let selectPopup = menulist.menupopup; + + await hideSelectPopup(selectPopup, "escape"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +); + +add_task(async function test_select_font_inherits_to_option() { + let { tab, menulist, selectPopup } = await openSelectPopup( + 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(selectPopup, "escape"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_scrollbar_props() { + let { tab, selectPopup } = await openSelectPopup(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(selectPopup, "escape"); + BrowserTestUtils.removeTab(tab); +}); 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..bdfcf98005 --- /dev/null +++ b/browser/base/content/test/forms/browser_selectpopup_searchfocus.js @@ -0,0 +1,52 @@ +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_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.select_popup_in_parent.enabled", true], + ["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 menulist = document.getElementById("ContentSelectDropdown"); + let selectPopup = menulist.menupopup; + + let popupShownPromise = BrowserTestUtils.waitForEvent( + selectPopup, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#one", + { type: "mousedown" }, + gBrowser.selectedBrowser + ); + await popupShownPromise; + + 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(selectPopup, "escape"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/base/content/test/forms/head.js b/browser/base/content/test/forms/head.js new file mode 100644 index 0000000000..4776639054 --- /dev/null +++ b/browser/base/content/test/forms/head.js @@ -0,0 +1,20 @@ +function hideSelectPopup(selectPopup, mode = "enter", win = window) { + let browser = win.gBrowser.selectedBrowser; + let selectClosedPromise = SpecialPowers.spawn(browser, [], async function() { + let { SelectContentHelper } = ChromeUtils.import( + "resource://gre/actors/SelectChild.jsm", + null + ); + 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") { + EventUtils.synthesizeMouseAtCenter(selectPopup.lastElementChild, {}, win); + } + + return selectClosedPromise; +} |