/* 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/. */
/* eslint-disable no-restricted-globals */
"use strict";
const EXPORTED_SYMBOLS = ["interaction"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
Preferences: "resource://gre/modules/Preferences.jsm",
accessibility: "chrome://marionette/content/accessibility.js",
atom: "chrome://marionette/content/atom.js",
element: "chrome://marionette/content/element.js",
error: "chrome://marionette/content/error.js",
event: "chrome://marionette/content/event.js",
Log: "chrome://marionette/content/log.js",
pprint: "chrome://marionette/content/format.js",
TimedPromise: "chrome://marionette/content/sync.js",
});
XPCOMUtils.defineLazyGlobalGetters(this, ["File"]);
XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
/** XUL elements that support disabled attribute. */
const DISABLED_ATTRIBUTE_SUPPORTED_XUL = new Set([
"ARROWSCROLLBOX",
"BUTTON",
"CHECKBOX",
"COMMAND",
"DESCRIPTION",
"KEY",
"KEYSET",
"LABEL",
"MENU",
"MENUITEM",
"MENULIST",
"MENUSEPARATOR",
"RADIO",
"RADIOGROUP",
"RICHLISTBOX",
"RICHLISTITEM",
"TAB",
"TABS",
"TOOLBARBUTTON",
"TREE",
]);
/**
* Common form controls that user can change the value property
* interactively.
*/
const COMMON_FORM_CONTROLS = new Set(["input", "textarea", "select"]);
/**
* Input elements that do not fire input and change
* events when value property changes.
*/
const INPUT_TYPES_NO_EVENT = new Set([
"checkbox",
"radio",
"file",
"hidden",
"image",
"reset",
"button",
"submit",
]);
/** @namespace */
this.interaction = {};
/**
* Interact with an element by clicking it.
*
* The element is scrolled into view before visibility- or interactability
* checks are performed.
*
* Selenium-style visibility checks will be performed
* if specCompat is false (default). Otherwise
* pointer-interactability checks will be performed. If either of these
* fail an {@link ElementNotInteractableError} is thrown.
*
* If strict is enabled (defaults to disabled), further
* accessibility checks will be performed, and these may result in an
* {@link ElementNotAccessibleError} being returned.
*
* When el is not enabled, an {@link InvalidElementStateError}
* is returned.
*
* @param {(DOMElement|XULElement)} el
* Element to click.
* @param {boolean=} [strict=false] strict
* Enforce strict accessibility tests.
* @param {boolean=} [specCompat=false] specCompat
* Use WebDriver specification compatible interactability definition.
*
* @throws {ElementNotInteractableError}
* If either Selenium-style visibility check or
* pointer-interactability check fails.
* @throws {ElementClickInterceptedError}
* If el is obscured by another element and a click would
* not hit, in specCompat mode.
* @throws {ElementNotAccessibleError}
* If strict is true and element is not accessible.
* @throws {InvalidElementStateError}
* If el is not enabled.
*/
interaction.clickElement = async function(
el,
strict = false,
specCompat = false
) {
const a11y = accessibility.get(strict);
if (element.isXULElement(el)) {
await chromeClick(el, a11y);
} else if (specCompat) {
await webdriverClickElement(el, a11y);
} else {
logger.trace(`Using non spec-compatible element click`);
await seleniumClickElement(el, a11y);
}
};
async function webdriverClickElement(el, a11y) {
const win = getWindow(el);
// step 3
if (el.localName == "input" && el.type == "file") {
throw new error.InvalidArgumentError(
"Cannot click elements"
);
}
let containerEl = element.getContainer(el);
// step 4
if (!element.isInView(containerEl)) {
element.scrollIntoView(containerEl);
}
// step 5
// TODO(ato): wait for containerEl to be in view
// step 6
// if we cannot bring the container element into the viewport
// there is no point in checking if it is pointer-interactable
if (!element.isInView(containerEl)) {
throw new error.ElementNotInteractableError(
pprint`Element ${el} could not be scrolled into view`
);
}
// step 7
let rects = containerEl.getClientRects();
let clickPoint = element.getInViewCentrePoint(rects[0], win);
if (element.isObscured(containerEl)) {
throw new error.ElementClickInterceptedError(containerEl, clickPoint);
}
let acc = await a11y.getAccessible(el, true);
a11y.assertVisible(acc, el, true);
a11y.assertEnabled(acc, el, true);
a11y.assertActionable(acc, el);
// step 8
if (el.localName == "option") {
interaction.selectOption(el);
} else {
// step 9
let clicked = interaction.flushEventLoop(containerEl);
// Synthesize a pointerMove action.
event.synthesizeMouseAtPoint(
clickPoint.x,
clickPoint.y,
{
type: "mousemove",
// Remove buttons attribute with https://bugzilla.mozilla.org/show_bug.cgi?id=1686361
buttons: 0,
},
win
);
// Synthesize a pointerDown + pointerUp action.
event.synthesizeMouseAtPoint(clickPoint.x, clickPoint.y, {}, win);
await clicked;
}
// step 10
// if the click causes navigation, the post-navigation checks are
// handled by the load listener in listener.js
}
async function chromeClick(el, a11y) {
if (!atom.isElementEnabled(el)) {
throw new error.InvalidElementStateError("Element is not enabled");
}
let acc = await a11y.getAccessible(el, true);
a11y.assertVisible(acc, el, true);
a11y.assertEnabled(acc, el, true);
a11y.assertActionable(acc, el);
if (el.localName == "option") {
interaction.selectOption(el);
} else {
el.click();
}
}
async function seleniumClickElement(el, a11y) {
let win = getWindow(el);
let visibilityCheckEl = el;
if (el.localName == "option") {
visibilityCheckEl = element.getContainer(el);
}
if (!element.isVisible(visibilityCheckEl)) {
throw new error.ElementNotInteractableError();
}
if (!atom.isElementEnabled(el)) {
throw new error.InvalidElementStateError("Element is not enabled");
}
let acc = await a11y.getAccessible(el, true);
a11y.assertVisible(acc, el, true);
a11y.assertEnabled(acc, el, true);
a11y.assertActionable(acc, el);
if (el.localName == "option") {
interaction.selectOption(el);
} else {
let rects = el.getClientRects();
let centre = element.getInViewCentrePoint(rects[0], win);
let opts = {};
event.synthesizeMouseAtPoint(centre.x, centre.y, opts, win);
}
}
/**
* Select <option> element in a <select>
* list.
*
* Because the dropdown list of select elements are implemented using
* native widget technology, our trusted synthesised events are not able
* to reach them. Dropdowns are instead handled mimicking DOM events,
* which for obvious reasons is not ideal, but at the current point in
* time considered to be good enough.
*
* @param {HTMLOptionElement} option
* Option element to select.
*
* @throws {TypeError}
* If el is a XUL element or not an <option>
* element.
* @throws {Error}
* If unable to find el's parent <select>
* element.
*/
interaction.selectOption = function(el) {
if (element.isXULElement(el)) {
throw new TypeError("XUL dropdowns not supported");
}
if (el.localName != "option") {
throw new TypeError(pprint`Expected