diff options
Diffstat (limited to 'toolkit/content/tests/browser/datetime/head.js')
-rw-r--r-- | toolkit/content/tests/browser/datetime/head.js | 441 |
1 files changed, 441 insertions, 0 deletions
diff --git a/toolkit/content/tests/browser/datetime/head.js b/toolkit/content/tests/browser/datetime/head.js new file mode 100644 index 0000000000..ca301a2e46 --- /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.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: 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" + ); +} |