445 lines
13 KiB
JavaScript
445 lines
13 KiB
JavaScript
/* 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, () => {
|
|
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);
|
|
}
|
|
|
|
async closePicker() {
|
|
if (this.panel.state != "closed") {
|
|
let pickerClosePromise = this.promisePickerClosed();
|
|
this.panel.hidePopup();
|
|
await pickerClosePromise;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close the panel and the tab
|
|
*/
|
|
async tearDown() {
|
|
await this.closePicker();
|
|
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"
|
|
);
|
|
}
|