diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /remote/marionette/legacyaction.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/marionette/legacyaction.sys.mjs')
-rw-r--r-- | remote/marionette/legacyaction.sys.mjs | 640 |
1 files changed, 640 insertions, 0 deletions
diff --git a/remote/marionette/legacyaction.sys.mjs b/remote/marionette/legacyaction.sys.mjs new file mode 100644 index 0000000000..6790ba55c7 --- /dev/null +++ b/remote/marionette/legacyaction.sys.mjs @@ -0,0 +1,640 @@ +/* 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 */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", + + accessibility: "chrome://remote/content/marionette/accessibility.sys.mjs", + element: "chrome://remote/content/marionette/element.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + json: "chrome://remote/content/marionette/json.sys.mjs", + event: "chrome://remote/content/marionette/event.sys.mjs", + Log: "chrome://remote/content/shared/Log.sys.mjs", + WebElement: "chrome://remote/content/marionette/element.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.MARIONETTE) +); + +const CONTEXT_MENU_DELAY_PREF = "ui.click_hold_context_menus.delay"; +const DEFAULT_CONTEXT_MENU_DELAY = 750; // ms + +/** @namespace */ +export const legacyaction = {}; + +const action = legacyaction; + +/** + * Functionality for (single finger) action chains. + */ +action.Chain = function () { + // for assigning unique ids to all touches + this.nextTouchId = 1000; + // keep track of active Touches + this.touchIds = {}; + // last touch for each fingerId + this.lastCoordinates = null; + this.isTap = false; + this.scrolling = false; + // whether to send mouse event + this.mouseEventsOnly = false; + this.checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + // determines if we create touch events + this.inputSource = null; +}; + +/** + * Create a touch based event. + * + * @param {Element} elem + * The Element on which the touch event should be created. + * @param {number} x + * x coordinate relative to the viewport. + * @param {number} y + * y coordinate relative to the viewport. + * @param {number} touchId + * Touch event id used by legacyactions. + */ +action.Chain.prototype.createATouch = function (elem, x, y, touchId) { + const doc = elem.ownerDocument; + const win = doc.defaultView; + const [clientX, clientY, pageX, pageY, screenX, screenY] = + this.getCoordinateInfo(elem, x, y); + const atouch = doc.createTouch( + win, + elem, + touchId, + pageX, + pageY, + screenX, + screenY, + clientX, + clientY + ); + return atouch; +}; + +action.Chain.prototype.dispatchActions = function ( + args, + touchId, + container, + seenEls +) { + this.seenEls = seenEls; + this.container = container; + let commandArray = lazy.json.deserialize(args, seenEls, container.frame); + + if (touchId == null) { + touchId = this.nextTSouchId++; + } + + if (!container.frame.document.createTouch) { + this.mouseEventsOnly = true; + } + + let keyModifiers = { + shiftKey: false, + ctrlKey: false, + altKey: false, + metaKey: false, + }; + + return new Promise(resolve => { + this.actions(commandArray, touchId, 0, keyModifiers, resolve); + }).catch(this.resetValues.bind(this)); +}; + +/** + * This function emit mouse event. + * + * @param {Document} doc + * Current document. + * @param {string} type + * Type of event to dispatch. + * @param {number} elClientX + * X coordinate of the mouse relative to the viewport. + * @param {number} elClientY + * Y coordinate of the mouse relative to the viewport. + * @param {number} button + * The button number. + * @param {number} clickCount + * Number of clicks, button notes the mouse button. + * @param {object} modifiers + * An object of modifier keys present. + */ +action.Chain.prototype.emitMouseEvent = function ( + doc, + type, + elClientX, + elClientY, + button, + clickCount, + modifiers +) { + lazy.logger.debug( + `Emitting ${type} mouse event ` + + `at coordinates (${elClientX}, ${elClientY}) ` + + `relative to the viewport, ` + + `button: ${button}, ` + + `clickCount: ${clickCount}` + ); + + let win = doc.defaultView; + let domUtils = win.windowUtils; + + let mods; + if (typeof modifiers != "undefined") { + mods = lazy.event.parseModifiers_(modifiers, win); + } else { + mods = 0; + } + + domUtils.sendMouseEvent( + type, + elClientX, + elClientY, + button || 0, + clickCount || 1, + mods, + false, + 0, + this.inputSource + ); +}; + +action.Chain.prototype.emitTouchEvent = function (doc, type, touch) { + lazy.logger.info( + `Emitting Touch event of type ${type} ` + + `to element with id: ${touch.target.id} ` + + `and tag name: ${touch.target.tagName} ` + + `at coordinates (${touch.clientX}), ` + + `${touch.clientY}) relative to the viewport` + ); + + const win = doc.defaultView; + if (win.docShell.asyncPanZoomEnabled && this.scrolling) { + lazy.logger.debug( + `Cannot emit touch event with asyncPanZoomEnabled and legacyactions.scrolling` + ); + return; + } + + // we get here if we're not in asyncPacZoomEnabled land, or if we're + // the main process + win.windowUtils.sendTouchEvent( + type, + [touch.identifier], + [touch.clientX], + [touch.clientY], + [touch.radiusX], + [touch.radiusY], + [touch.rotationAngle], + [touch.force], + [0], + [0], + [0], + 0 + ); +}; + +/** + * Reset any persisted values after a command completes. + */ +action.Chain.prototype.resetValues = function () { + this.container = null; + this.seenEls = null; + this.mouseEventsOnly = false; +}; + +/** + * Function that performs a single tap. + */ +action.Chain.prototype.singleTap = async function ( + el, + corx, + cory, + capabilities +) { + const doc = el.ownerDocument; + // after this block, the element will be scrolled into view + let visible = lazy.element.isVisible(el, corx, cory); + if (!visible) { + throw new lazy.error.ElementNotInteractableError( + "Element is not currently visible and may not be manipulated" + ); + } + + let a11y = lazy.accessibility.get(capabilities["moz:accessibilityChecks"]); + let acc = await a11y.assertAccessible(el, true); + a11y.assertVisible(acc, el, visible); + a11y.assertActionable(acc, el); + if (!doc.createTouch) { + this.mouseEventsOnly = true; + } + let c = lazy.element.coordinates(el, corx, cory); + if (!this.mouseEventsOnly) { + let touchId = this.nextTouchId++; + let touch = this.createATouch(el, c.x, c.y, touchId); + this.emitTouchEvent(doc, "touchstart", touch); + this.emitTouchEvent(doc, "touchend", touch); + } + this.mouseTap(doc, c.x, c.y); +}; + +/** + * Emit events for each action in the provided chain. + * + * To emit touch events for each finger, one might send a [["press", id], + * ["wait", 5], ["release"]] chain. + * + * @param {Array.<Array<?>>} chain + * A multi-dimensional array of actions. + * @param {Object<string, number>} touchId + * Represents the finger ID. + * @param {number} i + * Keeps track of the current action of the chain. + * @param {Object<string, boolean>} keyModifiers + * Keeps track of keyDown/keyUp pairs through an action chain. + * @param {function(?)} cb + * Called on success. + * + * @returns {Object<string, number>} + * Last finger ID, or an empty object. + */ +action.Chain.prototype.actions = function ( + chain, + touchId, + i, + keyModifiers, + cb +) { + if (i == chain.length) { + cb(touchId || null); + this.resetValues(); + return; + } + + let pack = chain[i]; + let command = pack[0]; + let webEl; + let el; + let c; + i++; + + if (!["press", "wait", "keyDown", "keyUp", "click"].includes(command)) { + // if mouseEventsOnly, then touchIds isn't used + if (!(touchId in this.touchIds) && !this.mouseEventsOnly) { + this.resetValues(); + throw new lazy.error.WebDriverError("Element has not been pressed"); + } + } + + switch (command) { + case "keyDown": + lazy.event.sendKeyDown(pack[1], keyModifiers, this.container.frame); + this.actions(chain, touchId, i, keyModifiers, cb); + break; + + case "keyUp": + lazy.event.sendKeyUp(pack[1], keyModifiers, this.container.frame); + this.actions(chain, touchId, i, keyModifiers, cb); + break; + + case "click": + webEl = lazy.WebElement.fromUUID(pack[1]); + el = this.seenEls.get(webEl); + let button = pack[2]; + let clickCount = pack[3]; + c = lazy.element.coordinates(el); + this.mouseTap( + el.ownerDocument, + c.x, + c.y, + button, + clickCount, + keyModifiers + ); + if (button == 2) { + this.emitMouseEvent( + el.ownerDocument, + "contextmenu", + c.x, + c.y, + button, + clickCount, + keyModifiers + ); + } + this.actions(chain, touchId, i, keyModifiers, cb); + break; + + case "press": + if (this.lastCoordinates) { + this.generateEvents( + "cancel", + this.lastCoordinates[0], + this.lastCoordinates[1], + touchId, + null, + keyModifiers + ); + this.resetValues(); + throw new lazy.error.WebDriverError( + "Invalid Command: press cannot follow an active touch event" + ); + } + + // look ahead to check if we're scrolling, + // needed for APZ touch dispatching + if (i != chain.length && chain[i][0].includes("move")) { + this.scrolling = true; + } + webEl = lazy.WebElement.fromUUID(pack[1]); + el = this.seenEls.get(webEl); + c = lazy.element.coordinates(el, pack[2], pack[3]); + touchId = this.generateEvents("press", c.x, c.y, null, el, keyModifiers); + this.actions(chain, touchId, i, keyModifiers, cb); + break; + + case "release": + this.generateEvents( + "release", + this.lastCoordinates[0], + this.lastCoordinates[1], + touchId, + null, + keyModifiers + ); + this.actions(chain, null, i, keyModifiers, cb); + this.scrolling = false; + break; + + case "move": + webEl = lazy.WebElement.fromUUID(pack[1]); + el = this.seenEls.get(webEl); + c = lazy.element.coordinates(el); + this.generateEvents("move", c.x, c.y, touchId, null, keyModifiers); + this.actions(chain, touchId, i, keyModifiers, cb); + break; + + case "moveByOffset": + this.generateEvents( + "move", + this.lastCoordinates[0] + pack[1], + this.lastCoordinates[1] + pack[2], + touchId, + null, + keyModifiers + ); + this.actions(chain, touchId, i, keyModifiers, cb); + break; + + case "wait": + if (pack[1] != null) { + let time = pack[1] * 1000; + + // standard waiting time to fire contextmenu + let standard = lazy.Preferences.get( + CONTEXT_MENU_DELAY_PREF, + DEFAULT_CONTEXT_MENU_DELAY + ); + + if (time >= standard && this.isTap) { + chain.splice(i, 0, ["longPress"], ["wait", (time - standard) / 1000]); + time = standard; + } + this.checkTimer.initWithCallback( + () => this.actions(chain, touchId, i, keyModifiers, cb), + time, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } else { + this.actions(chain, touchId, i, keyModifiers, cb); + } + break; + + case "cancel": + this.generateEvents( + "cancel", + this.lastCoordinates[0], + this.lastCoordinates[1], + touchId, + null, + keyModifiers + ); + this.actions(chain, touchId, i, keyModifiers, cb); + this.scrolling = false; + break; + + case "longPress": + this.generateEvents( + "contextmenu", + this.lastCoordinates[0], + this.lastCoordinates[1], + touchId, + null, + keyModifiers + ); + this.actions(chain, touchId, i, keyModifiers, cb); + break; + } +}; + +/** + * Given an element and a pair of coordinates, returns an array of the + * form [clientX, clientY, pageX, pageY, screenX, screenY]. + */ +action.Chain.prototype.getCoordinateInfo = function (el, corx, cory) { + let win = el.ownerGlobal; + return [ + corx, // clientX + cory, // clientY + corx + win.pageXOffset, // pageX + cory + win.pageYOffset, // pageY + corx + win.mozInnerScreenX, // screenX + cory + win.mozInnerScreenY, // screenY + ]; +}; + +/** + * @param {string} type + * The event type (eg "tap", "press", ...). + * @param {number} x + * X coordinate of the location to generate the event that is relative + * to the viewport. + * @param {number} y + * Y coordinate of the location to generate the event that is relative + * to the viewport. + * @param {number} touchId + * The current touch id. + * @param {Element} target + * The Element on which the events should be created. + */ +action.Chain.prototype.generateEvents = function ( + type, + x, + y, + touchId, + target, + keyModifiers +) { + this.lastCoordinates = [x, y]; + let doc = this.container.frame.document; + + switch (type) { + case "tap": + if (this.mouseEventsOnly) { + let touch = this.createATouch(target, x, y, touchId); + this.mouseTap( + touch.target.ownerDocument, + touch.clientX, + touch.clientY, + null, + null, + keyModifiers + ); + } else { + touchId = this.nextTouchId++; + let touch = this.createATouch(target, x, y, touchId); + this.emitTouchEvent(doc, "touchstart", touch); + this.emitTouchEvent(doc, "touchend", touch); + this.mouseTap( + touch.target.ownerDocument, + touch.clientX, + touch.clientY, + null, + null, + keyModifiers + ); + } + this.lastCoordinates = null; + break; + + case "press": + this.isTap = true; + if (this.mouseEventsOnly) { + this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers); + this.emitMouseEvent(doc, "mousedown", x, y, null, null, keyModifiers); + } else { + touchId = this.nextTouchId++; + let touch = this.createATouch(target, x, y, touchId); + this.emitTouchEvent(doc, "touchstart", touch); + this.touchIds[touchId] = touch; + return touchId; + } + break; + + case "release": + if (this.mouseEventsOnly) { + let [x, y] = this.lastCoordinates; + this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers); + } else { + let touch = this.touchIds[touchId]; + let [x, y] = this.lastCoordinates; + + touch = this.createATouch(touch.target, x, y, touchId); + this.emitTouchEvent(doc, "touchend", touch); + + if (this.isTap) { + this.mouseTap( + touch.target.ownerDocument, + touch.clientX, + touch.clientY, + null, + null, + keyModifiers + ); + } + delete this.touchIds[touchId]; + } + + this.isTap = false; + this.lastCoordinates = null; + break; + + case "cancel": + this.isTap = false; + if (this.mouseEventsOnly) { + let [x, y] = this.lastCoordinates; + this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers); + } else { + this.emitTouchEvent(doc, "touchcancel", this.touchIds[touchId]); + delete this.touchIds[touchId]; + } + this.lastCoordinates = null; + break; + + case "move": + this.isTap = false; + if (this.mouseEventsOnly) { + this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers); + } else { + let touch = this.createATouch( + this.touchIds[touchId].target, + x, + y, + touchId + ); + this.touchIds[touchId] = touch; + this.emitTouchEvent(doc, "touchmove", touch); + } + break; + + case "contextmenu": + this.isTap = false; + let event = this.container.frame.document.createEvent("MouseEvents"); + if (this.mouseEventsOnly) { + target = doc.elementFromPoint( + this.lastCoordinates[0], + this.lastCoordinates[1] + ); + } else { + target = this.touchIds[touchId].target; + } + + let [clientX, clientY, , , screenX, screenY] = this.getCoordinateInfo( + target, + x, + y + ); + + event.initMouseEvent( + "contextmenu", + true, + true, + target.ownerGlobal, + 1, + screenX, + screenY, + clientX, + clientY, + false, + false, + false, + false, + 0, + null + ); + target.dispatchEvent(event); + break; + + default: + throw new lazy.error.WebDriverError("Unknown event type: " + type); + } + return null; +}; + +action.Chain.prototype.mouseTap = function (doc, x, y, button, count, mod) { + this.emitMouseEvent(doc, "mousemove", x, y, button, count, mod); + this.emitMouseEvent(doc, "mousedown", x, y, button, count, mod); + this.emitMouseEvent(doc, "mouseup", x, y, button, count, mod); +}; |