/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const MONTH_YEAR = ".month-year",
BTN_MONTH_YEAR = "#month-year-label",
MONTH_YEAR_VIEW = ".month-year-view",
BTN_PREV_MONTH = ".prev",
BTN_NEXT_MONTH = ".next",
DAYS_VIEW = ".days-view",
DAY_SELECTED = ".selection";
const DATE_FORMAT = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
timeZone: "UTC",
}).format;
/**
* 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
*/
async function testCalendarBtnAttribute(attr, val) {
let browser = helper.tab.linkedBrowser;
await SpecialPowers.spawn(browser, [attr, val], (attr, val) => {
const input = content.document.querySelector("input");
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
const calendarBtn = shadowRoot.getElementById("calendar-button");
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.is_visible(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.is_hidden(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: 2 });
}
/**
* 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());
}
/**
* 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;
}
let helper = new DateTimeTestHelper();
registerCleanupFunction(() => {
helper.cleanup();
});
/**
* 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,`
);
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