/* 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 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,