const gSelects = { PAGECONTENT_COLORS: "" + "", PAGECONTENT_COLORS_ON_SELECT: "" + "", TRANSPARENT_SELECT: "" + "", OPTION_COLOR_EQUAL_TO_UABACKGROUND_COLOR_SELECT: "" + "", GENERIC_OPTION_STYLED_AS_IMPORTANT: "" + "", TRANSLUCENT_SELECT_BECOMES_OPAQUE: "" + "", TRANSLUCENT_SELECT_APPLIES_ON_BASE_COLOR: "" + "", DISABLED_OPTGROUP_AND_OPTIONS: "" + "", SELECT_CHANGES_COLOR_ON_FOCUS: "" + "", SELECT_BGCOLOR_ON_SELECT_COLOR_ON_OPTIONS: "" + "", SELECT_STYLE_OF_OPTION_IS_BASED_ON_FOCUS_OF_SELECT: "" + "", SELECT_STYLE_OF_OPTION_CHANGES_AFTER_FOCUS_EVENT: "" + " var select = document.getElementById('one');" + " select.addEventListener('focus', () => select.style.color = 'red');" + "", SELECT_COLOR_OF_OPTION_CHANGES_AFTER_TRANSITIONEND: "", SELECT_TEXTSHADOW_OF_OPTION_CHANGES_AFTER_TRANSITIONEND: "", SELECT_TRANSPARENT_COLOR_WITH_TEXT_SHADOW: "", SELECT_LONG_WITH_TRANSITION: ""; return rv; })(), SELECT_INHERITED_COLORS_ON_OPTIONS_DONT_GET_UNIQUE_RULES_IF_RULE_SET_ON_SELECT: ` `, SELECT_FONT_INHERITS_TO_OPTION: ` `, SELECT_SCROLLBAR_PROPS: ` `, DEFAULT_DARKMODE: ` `, DEFAULT_DARKMODE_DARK: ` `, SPLIT_FG_BG_OPTION_DARKMODE: ` `, IDENTICAL_BG_DIFF_FG_OPTION_DARKMODE: ` `, }; function rgbaToString(parsedColor) { let { r, g, b, a } = parsedColor; if (a == 1) { return `rgb(${r}, ${g}, ${b})`; } return `rgba(${r}, ${g}, ${b}, ${a})`; } function testOptionColors(test, index, item, menulist) { // The label contains a JSON string of the expected colors for // `color` and `background-color`. let expected = JSON.parse(item.label); // Press Down to move the selected item to the next item in the // list and check the colors of this item when it's not selected. EventUtils.synthesizeKey("KEY_ArrowDown"); if (expected.end) { return; } if (expected.unstyled) { ok( !item.hasAttribute("customoptionstyling"), `${test}: Item ${index} should not have any custom option styling: ${item.outerHTML}` ); } else { is( getComputedStyle(item).color, expected.color, `${test}: Item ${index} has correct foreground color` ); is( getComputedStyle(item).backgroundColor, expected.backgroundColor, `${test}: Item ${index} has correct background color` ); if (expected.textShadow) { is( getComputedStyle(item).textShadow, expected.textShadow, `${test}: Item ${index} has correct text-shadow color` ); } } } function computeLabels(tab) { return SpecialPowers.spawn(tab.linkedBrowser, [], function () { function _rgbaToString(parsedColor) { let { r, g, b, a } = parsedColor; if (a == 1) { return `rgb(${r}, ${g}, ${b})`; } return `rgba(${r}, ${g}, ${b}, ${a})`; } function computeColors(expected) { let any = false; for (let color of Object.keys(expected)) { if ( color != "colorScheme" && color.toLowerCase().includes("color") && !expected[color].startsWith("rgb") ) { any = true; expected[color] = _rgbaToString( InspectorUtils.colorToRGBA(expected[color], content.document) ); } } return any; } for (let option of content.document.querySelectorAll("option,optgroup")) { if (!option.label) { continue; } let expected; try { expected = JSON.parse(option.label); } catch (ex) { continue; } if (computeColors(expected)) { option.label = JSON.stringify(expected); } } }); } async function openSelectPopup(select) { const pageUrl = "data:text/html," + escape(select); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); await computeLabels(tab); let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window); await BrowserTestUtils.synthesizeMouseAtCenter( "#one", { type: "mousedown" }, gBrowser.selectedBrowser ); let selectPopup = await popupShownPromise; let menulist = selectPopup.parentNode; return { tab, menulist, selectPopup }; } async function testSelectColors(selectID, itemCount, options) { let select = gSelects[selectID]; let { tab, menulist, selectPopup } = await openSelectPopup(select); if (options.unstyled) { ok( !selectPopup.hasAttribute("customoptionstyling"), `Shouldn't have custom option styling for ${selectID}` ); } let arrowSB = selectPopup.shadowRoot.querySelector( ".menupopup-arrowscrollbox" ); if (options.waitForComputedStyle) { let property = options.waitForComputedStyle.property; let expectedValue = options.waitForComputedStyle.value; await TestUtils.waitForCondition(() => { let node = ["background-image", "background-color"].includes(property) ? arrowSB : selectPopup; let value = getComputedStyle(node).getPropertyValue(property); info(`<${node.localName}> has ${property}: ${value}`); return value == expectedValue; }, `${selectID} - Waiting for `, function (browser) { return SpecialPowers.spawn(browser, [], function () { let cs = content.getComputedStyle( content.document.querySelector("select") ); return { backgroundColor: cs.backgroundColor, }; }); } ); }); // This test checks when a 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 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 element has a background set, and the // options have their own background set which is equal to the default // user-agent background color, but should be used because the select // background color has been changed. add_task(async function test_translucent_select_becomes_opaque() { // The popup is requested to show a translucent background // but we apply the requested background color on the system's base color. let options = { selectColor: "rgb(0, 0, 0)", selectBgColor: "rgb(255, 255, 255)", }; await testSelectColors("TRANSLUCENT_SELECT_BECOMES_OPAQUE", 2, options); }); // This test checks when a popup has a translucent background color, // and that the color painted to the screen of the translucent background // matches what the user expects. add_task(async function test_translucent_select_applies_on_base_color() { // The popup is requested to show a translucent background // but we apply the requested background color on the system's base color. let options = { selectColor: "rgb(0, 0, 0)", selectBgColor: "rgb(255, 115, 115)", }; await testSelectColors( "TRANSLUCENT_SELECT_APPLIES_ON_BASE_COLOR", 2, options ); }); add_task(async function test_disabled_optgroup_and_options() { await testSelectColors("DISABLED_OPTGROUP_AND_OPTIONS", 17, { skipSelectColorTest: true, }); }); add_task(async function test_disabled_optgroup_and_options() { let options = { selectColor: "rgb(0, 0, 0)", selectBgColor: "rgb(255, 165, 0)", }; await testSelectColors("SELECT_CHANGES_COLOR_ON_FOCUS", 2, options); }); add_task(async function test_bgcolor_on_select_color_on_options() { let options = { selectColor: "rgb(0, 0, 0)", selectBgColor: "rgb(0, 0, 0)", }; await testSelectColors( "SELECT_BGCOLOR_ON_SELECT_COLOR_ON_OPTIONS", 2, options ); }); add_task( async function test_style_of_options_is_dependent_on_focus_of_select() { let options = { selectColor: "rgb(0, 0, 0)", selectBgColor: "rgb(58, 150, 221)", }; await testSelectColors( "SELECT_STYLE_OF_OPTION_IS_BASED_ON_FOCUS_OF_SELECT", 2, options ); } ); add_task( async function test_style_of_options_is_dependent_on_focus_of_select_after_event() { let options = { skipSelectColorTest: true, waitForComputedStyle: { property: "--panel-color", value: "rgb(255, 0, 0)", }, }; await testSelectColors( "SELECT_STYLE_OF_OPTION_CHANGES_AFTER_FOCUS_EVENT", 2, options ); } ); add_task(async function test_color_of_options_is_dependent_on_transitionend() { let options = { selectColor: "rgb(0, 0, 0)", selectBgColor: "rgb(255, 165, 0)", waitForComputedStyle: { property: "background-image", value: "linear-gradient(rgb(255, 165, 0), rgb(255, 165, 0))", }, }; await testSelectColors( "SELECT_COLOR_OF_OPTION_CHANGES_AFTER_TRANSITIONEND", 2, options ); }); add_task( async function test_textshadow_of_options_is_dependent_on_transitionend() { let options = { skipSelectColorTest: true, waitForComputedStyle: { property: "text-shadow", value: "rgb(48, 48, 48) 0px 0px 0px", }, }; await testSelectColors( "SELECT_TEXTSHADOW_OF_OPTION_CHANGES_AFTER_TRANSITIONEND", 2, options ); } ); add_task(async function test_transparent_color_with_text_shadow() { let options = { selectColor: "rgba(0, 0, 0, 0)", selectTextShadow: "rgb(48, 48, 48) 0px 0px 0px", selectBgColor: kDefaultSelectStyles.backgroundColor, }; await testSelectColors( "SELECT_TRANSPARENT_COLOR_WITH_TEXT_SHADOW", 2, options ); }); add_task( async function test_select_with_transition_doesnt_lose_scroll_position() { let options = { selectColor: "rgb(128, 0, 128)", selectBgColor: kDefaultSelectStyles.backgroundColor, waitForComputedStyle: { property: "--panel-color", value: "rgb(128, 0, 128)", }, leaveOpen: true, }; await testSelectColors("SELECT_LONG_WITH_TRANSITION", 76, options); let selectPopup = document.getElementById( "ContentSelectDropdown" ).menupopup; let scrollBox = selectPopup.scrollBox; is( scrollBox.scrollTop, scrollBox.scrollTopMax, "The popup should be scrolled to the bottom of the list (where the selected item is)" ); await hideSelectPopup("escape"); BrowserTestUtils.removeTab(gBrowser.selectedTab); } ); add_task( async function test_select_inherited_colors_on_options_dont_get_unique_rules_if_rule_set_on_select() { let options = { selectColor: "rgb(0, 0, 255)", selectTextShadow: "rgb(0, 0, 255) 1px 1px 2px", selectBgColor: kDefaultSelectStyles.backgroundColor, leaveOpen: true, }; await testSelectColors( "SELECT_INHERITED_COLORS_ON_OPTIONS_DONT_GET_UNIQUE_RULES_IF_RULE_SET_ON_SELECT", 6, options ); let stylesheetEl = document.getElementById( "ContentSelectDropdownStylesheet" ); let sheet = stylesheetEl.sheet; /* Check that the rules are what we expect: There are three different option styles (even though there are 6 options, plus the select rules). */ let expectedSelectors = [ "#ContentSelectDropdown .ContentSelectDropdown-item-0", "#ContentSelectDropdown .ContentSelectDropdown-item-1", '#ContentSelectDropdown .ContentSelectDropdown-item-1:not([_moz-menuactive="true"])', "#ContentSelectDropdown .ContentSelectDropdown-item-2", '#ContentSelectDropdown .ContentSelectDropdown-item-2:not([_moz-menuactive="true"])', '#ContentSelectDropdown > menupopup > :is(menuitem, menucaption):not([_moz-menuactive="true"])', '#ContentSelectDropdown > menupopup > :is(menuitem, menucaption)[_moz-menuactive="true"]', ].sort(); let actualSelectors = [...sheet.cssRules].map(r => r.selectorText).sort(); is( actualSelectors.length, expectedSelectors.length, "Should have the expected number of rules" ); for (let i = 0; i < expectedSelectors.length; ++i) { is( actualSelectors[i], expectedSelectors[i], `Selector ${i} should match` ); } await hideSelectPopup("escape"); BrowserTestUtils.removeTab(gBrowser.selectedTab); } ); add_task(async function test_select_font_inherits_to_option() { let { tab, menulist, selectPopup } = await openSelectPopup( gSelects.SELECT_FONT_INHERITS_TO_OPTION ); let popupFont = getComputedStyle(selectPopup).fontFamily; let items = menulist.querySelectorAll("menuitem"); is(items.length, 2, "Should have two options"); let firstItemFont = getComputedStyle(items[0]).fontFamily; let secondItemFont = getComputedStyle(items[1]).fontFamily; is( popupFont, firstItemFont, "First menuitem's font should be inherited from the select" ); isnot( popupFont, secondItemFont, "Second menuitem's font should be the author specified one" ); await hideSelectPopup("escape"); BrowserTestUtils.removeTab(tab); }); add_task(async function test_scrollbar_props() { let { tab, selectPopup } = await openSelectPopup( gSelects.SELECT_SCROLLBAR_PROPS ); let popupStyle = getComputedStyle(selectPopup); is(popupStyle.getPropertyValue("--content-select-scrollbar-width"), "thin"); is(popupStyle.scrollbarColor, "rgb(255, 0, 0) rgb(0, 0, 255)"); let scrollBoxStyle = getComputedStyle(selectPopup.scrollBox.scrollbox); is(scrollBoxStyle.overflow, "auto", "Should be the scrollable box"); is(scrollBoxStyle.scrollbarWidth, "thin"); is(scrollBoxStyle.scrollbarColor, "rgb(255, 0, 0) rgb(0, 0, 255)"); await hideSelectPopup("escape"); BrowserTestUtils.removeTab(tab); }); if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) { add_task(async function test_darkmode() { let lightSelectColor = rgbaToString( InspectorUtils.colorToRGBA("MenuText", document) ); let lightSelectBgColor = rgbaToString( InspectorUtils.colorToRGBA("Menu", document) ); // Force dark mode: let darkModeQuery = matchMedia("(prefers-color-scheme: dark)"); let darkModeChange = BrowserTestUtils.waitForEvent(darkModeQuery, "change"); await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 1]] }); await darkModeChange; // Determine colours from the main context menu: let darkSelectColor = rgbaToString( InspectorUtils.colorToRGBA("MenuText", document) ); let darkSelectBgColor = rgbaToString( InspectorUtils.colorToRGBA("Menu", document) ); isnot(lightSelectColor, darkSelectColor); isnot(lightSelectBgColor, darkSelectBgColor); let { tab } = await openSelectPopup(gSelects.DEFAULT_DARKMODE); await testSelectColors("DEFAULT_DARKMODE", 3, { selectColor: lightSelectColor, selectBgColor: lightSelectBgColor, }); await hideSelectPopup("escape"); await testSelectColors("DEFAULT_DARKMODE_DARK", 3, { selectColor: darkSelectColor, selectBgColor: darkSelectBgColor, }); await hideSelectPopup("escape"); BrowserTestUtils.removeTab(tab); ({ tab } = await openSelectPopup( gSelects.IDENTICAL_BG_DIFF_FG_OPTION_DARKMODE )); // Custom styling on the options enforces using the select styling, too, // even if it matched the UA style. They'll be overridden on individual // options where necessary. await testSelectColors("IDENTICAL_BG_DIFF_FG_OPTION_DARKMODE", 3, { selectColor: "rgb(0, 0, 0)", selectBgColor: "rgb(255, 255, 255)", }); await hideSelectPopup("escape"); BrowserTestUtils.removeTab(tab); ({ tab } = await openSelectPopup(gSelects.SPLIT_FG_BG_OPTION_DARKMODE)); // Like the previous case, but here the bg colour is defined on the // select, and the fg colour on the option. The behaviour should be the // same. await testSelectColors("SPLIT_FG_BG_OPTION_DARKMODE", 3, { selectColor: "rgb(0, 0, 0)", selectBgColor: "rgb(255, 255, 255)", }); await hideSelectPopup("escape"); BrowserTestUtils.removeTab(tab); }); }