summaryrefslogtreecommitdiffstats
path: root/toolkit/content/tests/browser/datetime
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/tests/browser/datetime')
-rw-r--r--toolkit/content/tests/browser/datetime/browser.toml96
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_blur.js265
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker.js369
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_clear.js56
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_focus.js191
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js576
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_markup.js483
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_min_max.js405
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js209
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_mousenav.js201
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js534
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_showPicker.js52
-rw-r--r--toolkit/content/tests/browser/datetime/browser_datetime_toplevel.js27
-rw-r--r--toolkit/content/tests/browser/datetime/browser_spinner.js180
-rw-r--r--toolkit/content/tests/browser/datetime/browser_spinner_keynav.js622
-rw-r--r--toolkit/content/tests/browser/datetime/head.js441
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"
+ );
+}