diff options
Diffstat (limited to 'toolkit/content/tests/browser/datetime')
16 files changed, 4707 insertions, 0 deletions
diff --git a/toolkit/content/tests/browser/datetime/browser.toml b/toolkit/content/tests/browser/datetime/browser.toml new file mode 100644 index 0000000000..6e8580ddc4 --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser.toml @@ -0,0 +1,96 @@ +[DEFAULT] +support-files = ["head.js"] + +["browser_datetime_blur.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked td.outside may not be accessible +# This file was skipped before new tests were written based on it in Bug 1676068 +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_clear.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_focus.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_keynav.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_markup.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_min_max.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked TD may not be accessible +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_monthyear.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_mousenav.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked td.weekend.outside may not be accessible +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_prev_next_month.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_showPicker.js"] +# do not skip + +["browser_datetime_toplevel.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked input may not be accessible + +["browser_spinner.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_spinner_keynav.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_blur.js b/toolkit/content/tests/browser/datetime/browser_datetime_blur.js new file mode 100644 index 0000000000..e7ac0037b9 --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_blur.js @@ -0,0 +1,265 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_CONTENT = `data:text/html, + <body onload='gBlurEvents = 0; gDateFocusEvents = 0; gTextFocusEvents = 0'> + <input type='date' id='date' onfocus='gDateFocusEvents++' onblur='gBlurEvents++'> + <input type='text' id='text' onfocus='gTextFocusEvents++'> + </body>`; + +function getBlurEvents() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + return content.wrappedJSObject.gBlurEvents; + }); +} + +function getDateFocusEvents() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + return content.wrappedJSObject.gDateFocusEvents; + }); +} + +function getTextFocusEvents() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + return content.wrappedJSObject.gTextFocusEvents; + }); +} + +/** + * Test that when a picker panel is opened by an input + * the input is not blurred + */ +add_task(async function test_parent_blur() { + info( + "Test that when a picker panel is opened by an input the parent is not blurred" + ); + + // Set "prefers-reduced-motion" media to "reduce" + // to avoid intermittent scroll failures (1803612, 1803687) + await SpecialPowers.pushPrefEnv({ + set: [["ui.prefersReducedMotion", 1]], + }); + Assert.ok( + matchMedia("(prefers-reduced-motion: reduce)").matches, + "The reduce motion mode is active" + ); + + await helper.openPicker(PAGE_CONTENT, false, "showPicker"); + + Assert.equal( + await getDateFocusEvents(), + 0, + "Date input field is not calling a focus event when the '.showPicker()' method is called" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const input = content.document.querySelector("#date"); + + Assert.ok( + !input.matches(":focus"), + `The keyboard focus is not placed on the date input after showPicker is called` + ); + }); + + let closedOnEsc = helper.promisePickerClosed(); + + // Close a date picker + EventUtils.synthesizeKey("KEY_Escape", {}); + + await closedOnEsc; + + Assert.equal( + helper.panel.state, + "closed", + "Panel should be closed on Escape" + ); + Assert.equal( + await getDateFocusEvents(), + 0, + "Date input field is not focused when its picker is dismissed with Escape key" + ); + Assert.equal( + await getBlurEvents(), + 0, + "Date input field is not blurred when the picker is closed with Escape key" + ); + + // Ensure focus is on the input field + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const input = content.document.querySelector("#date"); + + input.focus(); + + Assert.ok( + input.matches(":focus"), + `The keyboard focus is placed on the date input field` + ); + }); + Assert.equal( + await getDateFocusEvents(), + 1, + "A focus event was fired on the Date input field" + ); + + let readyOnKey = helper.waitForPickerReady(); + + // Open a date picker + EventUtils.synthesizeKey(" ", {}); + + await readyOnKey; + + Assert.equal( + helper.panel.state, + "open", + "Date picker panel should be opened" + ); + Assert.equal( + helper.panel + .querySelector("#dateTimePopupFrame") + .contentDocument.activeElement.getAttribute("role"), + "gridcell", + "The picker is opened and a calendar day is focused" + ); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const input = content.document.querySelector("#date"); + + Assert.ok( + input.matches(":focus"), + `The keyboard focus is retained on the date input field` + ); + Assert.equal( + input, + content.document.activeElement, + "Input field does not loose focus when its picker is opened and focused" + ); + }); + + Assert.equal( + await getBlurEvents(), + 0, + "Date input field is not blurred when its picker is opened and focused" + ); + Assert.equal( + await getDateFocusEvents(), + 1, + "No new focus events were fired on the Date input while its picker is opened" + ); + + info( + `Test that the date input field is not blurred after interacting + with a month-year panel` + ); + + // Move focus from the today's date to the month-year toggle button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + + Assert.ok( + helper.getElement(BTN_MONTH_YEAR).matches(":focus"), + "The month-year toggle button is focused" + ); + + // Open the month-year selection panel: + EventUtils.synthesizeKey(" ", {}); + + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"), + "true", + "Month-year button is expanded when the spinners are shown" + ); + Assert.ok( + BrowserTestUtils.isVisible(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection panel is visible" + ); + + // Move focus from the month-year toggle button to the year spinner: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 }); + + // Change the year spinner value from February 2023 to March 2023: + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const input = content.document.querySelector("#date"); + + Assert.ok( + input.matches(":focus"), + `The keyboard focus is retained on the date input field` + ); + Assert.equal( + input, + content.document.activeElement, + "Input field does not loose focus when the month-year picker is opened and interacted with" + ); + }); + + Assert.equal( + await getBlurEvents(), + 0, + "Date input field is not blurred after interacting with a month-year panel" + ); + + info(`Test that when a picker panel is opened and then it is closed + with a click on the other field, the focus is updated`); + + let closedOnClick = helper.promisePickerClosed(); + + // Close a picker by clicking on another input + await BrowserTestUtils.synthesizeMouseAtCenter( + "#text", + {}, + gBrowser.selectedBrowser + ); + + await closedOnClick; + + Assert.equal( + helper.panel.state, + "closed", + "Panel should be closed when another element is clicked" + ); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + const inputText = content.document.querySelector("#text"); + const inputDate = content.document.querySelector("#date"); + + Assert.ok( + inputText.matches(":focus"), + `The keyboard focus is moved to the text input field` + ); + Assert.equal( + inputText, + content.document.activeElement, + "Text input field gains a focus when clicked" + ); + Assert.ok( + !inputDate.matches(":focus"), + `The keyboard focus is moved from the date input field` + ); + Assert.notEqual( + inputDate, + content.document.activeElement, + "Date input field is not focused anymore" + ); + }); + + Assert.equal( + await getBlurEvents(), + 1, + "Date input field is blurred when focus is moved to the text input field" + ); + Assert.equal( + await getTextFocusEvents(), + 1, + "Text input field is focused when it is clicked" + ); + Assert.equal( + await getDateFocusEvents(), + 1, + "No new focus events were fired on the Date input after its picker was closed" + ); + + await helper.tearDown(); + // Clear the prefers-reduced-motion pref from the test profile: + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker.js new file mode 100644 index 0000000000..b7c4df8d2a --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker.js @@ -0,0 +1,369 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Create a list of abbreviations for calendar class names +const W = "weekend", + O = "outside", + S = "selection", + R = "out-of-range", + T = "today", + P = "off-step"; + +// Calendar classlist for 2016-12. Used to verify the classNames are correct. +const calendarClasslist_201612 = [ + [W, O], + [O], + [O], + [O], + [], + [], + [W], + [W], + [], + [], + [], + [], + [], + [W], + [W], + [], + [], + [], + [S], + [], + [W], + [W], + [], + [], + [], + [], + [], + [W], + [W], + [], + [], + [], + [], + [], + [W], + [W, O], + [O], + [O], + [O], + [O], + [O], + [W, O], +]; + +/** + * Test that date picker opens to today's date when input field is blank + */ +add_task(async function test_datepicker_today() { + info("Test that date picker opens to today's date when input field is blank"); + + const date = new Date(); + + await helper.openPicker("data:text/html, <input type='date'>"); + + if (date.getMonth() === new Date().getMonth()) { + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT_LOCAL(date), + "Today's date is opened" + ); + Assert.equal( + helper.getElement(DAY_TODAY).getAttribute("aria-current"), + "date", + "Today's date is programmatically current" + ); + Assert.equal( + helper.getElement(DAY_TODAY).getAttribute("tabindex"), + "0", + "Today's date is included in the focus order, when nothing is selected" + ); + } else { + Assert.ok( + true, + "Skipping datepicker today test if month changes when opening picker." + ); + } + + await helper.tearDown(); +}); + +/** + * Test that date picker opens to the correct month, with calendar days + * displayed correctly, given a date value is set. + */ +add_task(async function test_datepicker_open() { + info("Test the date picker markup with a set input date value"); + + const inputValue = "2016-12-15"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)), + "2016-12-15 date is opened" + ); + + Assert.deepEqual( + getCalendarText(), + [ + "27", + "28", + "29", + "30", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + ], + "Calendar text for 2016-12 is correct" + ); + Assert.deepEqual( + getCalendarClassList(), + calendarClasslist_201612, + "2016-12 classNames of the picker are correct" + ); + Assert.equal( + helper.getElement(DAY_SELECTED).getAttribute("aria-selected"), + "true", + "Chosen date is programmatically selected" + ); + Assert.equal( + helper.getElement(DAY_SELECTED).getAttribute("tabindex"), + "0", + "Selected date is included in the focus order" + ); + + await helper.tearDown(); +}); + +/** + * Ensure that the datepicker popup appears correctly positioned when + * the input field has been transformed. + */ +add_task(async function test_datepicker_transformed_position() { + const inputValue = "2016-12-15"; + + const style = + "transform: translateX(7px) translateY(13px); border-top: 2px; border-left: 5px; margin: 30px;"; + const iframeContent = `<input id="date" type="date" value="${inputValue}" style="${style}">`; + await helper.openPicker( + "data:text/html,<iframe id='iframe' src='http://example.net/document-builder.sjs?html=" + + encodeURI(iframeContent) + + "'>", + true + ); + + let bc = helper.tab.linkedBrowser.browsingContext.children[0]; + await verifyPickerPosition(bc, "date"); + + await helper.tearDown(); +}); + +/** + * Make sure picker is in correct state when it is reopened. + */ +add_task(async function test_datepicker_reopen_state() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + // Navigate to the next month but do not commit the change + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)) + ); + + helper.click(helper.getElement(BTN_NEXT_MONTH)); + + // January 2017 + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextMonth)) + ); + + let closed = helper.promisePickerClosed(); + + EventUtils.synthesizeKey("KEY_Escape", {}); + + await closed; + + Assert.equal(helper.panel.state, "closed", "Panel should be closed"); + + // December 2016 + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + let input = content.document.querySelector("input"); + Assert.equal( + input.value, + "2016-12-15", + "The input value remains unchanged after the picker is dismissed" + ); + }); + + let ready = helper.waitForPickerReady(); + + // Move focus from the browser to an input field and open a picker: + EventUtils.synthesizeKey("KEY_Tab", {}); + EventUtils.synthesizeKey(" ", {}); + + await ready; + + Assert.equal(helper.panel.state, "open", "Panel should be opened"); + + // December 2016 + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)) + ); + + await helper.tearDown(); +}); + +/** + * When step attribute is set, calendar should show some dates as off-step. + */ +add_task(async function test_datepicker_step() { + const inputValue = "2016-12-15"; + const inputStep = "5"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}" step="${inputStep}">` + ); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // P denotes off-step + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + ]), + "2016-12 with step" + ); + + await helper.tearDown(); +}); + +// This test checks if the change event is considered as user input event. +add_task(async function test_datepicker_handling_user_input() { + await helper.openPicker(`data:text/html, <input type="date">`); + + let changeEventPromise = helper.promiseChange(); + + // Click the first item (top-left corner) of the calendar + helper.click(helper.getElement(DAYS_VIEW).children[0]); + await changeEventPromise; + + await helper.tearDown(); +}); + +/** + * Ensure datetime-local picker closes when selection is made. + */ +add_task(async function test_datetime_focus_to_input() { + info("Ensure datetime-local picker closes when focus moves to a time input"); + + await helper.openPicker( + `data:text/html,<input id=datetime type=datetime-local>` + ); + let browser = helper.tab.linkedBrowser; + await verifyPickerPosition(browser, "datetime"); + + Assert.equal(helper.panel.state, "open", "Panel should be visible"); + + // Make selection to close the date dialog + await EventUtils.synthesizeKey(" ", {}); + + let closed = helper.promisePickerClosed(); + + await closed; + + Assert.equal(helper.panel.state, "closed", "Panel should be closed now"); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_clear.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_clear.js new file mode 100644 index 0000000000..3c3de2dc98 --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_clear.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +async function testClear(key) { + const inputValue = "2023-03-03"; + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + let browser = helper.tab.linkedBrowser; + + Assert.equal(helper.panel.state, "open", "Panel should be opened"); + + let closed = helper.promisePickerClosed(); + + // Clear the input fields + if (key) { + // Move focus from the selected date to the Clear button: + EventUtils.synthesizeKey("KEY_Tab", {}); + + Assert.ok( + helper.getElement(BTN_CLEAR).matches(":focus"), + "The Clear button can receive keyboard focus" + ); + + EventUtils.synthesizeKey(key, {}); + } else { + helper.click(helper.getElement(BTN_CLEAR)); + } + + await closed; + + await SpecialPowers.spawn(browser, [], () => { + is( + content.document.querySelector("input").value, + "", + "The input value is reset after the Clear button is pressed" + ); + }); + + await helper.tearDown(); +} + +add_task(async function test_datepicker_clear_keyboard() { + await testClear(" "); +}); + +add_task(async function test_datepicker_clear_keyboard_enter() { + await testClear("KEY_Enter"); +}); + +add_task(async function test_datepicker_clear_mouse() { + await testClear(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_focus.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_focus.js new file mode 100644 index 0000000000..b99e8ed0e8 --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_focus.js @@ -0,0 +1,191 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Ensure navigating through Datepicker using keyboard after a date + * has already been selected will keep the keyboard focus + * when reaching a different month (bug 1804466). + */ +add_task(async function test_focus_after_selection() { + info( + `Ensure navigating through Datepicker using keyboard after a date has already been selected will not lose keyboard focus when reaching a different month.` + ); + + // Set "prefers-reduced-motion" media to "reduce" + // to avoid intermittent scroll failures (1803612, 1803687) + await SpecialPowers.pushPrefEnv({ + set: [["ui.prefersReducedMotion", 1]], + }); + Assert.ok( + matchMedia("(prefers-reduced-motion: reduce)").matches, + "The reduce motion mode is active" + ); + + const inputValue = "2022-12-12"; + const prevMonth = "2022-10-01"; + const nextYear = "2023-11-01"; + const nextYearAfter = "2024-01-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value=${inputValue}>` + ); + let browser = helper.tab.linkedBrowser; + + info("Test behavior when selection is done on the calendar grid"); + + // Move focus from 2022-12-12 to 2022-10-24 by week + // Changing 2 month views along the way: + EventUtils.synthesizeKey("KEY_ArrowUp", { repeat: 7 }); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)), + "The calendar is updated to show the second previous month (2022-10)." + ); + + // 2022-10-24: + const focusedDayEl = getDayEl(24); + + Assert.ok( + focusedDayEl.matches(":focus"), + "An expected focusable day within a calendar grid is focused" + ); + + let closed = helper.promisePickerClosed(); + + // Make a selection and close the picker + EventUtils.synthesizeKey(" ", {}); + + // Check the focus is returned to main browser window when a panel is closed + await SpecialPowers.spawn(browser, [], async () => { + const body = content.document.body; + // Testing the focus position within content: + Assert.deepEqual( + body, + content.document.activeElement, + `The main content's <body> received programmatic focus` + ); + }); + + await closed; + + Assert.equal( + helper.panel.state, + "closed", + "Panel is closed when the selection is made" + ); + + let ready = helper.waitForPickerReady(); + + // Move the keyboard focus to the input field to reopen the picker + EventUtils.synthesizeKey("KEY_Tab", {}); + + // Check the focus is returned to the Calendar button + await SpecialPowers.spawn(browser, [], async () => { + const input = content.document.querySelector("input"); + // Testing the focus position within content: + Assert.deepEqual( + input, + content.document.activeElement, + `The input field includes programmatic focus` + ); + }); + + // Reopen the picker + EventUtils.synthesizeKey(" ", {}); + + await ready; + + Assert.equal(helper.panel.state, "open", "Panel is reopened"); + + // Move focus from 2022-10-24 to 2022-12-12 by week + // Changing 2 month views along the way: + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 7 }); + + // 2022-12-12: + const focusedDay = getDayEl(12); + const monthYearEl = helper.getElement(MONTH_YEAR); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue)); + }, + `Should change to December 2022, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + Assert.equal( + focusedDay, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "There is a focusable day within a calendar grid" + ); + Assert.ok( + focusedDay.matches(":focus"), + "The focusable day within a calendar grid is focused" + ); + + info("Test behavior when selection is done on the month-year panel"); + + // Move focus to the month-year toggle button and open it: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + EventUtils.synthesizeKey(" "); + + // Move focus to the month spin button and change its value + // from December to November: + EventUtils.synthesizeKey("KEY_Tab"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + + // Move focus to the year spin button and change its value + // from 2022 to 2023: + EventUtils.synthesizeKey("KEY_Tab"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(nextYear)); + }, + `Should change to November 2023, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + // Make a selection, close the month picker + EventUtils.synthesizeKey(" ", {}); + + Assert.ok( + BrowserTestUtils.isHidden(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection panel is not visible" + ); + + // Move focus from 2023-11-12 to 2024-01-07 by week + // Changing 2 month views along the way: + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 8 }); + + // 2024-01-07: + const newFocusedDay = getDayEl(7); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextYearAfter)), + "The calendar is updated to show another month (2024-01)." + ); + Assert.equal( + newFocusedDay, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "There is a focusable day within a calendar grid" + ); + Assert.ok( + newFocusedDay.matches(":focus"), + "The focusable day within a calendar grid is focused" + ); + + await helper.tearDown(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js new file mode 100644 index 0000000000..0b271ed77a --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js @@ -0,0 +1,576 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Ensure picker opens, closes, and updates its value with key bindings appropriately. + */ +add_task(async function test_datepicker_keyboard_nav() { + info( + "Ensure picker opens, closes, and updates its value with key bindings appropriately." + ); + + const inputValue = "2016-12-15"; + const prevMonth = "2016-11-01"; + await helper.openPicker( + `data:text/html,<input id=date type=date value=${inputValue}>` + ); + let browser = helper.tab.linkedBrowser; + Assert.equal(helper.panel.state, "open", "Panel should be opened"); + + await testCalendarBtnAttribute("aria-expanded", "true"); + + let closed = helper.promisePickerClosed(); + + // Close on Escape anywhere + EventUtils.synthesizeKey("KEY_Escape", {}); + + await closed; + + Assert.equal( + helper.panel.state, + "closed", + "Panel should be closed after Escape from anywhere on the window" + ); + + await testCalendarBtnAttribute("aria-expanded", "false"); + + let ready = helper.waitForPickerReady(); + + // Ensure focus is on the input field + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("#date").focus(); + }); + + info("Test that input updates with the keyboard update the picker"); + + // NOTE: After a Tab, the first input field (the month one) is focused, + // so down arrow will change the selected month. + // + // This assumes en-US locale, which seems fine for testing purposes (as + // DATE_FORMAT and other bits around do the same). + BrowserTestUtils.synthesizeKey("KEY_ArrowDown", {}, browser); + + // Toggle the picker on Space anywhere within the input + BrowserTestUtils.synthesizeKey(" ", {}, browser); + + await ready; + + await testCalendarBtnAttribute("aria-expanded", "true"); + + Assert.equal( + helper.panel.state, + "open", + "Panel should be opened on Space from anywhere within the input field" + ); + + Assert.equal( + helper.panel.querySelector("#dateTimePopupFrame").contentDocument + .activeElement.textContent, + "15", + "Picker is opened with a focus set to the currently selected date" + ); + + let monthYearEl = helper.getElement(MONTH_YEAR); + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(prevMonth)); + }, + `Should change to November 2016, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.ok( + true, + "The date on both the Calendar and Month-Year button was updated when updating months with Down arrow key" + ); + + closed = helper.promisePickerClosed(); + + // Close on Escape and return the focus to the input field (the month input in en-US locale) + EventUtils.synthesizeKey("KEY_Escape", {}, window); + + await closed; + + Assert.equal( + helper.panel.state, + "closed", + "Panel should be closed on Escape" + ); + + // Check the focus is returned to the Month field + await SpecialPowers.spawn(browser, [], async () => { + const input = content.document.querySelector("input"); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + // Separators "/" are odd children of the wrapper + const monthField = shadowRoot.getElementById("edit-wrapper").children[0]; + // Testing the focus position within content: + Assert.equal( + input, + content.document.activeElement, + `The input field includes programmatic focus` + ); + // Testing the focus indication within the shadow-root: + Assert.ok( + monthField.matches(":focus"), + `The keyboard focus was returned to the Month field` + ); + }); + + // Move focus to the second field (the day input in en-US locale) + BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, browser); + + // Change the day to 2016-12-16 + BrowserTestUtils.synthesizeKey("KEY_ArrowUp", {}, browser); + + ready = helper.waitForPickerReady(); + + // Open the picker on Space within the input to check the date update + await BrowserTestUtils.synthesizeKey(" ", {}, browser); + + await ready; + + await testCalendarBtnAttribute("aria-expanded", "true"); + + Assert.equal(helper.panel.state, "open", "Panel should be opened on Space"); + + let selectedDayEl = helper.getElement(DAY_SELECTED); + await BrowserTestUtils.waitForMutationCondition( + selectedDayEl, + { childList: true }, + () => { + return selectedDayEl.textContent === "16"; + }, + `Should change to the 16th, instead got ${ + helper.getElement(DAY_SELECTED).textContent + }` + ); + + Assert.ok( + true, + "The date on the Calendar was updated when updating days with Up arrow key" + ); + + closed = helper.promisePickerClosed(); + + // Close on Escape and return the focus to the input field (the day input in en-US locale) + EventUtils.synthesizeKey("KEY_Escape", {}, window); + + await closed; + + Assert.equal( + helper.panel.state, + "closed", + "Panel should be closed on Escape" + ); + + await testCalendarBtnAttribute("aria-expanded", "false"); + + // Check the focus is returned to the Day field + await SpecialPowers.spawn(browser, [], async () => { + const input = content.document.querySelector("input"); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + // Separators "/" are odd children of the wrapper + const dayField = shadowRoot.getElementById("edit-wrapper").children[2]; + // Testing the focus position within content: + Assert.equal( + input, + content.document.activeElement, + `The input field includes programmatic focus` + ); + // Testing the focus indication within the shadow-root: + Assert.ok( + dayField.matches(":focus"), + `The keyboard focus was returned to the Day field` + ); + }); + + info("Test the Calendar button can toggle the picker with Enter/Space"); + + // Move focus to the Calendar button + BrowserTestUtils.synthesizeKey("KEY_Tab", {}, browser); + BrowserTestUtils.synthesizeKey("KEY_Tab", {}, browser); + + // Toggle the picker on Enter on Calendar button + await BrowserTestUtils.synthesizeKey("KEY_Enter", {}, browser); + + await helper.waitForPickerReady(); + + Assert.equal( + helper.panel.state, + "open", + "Panel should be opened on Enter from the Calendar button" + ); + + await testCalendarBtnAttribute("aria-expanded", "true"); + + // Move focus from 2016-11-16 to 2016-11-17 + EventUtils.synthesizeKey("KEY_ArrowRight", {}); + + // Make a selection by pressing Space on date gridcell + await EventUtils.synthesizeKey(" ", {}); + + await helper.promisePickerClosed(); + + Assert.equal( + helper.panel.state, + "closed", + "Panel should be closed on Space from the date gridcell" + ); + await testCalendarBtnAttribute("aria-expanded", "false"); + + // Check the focus is returned to the Calendar button + await SpecialPowers.spawn(browser, [], async () => { + const input = content.document.querySelector("input"); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + const calendarBtn = shadowRoot.getElementById("calendar-button"); + // Testing the focus position within content: + Assert.equal( + input, + content.document.activeElement, + `The input field includes programmatic focus` + ); + // Testing the focus indication within the shadow-root: + Assert.ok( + calendarBtn.matches(":focus"), + `The keyboard focus was returned to the Calendar button` + ); + }); + + // Check the Backspace on Calendar button is not doing anything + await EventUtils.synthesizeKey("KEY_Backspace", {}); + + // The Calendar button is on its place and the input value is not changed + // (bug 1804669) + await SpecialPowers.spawn(browser, [], () => { + const input = content.document.querySelector("input"); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + const calendarBtn = shadowRoot.getElementById("calendar-button"); + Assert.equal( + calendarBtn.children[0].tagName, + "svg", + `Calendar button has an <svg> child` + ); + Assert.equal(input.value, "2016-11-17", `Input's value is not removed`); + }); + + // Toggle the picker on Space on Calendar button + await EventUtils.synthesizeKey(" ", {}); + + await helper.waitForPickerReady(); + + Assert.equal( + helper.panel.state, + "open", + "Panel should be opened on Space from the Calendar button" + ); + + await testCalendarBtnAttribute("aria-expanded", "true"); + + await helper.tearDown(); +}); + +/** + * Ensure calendar follows Arrow key bindings appropriately. + */ +add_task(async function test_datepicker_keyboard_arrows() { + info("Ensure calendar follows Arrow key bindings appropriately."); + + const inputValue = "2016-12-10"; + const prevMonth = "2016-11-01"; + await helper.openPicker( + `data:text/html,<input id=date type=date value=${inputValue}>` + ); + let pickerDoc = helper.panel.querySelector( + "#dateTimePopupFrame" + ).contentDocument; + Assert.equal(helper.panel.state, "open", "Panel should be opened"); + + // Move focus from 2016-12-10 to 2016-12-11: + EventUtils.synthesizeKey("KEY_ArrowRight", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "11", + "Arrow Right moves focus to the next day" + ); + + // Move focus from 2016-12-11 to 2016-12-04: + EventUtils.synthesizeKey("KEY_ArrowUp", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "4", + "Arrow Up moves focus to the same weekday of the previous week" + ); + + // Move focus from 2016-12-04 to 2016-12-03: + EventUtils.synthesizeKey("KEY_ArrowLeft", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "3", + "Arrow Left moves focus to the previous day" + ); + + // Move focus from 2016-12-03 to 2016-11-26: + EventUtils.synthesizeKey("KEY_ArrowUp", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "26", + "Arrow Up updates the view to be on the previous month, if needed" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)), + "Arrow Up updates the spinner to show the previous month, if needed" + ); + + // Move focus from 2016-11-26 to 2016-12-03: + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + Assert.equal( + pickerDoc.activeElement.textContent, + "3", + "Arrow Down updates the view to be on the next month, if needed" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)), + "Arrow Down updates the spinner to show the next month, if needed" + ); + + // Move focus from 2016-12-03 to 2016-12-10: + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "10", + "Arrow Down moves focus to the same day of the next week" + ); + + await helper.tearDown(); +}); + +/** + * Ensure calendar follows Home/End key bindings appropriately. + */ +add_task(async function test_datepicker_keyboard_home_end() { + info("Ensure calendar follows Home/End key bindings appropriately."); + + const inputValue = "2016-12-15"; + const prevMonth = "2016-11-01"; + await helper.openPicker( + `data:text/html,<input id=date type=date value=${inputValue}>` + ); + let pickerDoc = helper.panel.querySelector( + "#dateTimePopupFrame" + ).contentDocument; + Assert.equal(helper.panel.state, "open", "Panel should be opened"); + + // Move focus from 2016-12-15 to 2016-12-11 (in the en-US locale): + EventUtils.synthesizeKey("KEY_Home", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "11", + "Home key moves focus to the first day/Sunday of the current week" + ); + + // Move focus from 2016-12-11 to 2016-12-17 (in the en-US locale): + EventUtils.synthesizeKey("KEY_End", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "17", + "End key moves focus to the last day/Saturday of the current week" + ); + + // Move focus from 2016-12-17 to 2016-12-31: + EventUtils.synthesizeKey("KEY_End", { ctrlKey: true }); + + Assert.equal( + pickerDoc.activeElement.textContent, + "31", + "Ctrl + End keys move focus to the last day of the current month" + ); + + // Move focus from 2016-12-31 to 2016-12-01: + EventUtils.synthesizeKey("KEY_Home", { ctrlKey: true }); + + Assert.equal( + pickerDoc.activeElement.textContent, + "1", + "Ctrl + Home keys move focus to the first day of the current month" + ); + + // Move focus from 2016-12-01 to 2016-11-27 (in the en-US locale): + EventUtils.synthesizeKey("KEY_Home", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "27", + "Home key updates the view to be on the previous month, if needed" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)), + "Home key updates the spinner to show the previous month, if needed" + ); + + // Move focus from 2016-11-27 to 2016-12-03 (in the en-US locale): + EventUtils.synthesizeKey("KEY_End", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "3", + "End key updates the view to be on the next month, if needed" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)), + "End key updates the spinner to show the next month, if needed" + ); + + await helper.tearDown(); +}); + +/** + * Ensure calendar follows Page Up/Down key bindings appropriately. + */ +add_task(async function test_datepicker_keyboard_pgup_pgdown() { + info("Ensure calendar follows Page Up/Down key bindings appropriately."); + + const inputValue = "2023-01-31"; + const prevMonth = "2022-12-31"; + const prevYear = "2021-12-01"; + const nextMonth = "2023-01-31"; + const nextShortMonth = "2023-03-03"; + await helper.openPicker( + `data:text/html,<input id=date type=date value=${inputValue}>` + ); + let pickerDoc = helper.panel.querySelector( + "#dateTimePopupFrame" + ).contentDocument; + Assert.equal(helper.panel.state, "open", "Panel should be opened"); + + // Move focus from 2023-01-31 to 2022-12-31: + EventUtils.synthesizeKey("KEY_PageUp", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "31", + "Page Up key moves focus to the same day of the previous month" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)), + "Page Up key updates the month-year button to show the previous month" + ); + + // Move focus from 2022-12-31 to 2022-12-01 + // (because 2022-11-31 does not exist): + EventUtils.synthesizeKey("KEY_PageUp", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "1", + `When the same day does not exists in the previous month Page Up key moves + focus to the same day of the same week of the current month` + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)), + `When the same day does not exist in the previous month + Page Up key does not update the month-year button and shows the current month` + ); + + // Move focus from 2022-12-01 to 2021-12-01: + EventUtils.synthesizeKey("KEY_PageUp", { shiftKey: true }); + Assert.equal( + pickerDoc.activeElement.textContent, + "1", + "Page Up with Shift key moves focus to the same day of the same month of the previous year" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevYear)), + "Page Up with Shift key updates the month-year button to show the same month of the previous year" + ); + + // Move focus from 2021-12-01 to 2022-12-01 month by month (bug 1806645): + EventUtils.synthesizeKey("KEY_PageDown", { repeat: 12 }); + Assert.equal( + pickerDoc.activeElement.textContent, + "1", + "When repeated, Page Down key moves focus to the same day of the same month of the next year" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)), + "When repeated, Page Down key updates the month-year button to show the same month of the next year" + ); + + // Move focus from 2022-12-01 to 2021-12-01 month by month (bug 1806645): + EventUtils.synthesizeKey("KEY_PageUp", { repeat: 12 }); + Assert.equal( + pickerDoc.activeElement.textContent, + "1", + "When repeated, Page Up moves focus to the same day of the same month of the previous year" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevYear)), + "When repeated, Page Up key updates the month-year button to show the same month of the previous year" + ); + + // Move focus from 2021-12-01 to 2022-12-01: + EventUtils.synthesizeKey("KEY_PageDown", { shiftKey: true }); + Assert.equal( + pickerDoc.activeElement.textContent, + "1", + "Page Down with Shift key moves focus to the same day of the same month of the next year" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)), + "Page Down with Shift key updates the month-year button to show the same month of the next year" + ); + + // Move focus from 2016-12-01 to 2016-12-31: + EventUtils.synthesizeKey("KEY_End", { ctrlKey: true }); + // Move focus from 2022-12-31 to 2023-01-31: + EventUtils.synthesizeKey("KEY_PageDown", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "31", + "Page Down key moves focus to the same day of the next month" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextMonth)), + "Page Down key updates the month-year button to show the next month" + ); + + // Move focus from 2023-01-31 to 2023-03-03: + EventUtils.synthesizeKey("KEY_PageDown", {}); + + Assert.equal( + pickerDoc.activeElement.textContent, + "3", + `When the same day does not exists in the next month, Page Down key moves + focus to the same day of the same week of the month after` + ); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextShortMonth)), + "Page Down key updates the month-year button to show the month after" + ); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_markup.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_markup.js new file mode 100644 index 0000000000..efa2fbfeab --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_markup.js @@ -0,0 +1,483 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test that date picker opens with accessible markup + */ +add_task(async function test_datepicker_markup() { + info("Test that the date picker opens with accessible markup"); + + await helper.openPicker("data:text/html, <input type='date'>"); + + Assert.equal( + helper.getElement(DIALOG_PICKER).getAttribute("role"), + "dialog", + "Datepicker dialog has an appropriate ARIA role" + ); + Assert.ok( + helper.getElement(DIALOG_PICKER).getAttribute("aria-modal"), + "Datepicker dialog is a modal" + ); + Assert.equal( + helper.getElement(BTN_PREV_MONTH).tagName, + "button", + "Previous Month control is a button" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).tagName, + "button", + "Month picker view toggle is a button" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).getAttribute("aria-expanded"), + "false", + "Month picker view toggle is collapsed when the dialog is hidden" + ); + Assert.equal( + helper.getElement(MONTH_YEAR).getAttribute("aria-live"), + "polite", + "Month picker view toggle is a live region when it's not expanded" + ); + Assert.ok( + BrowserTestUtils.isHidden(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection spinner is not visible" + ); + Assert.ok( + BrowserTestUtils.isHidden(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection spinner is programmatically hidden" + ); + Assert.equal( + helper.getElement(BTN_NEXT_MONTH).tagName, + "button", + "Next Month control is a button" + ); + Assert.equal( + helper.getElement(DAYS_VIEW).parentNode.tagName, + "table", + "Calendar view is marked up as a table" + ); + Assert.equal( + helper.getElement(DAYS_VIEW).parentNode.getAttribute("role"), + "grid", + "Calendar view is a grid" + ); + Assert.ok( + helper.getElement( + `#${helper + .getElement(DAYS_VIEW) + .parentNode.getAttribute("aria-labelledby")}` + ), + "Calendar view has a valid accessible name" + ); + Assert.equal( + helper.getElement(WEEK_HEADER).firstChild.tagName, + "tr", + "Week headers within the Calendar view are marked up as table rows" + ); + Assert.equal( + helper.getElement(WEEK_HEADER).firstChild.firstChild.tagName, + "th", + "Weekdays within the Calendar view are marked up as header cells" + ); + Assert.equal( + helper.getElement(WEEK_HEADER).firstChild.firstChild.getAttribute("role"), + "columnheader", + "Weekdays within the Calendar view are grid column headers" + ); + Assert.equal( + helper.getElement(DAYS_VIEW).firstChild.tagName, + "tr", + "Weeks within the Calendar view are marked up as table rows" + ); + Assert.equal( + helper.getElement(DAYS_VIEW).firstChild.firstChild.tagName, + "td", + "Days within the Calendar view are marked up as table cells" + ); + Assert.equal( + helper.getElement(DAYS_VIEW).firstChild.firstChild.getAttribute("role"), + "gridcell", + "Days within the Calendar view are gridcells" + ); + Assert.equal( + helper.getElement(BTN_CLEAR).tagName, + "button", + "Clear control is a button" + ); + + await helper.tearDown(); +}); + +/** + * Test that date picker has localizable labels + */ +add_task(async function test_datepicker_l10n() { + info("Test that the date picker has localizable labels"); + + await helper.openPicker("data:text/html, <input type='date'>"); + + const testcases = [ + { + selector: DIALOG_PICKER, + id: "date-picker-label", + args: null, + }, + { + selector: MONTH_YEAR_NAV, + id: "date-spinner-label", + args: null, + }, + { + selector: BTN_PREV_MONTH, + id: "date-picker-previous", + args: null, + }, + { + selector: BTN_NEXT_MONTH, + id: "date-picker-next", + args: null, + }, + { + selector: BTN_CLEAR, + id: "date-picker-clear-button", + args: null, + }, + ]; + + // Check "aria-label" attributes + for (let { selector, id, args } of testcases) { + const el = helper.getElement(selector); + const l10nAttrs = document.l10n.getAttributes(el); + + Assert.ok( + el.hasAttribute("aria-label") || el.textContent, + `Datepicker "${selector}" element has accessible name` + ); + Assert.deepEqual( + l10nAttrs, + { + id, + args, + }, + `Datepicker "${selector}" element's accessible name is localizable` + ); + } + + await helper.tearDown(); +}); + +/** + * Test that date picker opens to today's date, with today's and selected days + * marked up correctly, given a date value is set. + */ +add_task(async function test_datepicker_today_and_selected() { + info("Test today's and selected days' markup when a date value is set"); + + const date = new Date(); + let inputValue = new Date(); + // Both 2 and 10 dates are used as an example only to test that + // the current date and selected dates are marked up differently. + if (date.getDate() === 2) { + inputValue.setDate(10); + } else { + inputValue.setDate(2); + } + inputValue = inputValue.toISOString().split("T")[0]; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}"> ` + ); + + if (date.getMonth() === new Date().getMonth()) { + Assert.notEqual( + helper.getElement(DAY_TODAY), + helper.getElement(DAY_SELECTED), + "Today and selected dates are different" + ); + Assert.equal( + helper.getElement(DAY_TODAY).getAttribute("aria-current"), + "date", + "Today's date is programmatically current" + ); + Assert.equal( + helper.getElement(DAY_SELECTED).getAttribute("aria-selected"), + "true", + "Chosen date is programmatically selected" + ); + Assert.ok( + !helper.getElement(DAY_TODAY).hasAttribute("tabindex"), + "Today is not included in the focus order, when another day is selected" + ); + Assert.equal( + helper.getElement(DAY_SELECTED).getAttribute("tabindex"), + "0", + "Selected date is included in the focus order" + ); + } else { + Assert.ok( + true, + "Skipping datepicker today test if month changes when opening picker." + ); + } + + await helper.tearDown(); +}); + +/** + * Test that date picker refreshes ARIA properties + * after the other month was displayed. + */ +add_task(async function test_datepicker_markup_refresh() { + const inputValue = "2016-12-05"; + const minValue = "2016-12-05"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}" min="${minValue}">` + ); + + const secondRowDec = helper.getChildren(DAYS_VIEW)[1].children; + + // 2016-12-05 Monday is selected (in en_US locale) + if (secondRowDec[1] === helper.getElement(DAY_SELECTED)) { + Assert.equal( + secondRowDec[1].getAttribute("aria-selected"), + "true", + "Chosen date is programmatically selected" + ); + Assert.ok( + !secondRowDec[1].classList.contains("out-of-range"), + "Chosen date is not styled as out-of-range" + ); + Assert.ok( + !secondRowDec[1].hasAttribute("aria-disabled"), + "Chosen date is not programmatically disabled" + ); + // I.e. 2016-12-04 Sunday is out-of-range (in en_US locale) + Assert.ok( + secondRowDec[0].classList.contains("out-of-range"), + "Less than min date is styled as out-of-range" + ); + Assert.equal( + secondRowDec[0].getAttribute("aria-disabled"), + "true", + "Less than min date is programmatically disabled" + ); + + // Change month view from December 2016 to January 2017 + // to check an updated markup + helper.click(helper.getElement(BTN_NEXT_MONTH)); + + const secondRowJan = helper.getChildren(DAYS_VIEW)[1].children; + + // 2017-01-02 Monday is not selected and in-range (in en_US locale) + Assert.equal( + secondRowJan[1].getAttribute("aria-selected"), + "false", + "Day with the same position as selected is not programmatically selected" + ); + Assert.ok( + !secondRowJan[1].classList.contains("out-of-range"), + "Day with the same position as selected is not styled as out-of-range" + ); + Assert.ok( + !secondRowJan[1].hasAttribute("aria-disabled"), + "Day with the same position as selected is not programmatically disabled" + ); + // I.e. 2017-01-01 Sunday is in-range (in en_US locale) + Assert.ok( + !secondRowJan[0].classList.contains("out-of-range"), + "Day with the same as less than min date is not styled as out-of-range" + ); + Assert.ok( + !secondRowJan[0].hasAttribute("aria-disabled"), + "Day with the same as less than min date is not programmatically disabled" + ); + // 2016-12-05 was focused before the change, thus the same day of the month + // is expected to be focused now (2017-01-05): + Assert.equal( + secondRowJan[4].getAttribute("tabindex"), + "0", + "The same day of the month is made focusable" + ); + Assert.ok( + !secondRowJan[0].hasAttribute("tabindex"), + "The first day of the month is not focusable" + ); + Assert.ok( + !secondRowJan[1].hasAttribute("tabindex"), + "Day with the same position as selected is not focusable" + ); + Assert.ok(!helper.getElement(DAY_TODAY), "No date is marked up as today"); + Assert.ok( + !helper.getElement(DAY_SELECTED), + "No date is marked up as selected" + ); + } else { + Assert.ok( + true, + "Skipping datepicker attributes flushing test if the week/locale is different from the en_US used for the test" + ); + } + + await helper.tearDown(); +}); + +/** + * Test that date input field has a Calendar button with an accessible markup + */ +add_task(async function test_calendar_button_markup_date() { + info( + "Test that type=date input field has a Calendar button with an accessible markup" + ); + + await helper.openPicker("data:text/html, <input type='date'>"); + let browser = helper.tab.linkedBrowser; + + Assert.equal(helper.panel.state, "open", "Panel is visible"); + + let closed = helper.promisePickerClosed(); + + await testCalendarBtnAttribute("aria-expanded", "true"); + await testCalendarBtnAttribute("aria-label", null, true); + await testCalendarBtnAttribute("data-l10n-id", "datetime-calendar"); + + await SpecialPowers.spawn(browser, [], () => { + const input = content.document.querySelector("input"); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + const calendarBtn = shadowRoot.getElementById("calendar-button"); + + Assert.equal(calendarBtn.tagName, "BUTTON", "Calendar control is a button"); + Assert.ok( + ContentTaskUtils.isVisible(calendarBtn), + "The Calendar button is visible" + ); + + calendarBtn.click(); + }); + + await closed; + + Assert.equal( + helper.panel.state, + "closed", + "Panel should be closed on click on the Calendar button" + ); + + await testCalendarBtnAttribute("aria-expanded", "false"); + + await helper.tearDown(); +}); + +/** + * Test that datetime-local input field has a Calendar button + * with an accessible markup + */ +add_task(async function test_calendar_button_markup_datetime() { + info( + "Test that type=datetime-local input field has a Calendar button with an accessible markup" + ); + + await helper.openPicker("data:text/html, <input type='datetime-local'>"); + let browser = helper.tab.linkedBrowser; + + Assert.equal(helper.panel.state, "open", "Panel is visible"); + + let closed = helper.promisePickerClosed(); + + await testCalendarBtnAttribute("aria-expanded", "true"); + await testCalendarBtnAttribute("aria-label", null, true); + await testCalendarBtnAttribute("data-l10n-id", "datetime-calendar"); + + await SpecialPowers.spawn(browser, [], () => { + const input = content.document.querySelector("input"); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + const calendarBtn = shadowRoot.getElementById("calendar-button"); + + Assert.equal(calendarBtn.tagName, "BUTTON", "Calendar control is a button"); + Assert.ok( + ContentTaskUtils.isVisible(calendarBtn), + "The Calendar button is visible" + ); + + calendarBtn.click(); + }); + + await closed; + + Assert.equal( + helper.panel.state, + "closed", + "Panel should be closed on click on the Calendar button" + ); + + await testCalendarBtnAttribute("aria-expanded", "false"); + + await helper.tearDown(); +}); + +/** + * Test that time input field does not include a Calendar button, + * but opens a time picker panel on click within the field (with a pref) + */ +add_task(async function test_calendar_button_markup_time() { + info("Test that type=time input field does not include a Calendar button"); + + // Toggle a pref to allow a time picker to be shown + await SpecialPowers.pushPrefEnv({ + set: [["dom.forms.datetime.timepicker", true]], + }); + + let testTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html, <input type='time'>" + ); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const input = content.document.querySelector("input"); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + const calendarBtn = shadowRoot.getElementById("calendar-button"); + + Assert.ok( + ContentTaskUtils.isHidden(calendarBtn), + "The Calendar control within a type=time input field is programmatically hidden" + ); + }); + + let ready = helper.waitForPickerReady(); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "input", + {}, + gBrowser.selectedBrowser + ); + + await ready; + + Assert.equal( + helper.panel.state, + "open", + "Time picker panel should be opened on click from anywhere within the time input field" + ); + + let closed = helper.promisePickerClosed(); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "input", + {}, + gBrowser.selectedBrowser + ); + + await closed; + + Assert.equal( + helper.panel.state, + "closed", + "Time picker panel should be closed on click from anywhere within the time input field" + ); + + BrowserTestUtils.removeTab(testTab); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_min_max.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_min_max.js new file mode 100644 index 0000000000..3b0de45672 --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_min_max.js @@ -0,0 +1,405 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Create a list of abbreviations for calendar class names +const W = "weekend", + O = "outside", + S = "selection", + R = "out-of-range", + T = "today", + P = "off-step"; + +// Calendar classlist for 2016-12. Used to verify the classNames are correct. +const calendarClasslist_201612 = [ + [W, O], + [O], + [O], + [O], + [], + [], + [W], + [W], + [], + [], + [], + [], + [], + [W], + [W], + [], + [], + [], + [S], + [], + [W], + [W], + [], + [], + [], + [], + [], + [W], + [W], + [], + [], + [], + [], + [], + [W], + [W, O], + [O], + [O], + [O], + [O], + [O], + [W, O], +]; + +/** + * When min and max attributes are set, calendar should show some dates as + * out-of-range. + */ +add_task(async function test_datepicker_min_max() { + const inputValue = "2016-12-15"; + const inputMin = "2016-12-05"; + const inputMax = "2016-12-25"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">` + ); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // R denotes out-of-range + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + ]), + "2016-12 with min & max" + ); + + Assert.ok( + helper + .getElement(DAYS_VIEW) + .firstChild.firstChild.getAttribute("aria-disabled"), + "An out-of-range date is programmatically disabled" + ); + + Assert.ok( + !helper.getElement(DAY_SELECTED).hasAttribute("aria-disabled"), + "An in-range date is not programmatically disabled" + ); + + await helper.tearDown(); +}); + +add_task(async function test_datepicker_abs_min() { + const inputValue = "0001-01-01"; + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + Assert.deepEqual( + getCalendarText(), + [ + "", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + ], + "0001-01" + ); + + await helper.tearDown(); +}); + +add_task(async function test_datepicker_abs_max() { + const inputValue = "275760-09-13"; + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + Assert.deepEqual( + getCalendarText(), + [ + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + ], + "275760-09" + ); + + await helper.tearDown(); +}); + +// Bug 1726546 +add_task(async function test_datetime_local_min() { + const inputValue = "2016-12-15T04:00"; + const inputMin = "2016-12-05T12:22"; + const inputMax = "2016-12-25T12:22"; + + await helper.openPicker( + `data:text/html,<input type="datetime-local" value="${inputValue}" min="${inputMin}" max="${inputMax}">` + ); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // R denotes out-of-range + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + ]), + "2016-12 with min & max" + ); + + await helper.tearDown(); +}); + +// Bug 1726546 +add_task(async function test_datetime_local_min_select_invalid() { + const inputValue = "2016-12-15T05:00"; + const inputMin = "2016-12-05T12:22"; + const inputMax = "2016-12-25T12:22"; + + await helper.openPicker( + `data:text/html,<input type="datetime-local" value="${inputValue}" min="${inputMin}" max="${inputMax}">` + ); + + let changePromise = helper.promiseChange(); + + // Select the minimum day (the 5th, which is the 2nd child of 2nd row). + // The date becomes invalid (we select 2016-12-05T05:00). + helper.click(helper.getElement(DAYS_VIEW).children[1].children[1]); + + await changePromise; + + let [value, invalid] = await SpecialPowers.spawn( + helper.tab.linkedBrowser, + [], + async () => { + let input = content.document.querySelector("input"); + return [input.value, input.matches(":invalid")]; + } + ); + + Assert.equal(value, "2016-12-05T05:00", "Value should've changed"); + Assert.ok(invalid, "input should be now invalid"); + + await helper.tearDown(); +}); + +/** + * Test that date picker opens to the minium valid date when the value property is lower than the min property + */ +add_task(async function test_datepicker_value_lower_than_min() { + const date = new Date(); + const inputValue = "2001-02-03"; + const minValue = "2004-05-06"; + const maxValue = "2007-08-09"; + + await helper.openPicker( + `data:text/html, <input type='date' value="${inputValue}" min="${minValue}" max="${maxValue}">` + ); + + if (date.getMonth() === new Date().getMonth()) { + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(minValue)) + ); + } else { + Assert.ok( + true, + "Skipping datepicker value lower than min test if month changes when opening picker." + ); + } + + await helper.tearDown(); +}); + +/** + * Test that date picker opens to the maximum valid date when the value property is higher than the max property + */ +add_task(async function test_datepicker_value_higher_than_max() { + const date = new Date(); + const minValue = "2001-02-03"; + const maxValue = "2004-05-06"; + const inputValue = "2007-08-09"; + + await helper.openPicker( + `data:text/html, <input type='date' value="${inputValue}" min="${minValue}" max="${maxValue}">` + ); + + if (date.getMonth() === new Date().getMonth()) { + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(maxValue)) + ); + } else { + Assert.ok( + true, + "Skipping datepicker value higher than max test if month changes when opening picker." + ); + } + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js new file mode 100644 index 0000000000..e722f883d5 --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js @@ -0,0 +1,209 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Ensure the month-year panel of a date input handles Space and Enter appropriately. + */ +add_task(async function test_monthyear_close_date() { + info( + "Ensure the month-year panel of a date input handles Space and Enter appropriately." + ); + + const inputValue = "2022-11-11"; + + await helper.openPicker( + `data:text/html, <input type="date" value=${inputValue}>` + ); + let pickerDoc = helper.panel.querySelector( + "#dateTimePopupFrame" + ).contentDocument; + + // Move focus from the selected date to the month-year toggle button: + await EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + + // Test a month spinner + await testKeyOnSpinners("KEY_Enter", pickerDoc); + await testKeyOnSpinners(" ", pickerDoc); + + // Test a year spinner + await testKeyOnSpinners("KEY_Enter", pickerDoc, 2); + await testKeyOnSpinners(" ", pickerDoc, 2); + + await helper.tearDown(); +}); + +/** + * Ensure the month-year panel of a datetime-local input handles Space and Enter appropriately. + */ +add_task(async function test_monthyear_close_datetime() { + info( + "Ensure the month-year panel of a datetime-local input handles Space and Enter appropriately." + ); + + const inputValue = "2022-11-11T11:11"; + + await helper.openPicker( + `data:text/html, <input type="datetime-local" value=${inputValue}>` + ); + let pickerDoc = helper.panel.querySelector( + "#dateTimePopupFrame" + ).contentDocument; + + // Move focus from the selected date to the month-year toggle button: + await EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + + // Test a month spinner + await testKeyOnSpinners("KEY_Enter", pickerDoc); + await testKeyOnSpinners(" ", pickerDoc); + + // Test a year spinner + await testKeyOnSpinners("KEY_Enter", pickerDoc, 2); + await testKeyOnSpinners(" ", pickerDoc, 2); + + await helper.tearDown(); +}); + +/** + * Ensure the month-year panel of a date input can be closed with Escape key. + */ +add_task(async function test_monthyear_escape_date() { + info("Ensure the month-year panel of a date input can be closed with Esc."); + + const inputValue = "2022-12-12"; + + await helper.openPicker( + `data:text/html, <input type="date" value=${inputValue}>` + ); + let pickerDoc = helper.panel.querySelector( + "#dateTimePopupFrame" + ).contentDocument; + + // Move focus from the today's date to the month-year toggle button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + + // Test a month spinner + await testKeyOnSpinners("KEY_Escape", pickerDoc); + + // Test a year spinner + await testKeyOnSpinners("KEY_Escape", pickerDoc, 2); + + info( + `Testing "KEY_Escape" behavior without any interaction with spinners + (bug 1815184)` + ); + + Assert.ok( + helper.getElement(BTN_MONTH_YEAR).matches(":focus"), + "The month-year toggle button is focused" + ); + + // Open the month-year selection panel with spinners: + EventUtils.synthesizeKey(" ", {}); + + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"), + "true", + "Month-year button is expanded when the spinners are shown" + ); + Assert.ok( + BrowserTestUtils.isVisible(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection panel is visible" + ); + + // Close the month-year selection panel without interacting with its spinners: + EventUtils.synthesizeKey("KEY_Escape", {}); + + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"), + "false", + "Month-year button is collapsed when the spinners are hidden" + ); + Assert.ok( + BrowserTestUtils.isHidden(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection panel is not visible" + ); + Assert.ok( + helper + .getElement(DAYS_VIEW) + .querySelector('[tabindex="0"]') + .matches(":focus"), + "A focusable day within a calendar grid is focused" + ); + + await helper.tearDown(); +}); + +/** + * Ensure the month-year panel of a datetime-local input can be closed with Escape key. + */ +add_task(async function test_monthyear_escape_datetime() { + info( + "Ensure the month-year panel of a datetime-local input can be closed with Esc." + ); + + const inputValue = "2022-12-12T01:01"; + + await helper.openPicker( + `data:text/html, <input type="datetime-local" value=${inputValue}>` + ); + let pickerDoc = helper.panel.querySelector( + "#dateTimePopupFrame" + ).contentDocument; + + // Move focus from the today's date to the month-year toggle button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + + // Test a month spinner + await testKeyOnSpinners("KEY_Escape", pickerDoc); + + // Test a year spinner + await testKeyOnSpinners("KEY_Escape", pickerDoc, 2); + + info( + `Testing "KEY_Escape" behavior without any interaction with spinners + (bug 1815184)` + ); + + Assert.ok( + helper.getElement(BTN_MONTH_YEAR).matches(":focus"), + "The month-year toggle button is focused" + ); + + // Open the month-year selection panel with spinners: + EventUtils.synthesizeKey(" ", {}); + + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"), + "true", + "Month-year button is expanded when the spinners are shown" + ); + Assert.ok( + BrowserTestUtils.isVisible(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection panel is visible" + ); + + // Close the month-year selection panel without interacting with its spinners: + EventUtils.synthesizeKey("KEY_Escape", {}); + + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"), + "false", + "Month-year button is collapsed when the spinners are hidden" + ); + Assert.ok( + BrowserTestUtils.isHidden(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection panel is not visible" + ); + Assert.ok( + helper + .getElement(DAYS_VIEW) + .querySelector('[tabindex="0"]') + .matches(":focus"), + "A focusable day within a calendar grid is focused" + ); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_mousenav.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_mousenav.js new file mode 100644 index 0000000000..d38992df1b --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_mousenav.js @@ -0,0 +1,201 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * When the previous month button is clicked, calendar should display the dates + * for the previous month. + */ +add_task(async function test_datepicker_prev_month_btn() { + const inputValue = "2016-12-15"; + const prevMonth = "2016-11-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + helper.click(helper.getElement(BTN_PREV_MONTH)); + + // 2016-11-15: + const focusableDay = getDayEl(15); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)) + ); + Assert.deepEqual( + getCalendarText(), + [ + "30", + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + ], + "2016-11" + ); + Assert.equal( + focusableDay.textContent, + "15", + "The same day of the month is present within a calendar grid" + ); + Assert.equal( + focusableDay, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "The same day of the month is focusable within a calendar grid" + ); + + await helper.tearDown(); +}); + +/** + * When the next month button is clicked, calendar should display the dates for + * the next month. + */ +add_task(async function test_datepicker_next_month_btn() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + helper.click(helper.getElement(BTN_NEXT_MONTH)); + + // 2017-01-15: + const focusableDay = getDayEl(15); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextMonth)) + ); + Assert.deepEqual( + getCalendarText(), + [ + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "1", + "2", + "3", + "4", + ], + "2017-01" + ); + Assert.equal( + focusableDay.textContent, + "15", + "The same day of the month is present within a calendar grid" + ); + Assert.equal( + focusableDay, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "The same day of the month is focusable within a calendar grid" + ); + + await helper.tearDown(); +}); + +/** + * When a date on the calendar is clicked, date picker should close and set + * value to the input box. + */ +add_task(async function test_datepicker_clicked() { + info("When a calendar day is clicked, the picker closes, the value is set"); + const inputValue = "2016-12-15"; + const firstDayOnCalendar = "2016-11-27"; + + await helper.openPicker( + `data:text/html, <input id="date" type="date" value="${inputValue}">` + ); + + let browser = helper.tab.linkedBrowser; + Assert.equal(helper.panel.state, "open", "Panel should be opened"); + + // Click the first item (top-left corner) of the calendar + let promise = BrowserTestUtils.waitForContentEvent(browser, "input"); + helper.click(helper.getElement(DAYS_VIEW).querySelector("td")); + await promise; + + let value = await SpecialPowers.spawn(browser, [], () => { + return content.document.querySelector("input").value; + }); + + Assert.equal(value, firstDayOnCalendar); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js new file mode 100644 index 0000000000..1734e6fdc0 --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js @@ -0,0 +1,534 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * When the Previous Month button is pressed, calendar should display + * the dates for the previous month. + */ +add_task(async function test_datepicker_prev_month_btn() { + const inputValue = "2016-12-15"; + const prevMonth = "2016-11-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + // Move focus from the selected date to the Previous Month button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 }); + EventUtils.synthesizeKey(" ", {}); + + // 2016-11-15: + const focusableDay = getDayEl(15); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)) + ); + Assert.deepEqual( + getCalendarText(), + [ + "30", + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + ], + "The calendar is updated to show the previous month (2016-11)" + ); + Assert.ok( + helper.getElement(BTN_PREV_MONTH).matches(":focus"), + "Focus stays on a Previous Month button after it's pressed" + ); + Assert.equal( + focusableDay.textContent, + "15", + "The same day of the month is present within a calendar grid" + ); + Assert.equal( + focusableDay, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "The same day of the month is focusable within a calendar grid" + ); + + // Move focus from the Previous Month button to the same day of the month (2016-11-15): + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + + Assert.ok( + focusableDay.matches(":focus"), + "The same day of the previous month can be focused with a keyboard" + ); + + await helper.tearDown(); +}); + +/** + * When the Next Month button is clicked, calendar should display the dates for + * the next month. + */ +add_task(async function test_datepicker_next_month_btn() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + // Move focus from the selected date to the Next Month button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 4 }); + EventUtils.synthesizeKey(" ", {}); + + // 2017-01-15: + const focusableDay = getDayEl(15); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextMonth)) + ); + Assert.deepEqual( + getCalendarText(), + [ + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "1", + "2", + "3", + "4", + ], + "The calendar is updated to show the next month (2017-01)." + ); + Assert.ok( + helper.getElement(BTN_NEXT_MONTH).matches(":focus"), + "Focus stays on a Next Month button after it's pressed" + ); + Assert.equal( + focusableDay.textContent, + "15", + "The same day of the month is present within a calendar grid" + ); + Assert.equal( + focusableDay, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "The same day of the month is focusable within a calendar grid" + ); + + // Move focus from the Next Month button to the same day of the month (2017-01-15): + EventUtils.synthesizeKey("KEY_Tab", {}); + + Assert.ok( + focusableDay.matches(":focus"), + "The same day of the next month can be focused with a keyboard" + ); + + await helper.tearDown(); +}); + +/** + * When the Previous Month button is pressed, calendar should display + * the dates for the previous month on RTL build (bug 1806823). + */ +add_task(async function test_datepicker_prev_month_btn_rtl() { + const inputValue = "2016-12-15"; + const prevMonth = "2016-11-01"; + + await SpecialPowers.pushPrefEnv({ set: [["intl.l10n.pseudo", "bidi"]] }); + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + // Move focus from the selected date to the Previous Month button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 }); + EventUtils.synthesizeKey(" ", {}); + + // 2016-11-15: + const focusableDay = getDayEl(15); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)), + "The calendar is updated to show the previous month (2016-11)" + ); + Assert.ok( + helper.getElement(BTN_PREV_MONTH).matches(":focus"), + "Focus stays on a Previous Month button after it's pressed" + ); + Assert.equal( + focusableDay.textContent, + "15", + "The same day of the month is present within a calendar grid" + ); + Assert.equal( + focusableDay, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "The same day of the month is focusable within a calendar grid" + ); + + // Move focus from the Previous Month button to the same day of the month (2016-11-15): + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + + Assert.ok( + focusableDay.matches(":focus"), + "The same day of the previous month can be focused with a keyboard" + ); + + await helper.tearDown(); + await SpecialPowers.popPrefEnv(); +}); + +/** + * When the Next Month button is clicked, calendar should display the dates for + * the next month on RTL build (bug 1806823). + */ +add_task(async function test_datepicker_next_month_btn_rtl() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await SpecialPowers.pushPrefEnv({ set: [["intl.l10n.pseudo", "bidi"]] }); + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + // Move focus from the selected date to the Next Month button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 4 }); + EventUtils.synthesizeKey(" ", {}); + + // 2017-01-15: + const focusableDay = getDayEl(15); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextMonth)), + "The calendar is updated to show the next month (2017-01)." + ); + Assert.ok( + helper.getElement(BTN_NEXT_MONTH).matches(":focus"), + "Focus stays on a Next Month button after it's pressed" + ); + Assert.equal( + focusableDay.textContent, + "15", + "The same day of the month is present within a calendar grid" + ); + Assert.equal( + focusableDay, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "The same day of the month is focusable within a calendar grid" + ); + + // Move focus from the Next Month button to the same day of the month (2017-01-15): + EventUtils.synthesizeKey("KEY_Tab", {}); + + Assert.ok( + focusableDay.matches(":focus"), + "The same day of the next month can be focused with a keyboard" + ); + + await helper.tearDown(); + await SpecialPowers.popPrefEnv(); +}); + +/** + * When Previous/Next Month buttons or arrow keys are used to change a month view + * when a time value is incomplete for datetime-local inputs, + * calendar should update the month (bug 1817785). + */ +add_task(async function test_datepicker_reopened_prev_next_month_btn() { + info("Setup a datetime-local datepicker to its reopened state for testing"); + + let inputValueDT = "2023-05-02T01:01"; + let prevMonth = new Date("2023-04-02"); + + await helper.openPicker( + `data:text/html, <input type="datetime-local" value="${inputValueDT}">` + ); + + let closed = helper.promisePickerClosed(); + EventUtils.synthesizeKey("KEY_Escape", {}); + await closed; + + Assert.equal( + helper.panel.state, + "closed", + "Date picker panel should be closed" + ); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const input = content.document.querySelector("input"); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + const editFields = shadowRoot.querySelectorAll(".datetime-edit-field"); + const amPm = editFields[5]; + amPm.focus(); + + Assert.ok( + amPm.matches(":focus"), + "Period of the day within the input is focused" + ); + }); + + // Use Backspace key to clear the value of the AM/PM section of the input + // and wait for input.value to change to null (bug 1833988): + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + const input = content.document.querySelector("input"); + + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeKey("KEY_Backspace", {}, content); + + await ContentTaskUtils.waitForMutationCondition( + input, + { attributeFilter: ["value"] }, + () => input.value == "" + ); + + Assert.ok( + !input.value, + `Expected an input value to be changed to 'null' when a time value became incomplete, instead got ${input.value}` + ); + }); + + let ready = helper.waitForPickerReady(); + + // Move focus to a day section of the input and open a picker: + EventUtils.synthesizeKey("KEY_Tab", {}); + EventUtils.synthesizeKey(" ", {}); + + await ready; + + Assert.equal( + helper.panel.querySelector("#dateTimePopupFrame").contentDocument + .activeElement.textContent, + "2", + "Picker is opened with a focus set to the currently selected date" + ); + + info("Test the Previous Month button behavior"); + + // Move focus from the selected date to the Previous Month button, + // and activate it to move calendar from 2023-05-02 to 2023-04-02: + EventUtils.synthesizeKey("KEY_Tab", { + repeat: 2, + }); + EventUtils.synthesizeKey(" ", {}); + + // Same date of the previous month should be visible and focusable + // (2023-04-02) but the focus should remain on the Previous Month button: + const focusableDayPrevMonth = getDayEl(2); + const monthYearEl = helper.getElement(MONTH_YEAR); + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { + childList: true, + }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(prevMonth)); + }, + `Should change to the previous month (April 2023), instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + Assert.ok( + true, + `The date on both the Calendar and Month-Year button was updated + when Previous Month button was used` + ); + Assert.ok( + helper.getElement(BTN_PREV_MONTH).matches(":focus"), + "Focus stays on a Previous Month button after it's pressed" + ); + Assert.equal( + focusableDayPrevMonth, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "The same day of the month is focusable within a calendar grid" + ); + Assert.equal( + focusableDayPrevMonth.textContent, + "2", + "The same day of the month is present within a calendar grid" + ); + + // Move focus from the Previous Month button to the same day of the month (2023-04-02): + EventUtils.synthesizeKey("KEY_Tab", { + repeat: 3, + }); + + Assert.ok( + focusableDayPrevMonth.matches(":focus"), + "The same day of the previous month can be focused with a keyboard" + ); + + info("Test the Next Month button behavior"); + + // Move focus from the focused date to the Next Month button and activate it, + // (from 2023-04-02 to 2023-05-02): + EventUtils.synthesizeKey("KEY_Tab", { + repeat: 4, + }); + EventUtils.synthesizeKey(" ", {}); + + // Same date of the next month should be visible and focusable + // (2023-05-02) but the focus should remain on the Next Month button: + const focusableDayNextMonth = getDayEl(2); + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { + childList: true, + }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(inputValueDT)); + }, + `Should change to May 2023, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + Assert.ok( + true, + `The date on both the Calendar and Month-Year button was updated + when Next Month button was used` + ); + Assert.ok( + helper.getElement(BTN_NEXT_MONTH).matches(":focus"), + "Focus stays on a Next Month button after it's pressed" + ); + Assert.equal( + focusableDayNextMonth, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "The same day of the month is focusable within a calendar grid" + ); + Assert.equal( + focusableDayNextMonth.textContent, + "2", + "The same day of the month is present within a calendar grid" + ); + + // Move focus from the Next Month button to the focusable day of the month (2023-05-02): + EventUtils.synthesizeKey("KEY_Tab", {}); + + Assert.ok( + focusableDayNextMonth.matches(":focus"), + "The same day of the month can be focused with a keyboard" + ); + + info("Test the arrow navigation behavior"); + + // Move focus from the focused date to the same weekday of the previous month, + // (From 2023-05-02 to 2023-04-25): + EventUtils.synthesizeKey("KEY_ArrowUp", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { + childList: true, + }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(prevMonth)); + }, + `Should change to the previous month, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + Assert.ok( + true, + `The date on both the Calendar and Month-Year button was updated + when an Up Arrow key was used` + ); + + // Move focus from the focused date to the same weekday of the next month, + // (from 2023-04-25 to 2023-05-02): + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { + childList: true, + }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(inputValueDT)); + }, + `Should change to the previous month, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + Assert.ok( + true, + `The date on both the Calendar and Month-Year button was updated + when a Down Arrow key was used` + ); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_showPicker.js b/toolkit/content/tests/browser/datetime/browser_datetime_showPicker.js new file mode 100644 index 0000000000..817c8958cd --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_showPicker.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test that date picker opens with showPicker. + */ +add_task(async function test_datepicker_showPicker() { + const date = new Date(); + + await helper.openPicker( + "data:text/html, <input type='date'>", + false, + "showPicker" + ); + + if (date.getMonth() === new Date().getMonth()) { + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT_LOCAL(date), + "Date picker opens when a showPicker method is called" + ); + } else { + Assert.ok( + true, + "Skipping datepicker today test if month changes when opening picker." + ); + } + + await helper.tearDown(); +}); + +/** + * Test that date picker opens with showPicker and the explicit value. + */ +add_task(async function test_datepicker_showPicker_value() { + await helper.openPicker( + "data:text/html, <input type='date' value='2012-10-15'>", + false, + "showPicker" + ); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT_LOCAL(new Date("2012-10-12")), + "Date picker opens when a showPicker method is called" + ); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_toplevel.js b/toolkit/content/tests/browser/datetime/browser_datetime_toplevel.js new file mode 100644 index 0000000000..2e97e4d2da --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_datetime_toplevel.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + let input = document.createElement("input"); + input.type = "date"; + registerCleanupFunction(() => input.remove()); + document.body.appendChild(input); + + let shown = BrowserTestUtils.waitForDateTimePickerPanelShown(window); + + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + + EventUtils.synthesizeMouseAtCenter( + shadowRoot.getElementById("calendar-button"), + {} + ); + + let popup = await shown; + ok(!!popup, "Should've shown the popup"); + + let hidden = BrowserTestUtils.waitForPopupEvent(popup, "hidden"); + popup.hidePopup(); + + await hidden; + popup.remove(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_spinner.js b/toolkit/content/tests/browser/datetime/browser_spinner.js new file mode 100644 index 0000000000..81ccef39ea --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_spinner.js @@ -0,0 +1,180 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test that the Month spinner opens with an accessible markup + */ +add_task(async function test_spinner_month_markup() { + info("Test that the Month spinner opens with an accessible markup"); + + const inputValue = "2022-09-09"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + helper.click(helper.getElement(MONTH_YEAR)); + + const spinnerMonth = helper.getElement(SPINNER_MONTH); + const spinnerMonthPrev = spinnerMonth.children[0]; + const spinnerMonthBtn = spinnerMonth.children[1]; + const spinnerMonthNext = spinnerMonth.children[2]; + + Assert.equal( + spinnerMonthPrev.tagName, + "button", + "Spinner's Previous Month control is a button" + ); + Assert.equal( + spinnerMonthBtn.getAttribute("role"), + "spinbutton", + "Spinner control is a spinbutton" + ); + Assert.equal( + spinnerMonthBtn.getAttribute("tabindex"), + "0", + "Spinner control is included in the focus order" + ); + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuemin"), + "0", + "Spinner control has a min value set" + ); + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuemax"), + "11", + "Spinner control has a max value set" + ); + // September 2022 as an example + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "8", + "Spinner control has a current value set" + ); + Assert.equal( + spinnerMonthNext.tagName, + "button", + "Spinner's Next Month control is a button" + ); + + testAttribute(spinnerMonthBtn, "aria-valuetext"); + + let visibleEls = spinnerMonthBtn.querySelectorAll( + ":scope > :not([aria-hidden])" + ); + Assert.equal( + visibleEls.length, + 0, + "There should be no children of the spinner without aria-hidden" + ); + + info("Test that the month spinner has localizable labels"); + + testAttributeL10n( + spinnerMonthPrev, + "aria-label", + "date-spinner-month-previous" + ); + testAttributeL10n(spinnerMonthBtn, "aria-label", "date-spinner-month"); + testAttributeL10n(spinnerMonthNext, "aria-label", "date-spinner-month-next"); + + await testReducedMotionProp( + spinnerMonthBtn, + "scroll-behavior", + "smooth", + "auto" + ); + + await helper.tearDown(); +}); + +/** + * Test that the Year spinner opens with an accessible markup + */ +add_task(async function test_spinner_year_markup() { + info("Test that the year spinner opens with an accessible markup"); + + const inputValue = "2022-06-06"; + const inputMin = "2020-06-01"; + const inputMax = "2030-12-31"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">` + ); + helper.click(helper.getElement(MONTH_YEAR)); + + const spinnerYear = helper.getElement(SPINNER_YEAR); + const spinnerYearPrev = spinnerYear.children[0]; + const spinnerYearBtn = spinnerYear.children[1]; + const spinnerYearNext = spinnerYear.children[2]; + + Assert.equal( + spinnerYearPrev.tagName, + "button", + "Spinner's Previous Year control is a button" + ); + Assert.equal( + spinnerYearBtn.getAttribute("role"), + "spinbutton", + "Spinner control is a spinbutton" + ); + Assert.equal( + spinnerYearBtn.getAttribute("tabindex"), + "0", + "Spinner control is included in the focus order" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuemin"), + "2020", + "Spinner control has a min value set, when the range is provided" + ); + // 2020-2030 range is an example + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuemax"), + "2030", + "Spinner control has a max value set, when the range is provided" + ); + // June 2022 is an example + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "Spinner control has a current value set" + ); + Assert.equal( + spinnerYearNext.tagName, + "button", + "Spinner's Next Year control is a button" + ); + + testAttribute(spinnerYearBtn, "aria-valuetext"); + + let visibleEls = spinnerYearBtn.querySelectorAll( + ":scope > :not([aria-hidden])" + ); + Assert.equal( + visibleEls.length, + 0, + "There should be no children of the spinner without aria-hidden" + ); + + info("Test that the year spinner has localizable labels"); + + testAttributeL10n( + spinnerYearPrev, + "aria-label", + "date-spinner-year-previous" + ); + testAttributeL10n(spinnerYearBtn, "aria-label", "date-spinner-year"); + testAttributeL10n(spinnerYearNext, "aria-label", "date-spinner-year-next"); + + await testReducedMotionProp( + spinnerYearBtn, + "scroll-behavior", + "smooth", + "auto" + ); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/datetime/browser_spinner_keynav.js b/toolkit/content/tests/browser/datetime/browser_spinner_keynav.js new file mode 100644 index 0000000000..ece96ce1cf --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser_spinner_keynav.js @@ -0,0 +1,622 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_setup(async function setPrefsReducedMotion() { + // Set "prefers-reduced-motion" media to "reduce" + // to avoid intermittent scroll failures (1803612, 1803687) + await SpecialPowers.pushPrefEnv({ + set: [["ui.prefersReducedMotion", 1]], + }); + Assert.ok( + matchMedia("(prefers-reduced-motion: reduce)").matches, + "The reduce motion mode is active" + ); +}); + +/** + * Ensure the month spinner follows arrow key bindings appropriately. + */ +add_task(async function test_spinner_month_keyboard_arrows() { + info("Ensure the month spinner follows arrow key bindings appropriately."); + + const inputValue = "2022-12-10"; + const nextMonthValue = "2022-01-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + let pickerDoc = helper.panel.querySelector( + "#dateTimePopupFrame" + ).contentDocument; + + info("Testing general keyboard navigation"); + + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"), + "false", + "Month-year button is collapsed when a picker is opened (by default)" + ); + + // Move focus from the selection to the month-year toggle button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + // Open month-year selection panel with spinners: + EventUtils.synthesizeKey(" ", {}); + + const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1]; + const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1]; + + let monthYearEl = helper.getElement(MONTH_YEAR); + + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"), + "true", + "Month-year button is expanded when the spinners are shown" + ); + // December 2022 is an example: + Assert.equal( + pickerDoc.activeElement.textContent, + DATE_FORMAT(new Date(inputValue)), + "Month-year toggle button is focused" + ); + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "11", + "Month Spinner control is ready" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "Year Spinner control is ready" + ); + + // Move focus from the month-year toggle button to the month spinner: + EventUtils.synthesizeKey("KEY_Tab", {}); + + Assert.equal( + pickerDoc.activeElement.getAttribute("aria-valuenow"), + "11", + "Tab moves focus to the month spinner" + ); + + info("Testing Up/Down Arrow keys behavior of the Month Spinner"); + + // Change the month-year from December 2022 to January 2022: + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(nextMonthValue)); + }, + `Should change to January 2022, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "0", + "Down Arrow selects the next month" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "Down Arrow on a month spinner does not update the year" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextMonthValue)), + "Down Arrow updates the month-year button to the next month" + ); + + // Change the month-year from January 2022 to December 2022: + EventUtils.synthesizeKey("KEY_ArrowUp", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue)); + }, + `Should change to December 2022, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "11", + "Up Arrow selects the previous month" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "Up Arrow on a month spinner does not update the year" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)), + "Up Arrow updates the month-year button to the previous month" + ); + + await helper.tearDown(); +}); + +/** + * Ensure the month spinner follows Page Up/Down key bindings appropriately. + */ +add_task(async function test_spinner_month_keyboard_pageup_pagedown() { + info( + "Ensure the month spinner follows Page Up/Down key bindings appropriately." + ); + + const inputValue = "2022-12-10"; + const nextFifthMonthValue = "2022-05-10"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + // const browser = helper.tab.linkedBrowser; + // Move focus from the selection to the month-year toggle button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + // Open month-year selection panel with spinners: + EventUtils.synthesizeKey(" ", {}); + + const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1]; + const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1]; + + let monthYearEl = helper.getElement(MONTH_YEAR); + + // Move focus from the month-year toggle button to the month spinner: + EventUtils.synthesizeKey("KEY_Tab", {}); + + // Change the month-year from December 2022 to May 2022: + EventUtils.synthesizeKey("KEY_PageDown", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return ( + monthYearEl.textContent == DATE_FORMAT(new Date(nextFifthMonthValue)) + ); + }, + `Should change to May 2022, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "4", + "Page Down selects the fifth later month" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "Page Down on a month spinner does not update the year" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextFifthMonthValue)), + "Page Down updates the month-year button to the fifth later month" + ); + + // Change the month-year from May 2022 to December 2022: + EventUtils.synthesizeKey("KEY_PageUp", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue)); + }, + `Should change to December 2022, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "11", + "Page Up selects the fifth earlier month" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "Page Up on a month spinner does not update the year" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)), + "Page Up updates the month-year button to the fifth earlier month" + ); + + await helper.tearDown(); +}); + +/** + * Ensure the month spinner follows Home/End key bindings appropriately. + */ +add_task(async function test_spinner_month_keyboard_home_end() { + info("Ensure the month spinner follows Home/End key bindings appropriately."); + + const inputValue = "2022-12-11"; + const firstMonthValue = "2022-01-11"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + // const browser = helper.tab.linkedBrowser; + // Move focus from the selection to the month-year toggle button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + // Open month-year selection panel with spinners: + EventUtils.synthesizeKey(" ", {}); + + const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1]; + const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1]; + + let monthYearEl = helper.getElement(MONTH_YEAR); + + // Move focus from the month-year toggle button to the month spinner: + EventUtils.synthesizeKey("KEY_Tab", {}); + + // Change the month-year from December 2022 to January 2022: + EventUtils.synthesizeKey("KEY_Home", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(firstMonthValue)); + }, + `Should change to January 2022, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "0", + "Home key selects the first month of the year (min value)" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "Home key does not update the year value" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(firstMonthValue)), + "Home key updates the month-year button to the first month of the same year (min value)" + ); + + // Change the month-year from January 2022 to December 2022: + EventUtils.synthesizeKey("KEY_End", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue)); + }, + `Should change to December 2022, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "11", + "End key selects the last month of the year (max value)" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "End key does not update the year value" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)), + "End key updates the month-year button to the last month of the same year (max value)" + ); + + await helper.tearDown(); +}); + +/** + * Ensure the year spinner follows arrow key bindings appropriately. + */ +add_task(async function test_spinner_year_keyboard_arrows() { + info("Ensure the year spinner follows arrow key bindings appropriately."); + + const inputValue = "2022-12-10"; + const nextYearValue = "2023-12-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + let pickerDoc = helper.panel.querySelector( + "#dateTimePopupFrame" + ).contentDocument; + + info("Testing general keyboard navigation"); + + // Move focus from the selection to the month-year toggle button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + // Open month-year selection panel with spinners: + EventUtils.synthesizeKey(" ", {}); + + const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1]; + const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1]; + + let monthYearEl = helper.getElement(MONTH_YEAR); + + // December 2022 is an example: + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "Year Spinner control is ready" + ); + + // Move focus from the month-year toggle button to the year spinner: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 }); + + Assert.equal( + pickerDoc.activeElement.getAttribute("aria-valuenow"), + "2022", + "Tab can move the focus to the year spinner" + ); + + info("Testing Up/Down Arrow keys behavior of the Year Spinner"); + + // Change the month-year from December 2022 to December 2023: + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(nextYearValue)); + }, + `Should change to December 2023, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "11", + "Down Arrow on the year spinner does not change the month" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2023", + "Down Arrow updates the year to the next" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextYearValue)), + "Down Arrow updates the month-year button to the next year" + ); + + // Change the month-year from December 2023 to December 2022: + EventUtils.synthesizeKey("KEY_ArrowUp", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue)); + }, + `Should change to December 2022, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "11", + "Up Arrow on the year spinner does not change the month" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "Up Arrow updates the year to the previous" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)), + "Up Arrow updates the month-year button to the previous year" + ); + + await helper.tearDown(); +}); + +/** + * Ensure the year spinner follows Page Up/Down key bindings appropriately. + */ +add_task(async function test_spinner_year_keyboard_pageup_pagedown() { + info( + "Ensure the year spinner follows Page Up/Down key bindings appropriately." + ); + + const inputValue = "2022-12-10"; + const nextFifthYearValue = "2027-12-10"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + // const browser = helper.tab.linkedBrowser; + // Move focus from the selection to the month-year toggle button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + // Open month-year selection panel with spinners: + EventUtils.synthesizeKey(" ", {}); + + const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1]; + const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1]; + + let monthYearEl = helper.getElement(MONTH_YEAR); + + // Move focus from the month-year toggle button to the year spinner: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 }); + + // Change the month-year from December 2022 to December 2027: + EventUtils.synthesizeKey("KEY_PageDown", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return ( + monthYearEl.textContent == DATE_FORMAT(new Date(nextFifthYearValue)) + ); + }, + `Should change to December 2027, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "11", + "Page Down on the year spinner does not change the month" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2027", + "Page Down selects the fifth later year" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextFifthYearValue)), + "Page Down updates the month-year button to the fifth later year" + ); + + // Change the month-year from December 2027 to December 2022: + EventUtils.synthesizeKey("KEY_PageUp", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(inputValue)); + }, + `Should change to December 2022, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "11", + "Page Up on the year spinner does not change the month" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2022", + "Page Up selects the fifth earlier year" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)), + "Page Up updates the month-year button to the fifth earlier year" + ); + + await helper.tearDown(); +}); + +/** + * Ensure the year spinner follows Home/End key bindings appropriately. + */ +add_task(async function test_spinner_year_keyboard_home_end() { + info("Ensure the year spinner follows Home/End key bindings appropriately."); + + const inputValue = "2022-12-10"; + const minValue = "2020-10-10"; + const maxValue = "2030-12-31"; + const minYearValue = "2020-12-10"; + const maxYearValue = "2030-12-10"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}" min="${minValue}" max="${maxValue}">` + ); + + // Move focus from the selection to the month-year toggle button: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); + // Open month-year selection panel with spinners: + EventUtils.synthesizeKey(" ", {}); + + const spinnerMonthBtn = helper.getElement(SPINNER_MONTH).children[1]; + const spinnerYearBtn = helper.getElement(SPINNER_YEAR).children[1]; + + let monthYearEl = helper.getElement(MONTH_YEAR); + + // Move focus from the month-year toggle button to the year spinner: + EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 }); + + // Change the month-year from December 2022 to December 2020: + EventUtils.synthesizeKey("KEY_Home", {}); + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(minYearValue)); + }, + `Should change to December 2020, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "11", + "Home key on the year spinner does not change the month" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2020", + "Home key selects the min year value" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(minYearValue)), + "Home key updates the month-year button to the min year value" + ); + + // Change the month-year from December 2022 to December 2030: + EventUtils.synthesizeKey("KEY_End", {}); + + await BrowserTestUtils.waitForMutationCondition( + monthYearEl, + { childList: true }, + () => { + return monthYearEl.textContent == DATE_FORMAT(new Date(maxYearValue)); + }, + `Should change to December 2030, instead got ${ + helper.getElement(MONTH_YEAR).textContent + }` + ); + + Assert.equal( + spinnerMonthBtn.getAttribute("aria-valuenow"), + "11", + "End key on the year spinner does not change the month" + ); + Assert.equal( + spinnerYearBtn.getAttribute("aria-valuenow"), + "2030", + "End key selects the max year value" + ); + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).textContent, + DATE_FORMAT(new Date(maxYearValue)), + "End key updates the month-year button to the max year value" + ); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/datetime/head.js b/toolkit/content/tests/browser/datetime/head.js new file mode 100644 index 0000000000..bbef72873c --- /dev/null +++ b/toolkit/content/tests/browser/datetime/head.js @@ -0,0 +1,441 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Helper class for testing datetime input picker widget + */ +class DateTimeTestHelper { + constructor() { + this.panel = null; + this.tab = null; + this.frame = null; + } + + /** + * Opens a new tab with the URL of the test page, and make sure the picker is + * ready for testing. + * + * @param {String} pageUrl + * @param {bool} inFrame true if input is in the first child frame + * @param {String} openMethod "click" or "showPicker" + */ + async openPicker(pageUrl, inFrame, openMethod = "click") { + this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + let bc = gBrowser.selectedBrowser; + if (inFrame) { + await SpecialPowers.spawn(bc, [], async function () { + const iframe = content.document.querySelector("iframe"); + // Ensure the iframe's position is correct before doing any + // other operations + iframe.getBoundingClientRect(); + }); + bc = bc.browsingContext.children[0]; + } + await SpecialPowers.spawn(bc, [], async function () { + // Ensure that screen coordinates are ok. + await SpecialPowers.contentTransformsReceived(content); + }); + + let shown = this.waitForPickerReady(); + + if (openMethod === "click") { + await SpecialPowers.spawn(bc, [], () => { + const input = content.document.querySelector("input"); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + shadowRoot.getElementById("calendar-button").click(); + }); + } else if (openMethod === "showPicker") { + await SpecialPowers.spawn(bc, [], function () { + content.document.notifyUserGestureActivation(); + content.document.querySelector("input").showPicker(); + }); + } + this.panel = await shown; + this.frame = this.panel.querySelector("#dateTimePopupFrame"); + } + + promisePickerClosed() { + return new Promise(resolve => { + this.panel.addEventListener("popuphidden", resolve, { once: true }); + }); + } + + promiseChange(selector = "input") { + return SpecialPowers.spawn( + this.tab.linkedBrowser, + [selector], + async selector => { + let input = content.document.querySelector(selector); + await ContentTaskUtils.waitForEvent(input, "change", false, e => { + ok( + content.window.windowUtils.isHandlingUserInput, + "isHandlingUserInput should be true" + ); + return true; + }); + } + ); + } + + waitForPickerReady() { + return BrowserTestUtils.waitForDateTimePickerPanelShown(window); + } + + /** + * Find an element on the picker. + * + * @param {String} selector + * @return {DOMElement} + */ + getElement(selector) { + return this.frame.contentDocument.querySelector(selector); + } + + /** + * Find the children of an element on the picker. + * + * @param {String} selector + * @return {Array<DOMElement>} + */ + getChildren(selector) { + return Array.from(this.getElement(selector).children); + } + + /** + * Click on an element + * + * @param {DOMElement} element + */ + click(element) { + EventUtils.synthesizeMouseAtCenter(element, {}, this.frame.contentWindow); + } + + /** + * Close the panel and the tab + */ + async tearDown() { + if (this.panel.state != "closed") { + let pickerClosePromise = this.promisePickerClosed(); + this.panel.hidePopup(); + await pickerClosePromise; + } + BrowserTestUtils.removeTab(this.tab); + this.tab = null; + } + + /** + * Clean up after tests. Remove the frame to prevent leak. + */ + cleanup() { + this.frame?.remove(); + this.frame = null; + this.panel = null; + } +} + +let helper = new DateTimeTestHelper(); + +registerCleanupFunction(() => { + helper.cleanup(); +}); + +const BTN_MONTH_YEAR = "#month-year-label", + BTN_NEXT_MONTH = ".next", + BTN_PREV_MONTH = ".prev", + BTN_CLEAR = "#clear-button", + DAY_SELECTED = ".selection", + DAY_TODAY = ".today", + DAYS_VIEW = ".days-view", + DIALOG_PICKER = "#date-picker", + MONTH_YEAR = ".month-year", + MONTH_YEAR_NAV = ".month-year-nav", + MONTH_YEAR_VIEW = ".month-year-view", + SPINNER_MONTH = "#spinner-month", + SPINNER_YEAR = "#spinner-year", + WEEK_HEADER = ".week-header"; +const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", + timeZone: "UTC", +}).format; +const DATE_FORMAT_LOCAL = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", +}).format; + +/** + * Helper function to find and return a gridcell element + * for a specific day of the month + * + * @return {Array[String]} TextContent of each gridcell within a calendar grid + */ +function getCalendarText() { + let calendarCells = []; + for (const tr of helper.getChildren(DAYS_VIEW)) { + for (const td of tr.children) { + calendarCells.push(td.textContent); + } + } + return calendarCells; +} + +function getCalendarClassList() { + let calendarCellsClasses = []; + for (const tr of helper.getChildren(DAYS_VIEW)) { + for (const td of tr.children) { + calendarCellsClasses.push(td.classList); + } + } + return calendarCellsClasses; +} + +/** + * Helper function to find and return a gridcell element + * for a specific day of the month + * + * @param {Number} day: A day of the month to find in the month grid + * + * @return {HTMLElement} A gridcell that represents the needed day of the month + */ +function getDayEl(dayNum) { + const dayEls = Array.from( + helper.getElement(DAYS_VIEW).querySelectorAll("td") + ); + return dayEls.find(el => el.textContent === dayNum.toString()); +} + +function mergeArrays(a, b) { + return a.map((classlist, index) => classlist.concat(b[index])); +} + +/** + * Helper function to check if a DOM element has a specific attribute + * + * @param {DOMElement} el: DOM Element to be tested + * @param {String} attr: The name of the attribute to be tested + */ +function testAttribute(el, attr) { + Assert.ok( + el.hasAttribute(attr), + `The "${el}" element has a "${attr}" attribute` + ); +} + +/** + * Helper function to check for l10n of an element's attribute + * + * @param {DOMElement} el: DOM Element to be tested + * @param {String} attr: The name of the attribute to be tested + * @param {String} id: Value of the "data-l10n-id" attribute of the element + * @param {Object} args: Args provided by the l10n object of the element + */ +function testAttributeL10n(el, attr, id, args = null) { + testAttribute(el, attr); + testLocalization(el, id, args); +} + +/** + * Helper function to check the value of a Calendar button's specific attribute + * + * @param {String} attr: The name of the attribute to be tested + * @param {String} val: Value that is expected to be assigned to the attribute. + * @param {Boolean} presenceOnly: If "true", test only the presence of the attribute + */ +async function testCalendarBtnAttribute(attr, val, presenceOnly = false) { + let browser = helper.tab.linkedBrowser; + + await SpecialPowers.spawn( + browser, + [attr, val, presenceOnly], + (attr, val, presenceOnly) => { + const input = content.document.querySelector("input"); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + const calendarBtn = shadowRoot.getElementById("calendar-button"); + + if (presenceOnly) { + Assert.ok( + calendarBtn.hasAttribute(attr), + `Calendar button has ${attr} attribute` + ); + } else { + Assert.equal( + calendarBtn.getAttribute(attr), + val, + `Calendar button has ${attr} attribute set to ${val}` + ); + } + } + ); +} + +/** + * Helper function to test if a submission/dismissal keyboard shortcut works + * on a month or a year selection spinner + * + * @param {String} key: A keyboard Event.key that will be synthesized + * @param {Object} document: Reference to the content document + * of the #dateTimePopupFrame + * @param {Number} tabs: How many times "Tab" key should be pressed + * to move a keyboard focus to a needed spinner + * (1 for month/default and 2 for year) + * + * @description Starts with the month-year toggle button being focused + * on the date/datetime-local input's datepicker panel + */ +async function testKeyOnSpinners(key, document, tabs = 1) { + info(`Testing "${key}" key behavior`); + + Assert.equal( + document.activeElement, + helper.getElement(BTN_MONTH_YEAR), + "The month-year toggle button is focused" + ); + + // Open the month-year selection panel with spinners: + await EventUtils.synthesizeKey(" ", {}); + + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"), + "true", + "Month-year button is expanded when the spinners are shown" + ); + Assert.ok( + BrowserTestUtils.isVisible(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection panel is visible" + ); + + // Move focus from the month-year toggle button to one of spinners: + await EventUtils.synthesizeKey("KEY_Tab", { repeat: tabs }); + + Assert.equal( + document.activeElement.getAttribute("role"), + "spinbutton", + "The spinner is focused" + ); + + // Confirm the spinbutton choice and close the month-year selection panel: + await EventUtils.synthesizeKey(key, {}); + + Assert.equal( + helper.getElement(BTN_MONTH_YEAR).getAttribute("aria-expanded"), + "false", + "Month-year button is collapsed when the spinners are hidden" + ); + Assert.ok( + BrowserTestUtils.isHidden(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection panel is not visible" + ); + Assert.equal( + document.activeElement, + helper.getElement(DAYS_VIEW).querySelector('[tabindex="0"]'), + "A focusable day within a calendar grid is focused" + ); + + // Return the focus to the month-year toggle button for future tests + // (passing a Previous button along the way): + await EventUtils.synthesizeKey("KEY_Tab", { repeat: 3 }); +} + +/** + * Helper function to check for localization attributes of a DOM element + * + * @param {DOMElement} el: DOM Element to be tested + * @param {String} id: Value of the "data-l10n-id" attribute of the element + * @param {Object} args: Args provided by the l10n object of the element + */ +function testLocalization(el, id, args = null) { + const l10nAttrs = document.l10n.getAttributes(el); + + Assert.deepEqual( + l10nAttrs, + { + id, + args, + }, + `The "${id}" element is localizable` + ); +} + +/** + * Helper function to check if a CSS property respects reduced motion mode + * + * @param {DOMElement} el: DOM Element to be tested + * @param {String} prop: The name of the CSS property to be tested + * @param {Object} valueNotReduced: Default value of the tested CSS property + * for "prefers-reduced-motion: no-preference" + * @param {String} valueReduced: Value of the tested CSS property + * for "prefers-reduced-motion: reduce" + */ +async function testReducedMotionProp(el, prop, valueNotReduced, valueReduced) { + info(`Test the panel's CSS ${prop} value depending on a reduced motion mode`); + + // Set "prefers-reduced-motion" media to "no-preference" + await SpecialPowers.pushPrefEnv({ + set: [["ui.prefersReducedMotion", 0]], + }); + + ok( + matchMedia("(prefers-reduced-motion: no-preference)").matches, + "The reduce motion mode is not active" + ); + is( + getComputedStyle(el).getPropertyValue(prop), + valueNotReduced, + `Default ${prop} will be provided, when a reduce motion mode is not active` + ); + + // Set "prefers-reduced-motion" media to "reduce" + await SpecialPowers.pushPrefEnv({ + set: [["ui.prefersReducedMotion", 1]], + }); + + ok( + matchMedia("(prefers-reduced-motion: reduce)").matches, + "The reduce motion mode is active" + ); + is( + getComputedStyle(el).getPropertyValue(prop), + valueReduced, + `Reduced ${prop} will be provided, when a reduce motion mode is active` + ); +} + +async function verifyPickerPosition(browsingContext, inputId) { + let inputRect = await SpecialPowers.spawn( + browsingContext, + [inputId], + async function (inputIdChild) { + let rect = content.document + .getElementById(inputIdChild) + .getBoundingClientRect(); + return { + left: content.mozInnerScreenX + rect.left, + bottom: content.mozInnerScreenY + rect.bottom, + }; + } + ); + + function is_close(got, exp, msg) { + // on some platforms we see differences of a fraction of a pixel - so + // allow any difference of < 1 pixels as being OK. + Assert.ok( + Math.abs(got - exp) < 1, + msg + ": " + got + " should be equal(-ish) to " + exp + ); + } + const marginLeft = parseFloat(getComputedStyle(helper.panel).marginLeft); + const marginTop = parseFloat(getComputedStyle(helper.panel).marginTop); + is_close( + helper.panel.screenX - marginLeft, + inputRect.left, + "datepicker x position" + ); + is_close( + helper.panel.screenY - marginTop, + inputRect.bottom, + "datepicker y position" + ); +} |