/* 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 */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { setTimeout: "resource://gre/modules/Timer.sys.mjs", accessibility: "chrome://remote/content/marionette/accessibility.sys.mjs", atom: "chrome://remote/content/marionette/atom.sys.mjs", dom: "chrome://remote/content/shared/DOM.sys.mjs", error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", event: "chrome://remote/content/shared/webdriver/Event.sys.mjs", Log: "chrome://remote/content/shared/Log.sys.mjs", pprint: "chrome://remote/content/shared/Format.sys.mjs", TimedPromise: "chrome://remote/content/marionette/sync.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get(lazy.Log.TYPES.MARIONETTE) ); // dragService may be null if it's in the headless mode (e.g., on Linux). // It depends on the platform, though. ChromeUtils.defineLazyGetter(lazy, "dragService", () => { try { return Cc["@mozilla.org/widget/dragservice;1"].getService( Ci.nsIDragService ); } catch (e) { // If we're in the headless mode, the drag service may be never // instantiated. In this case, an exception is thrown. Let's ignore // any exceptions since without the drag service, nobody can create a // drag session. return null; } }); /** 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 */ export const 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 = lazy.accessibility.get(strict); if (lazy.dom.isXULElement(el)) { await chromeClick(el, a11y); } else if (specCompat) { await webdriverClickElement(el, a11y); } else { lazy.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 lazy.error.InvalidArgumentError( "Cannot click elements" ); } let containerEl = lazy.dom.getContainer(el); // step 4 if (!lazy.dom.isInView(containerEl)) { lazy.dom.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 (!lazy.dom.isInView(containerEl)) { throw new lazy.error.ElementNotInteractableError( lazy.pprint`Element ${el} could not be scrolled into view` ); } // step 7 let rects = containerEl.getClientRects(); let clickPoint = lazy.dom.getInViewCentrePoint(rects[0], win); if (lazy.dom.isObscured(containerEl)) { throw new lazy.error.ElementClickInterceptedError( null, {}, containerEl, clickPoint ); } let acc = await a11y.assertAccessible(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 { // Synthesize a pointerMove action. lazy.event.synthesizeMouseAtPoint( clickPoint.x, clickPoint.y, { type: "mousemove", allowToHandleDragDrop: true, }, win ); if (lazy.dragService?.getCurrentSession()) { // Special handling is required if the mousemove started a drag session. // In this case, mousedown event shouldn't be fired, and the mouseup should // end the session. Therefore, we should synthesize only mouseup. lazy.event.synthesizeMouseAtPoint( clickPoint.x, clickPoint.y, { type: "mouseup", allowToHandleDragDrop: true, }, win ); } else { // step 9 let clicked = interaction.flushEventLoop(containerEl); // Synthesize a pointerDown + pointerUp action. lazy.event.synthesizeMouseAtPoint( clickPoint.x, clickPoint.y, { allowToHandleDragDrop: true }, win ); await clicked; } } // step 10 // if the click causes navigation, the post-navigation checks are // handled by navigate.js } async function chromeClick(el, a11y) { const win = getWindow(el); if (!(await lazy.atom.isElementEnabled(el, win))) { throw new lazy.error.InvalidElementStateError("Element is not enabled"); } let acc = await a11y.assertAccessible(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 = lazy.dom.getContainer(el); } if (!(await lazy.dom.isVisible(visibilityCheckEl))) { throw new lazy.error.ElementNotInteractableError(); } if (!(await lazy.atom.isElementEnabled(el, win))) { throw new lazy.error.InvalidElementStateError("Element is not enabled"); } let acc = await a11y.assertAccessible(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 = lazy.dom.getInViewCentrePoint(rects[0], win); let opts = {}; lazy.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} el * 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 (lazy.dom.isXULElement(el)) { throw new TypeError("XUL dropdowns not supported"); } if (el.localName != "option") { throw new TypeError(lazy.pprint`Expected