From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- remote/marionette/interaction.sys.mjs | 819 ++++++++++++++++++++++++++++++++++ 1 file changed, 819 insertions(+) create mode 100644 remote/marionette/interaction.sys.mjs (limited to 'remote/marionette/interaction.sys.mjs') diff --git a/remote/marionette/interaction.sys.mjs b/remote/marionette/interaction.sys.mjs new file mode 100644 index 0000000000..c71149a96a --- /dev/null +++ b/remote/marionette/interaction.sys.mjs @@ -0,0 +1,819 @@ +/* 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/marionette/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