1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* eslint-disable no-restricted-globals */
9 const EXPORTED_SYMBOLS = ["legacyaction"];
11 const { XPCOMUtils } = ChromeUtils.import(
12 "resource://gre/modules/XPCOMUtils.jsm"
15 XPCOMUtils.defineLazyModuleGetters(this, {
16 Preferences: "resource://gre/modules/Preferences.jsm",
18 accessibility: "chrome://remote/content/marionette/accessibility.js",
19 element: "chrome://remote/content/marionette/element.js",
20 error: "chrome://remote/content/shared/webdriver/Errors.jsm",
21 evaluate: "chrome://remote/content/marionette/evaluate.js",
22 event: "chrome://remote/content/marionette/event.js",
23 Log: "chrome://remote/content/shared/Log.jsm",
24 WebElement: "chrome://remote/content/marionette/element.js",
27 XPCOMUtils.defineLazyGetter(this, "logger", () =>
28 Log.get(Log.TYPES.MARIONETTE)
31 const CONTEXT_MENU_DELAY_PREF = "ui.click_hold_context_menus.delay";
32 const DEFAULT_CONTEXT_MENU_DELAY = 750; // ms
36 this.legacyaction = this.action = {};
39 * Functionality for (single finger) action chains.
41 action.Chain = function() {
42 // for assigning unique ids to all touches
43 this.nextTouchId = 1000;
44 // keep track of active Touches
46 // last touch for each fingerId
47 this.lastCoordinates = null;
49 this.scrolling = false;
50 // whether to send mouse event
51 this.mouseEventsOnly = false;
52 this.checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
54 // determines if we create touch events
55 this.inputSource = null;
59 * Create a touch based event.
61 * @param {Element} elem
62 * The Element on which the touch event should be created.
64 * x coordinate relative to the viewport.
66 * y coordinate relative to the viewport.
67 * @param {Number} touchId
68 * Touch event id used by legacyactions.
70 action.Chain.prototype.createATouch = function(elem, x, y, touchId) {
71 const doc = elem.ownerDocument;
72 const win = doc.defaultView;
80 ] = this.getCoordinateInfo(elem, x, y);
81 const atouch = doc.createTouch(
95 action.Chain.prototype.dispatchActions = function(
101 this.seenEls = seenEls;
102 this.container = container;
103 let commandArray = evaluate.fromJSON({
106 win: container.frame,
109 if (touchId == null) {
110 touchId = this.nextTSouchId++;
113 if (!container.frame.document.createTouch) {
114 this.mouseEventsOnly = true;
124 return new Promise(resolve => {
125 this.actions(commandArray, touchId, 0, keyModifiers, resolve);
126 }).catch(this.resetValues.bind(this));
130 * This function emit mouse event.
132 * @param {Document} doc
134 * @param {string} type
135 * Type of event to dispatch.
136 * @param {number} clickCount
137 * Number of clicks, button notes the mouse button.
138 * @param {number} elClientX
139 * X coordinate of the mouse relative to the viewport.
140 * @param {number} elClientY
141 * Y coordinate of the mouse relative to the viewport.
142 * @param {Object} modifiers
143 * An object of modifier keys present.
145 action.Chain.prototype.emitMouseEvent = function(
155 `Emitting ${type} mouse event ` +
156 `at coordinates (${elClientX}, ${elClientY}) ` +
157 `relative to the viewport, ` +
158 `button: ${button}, ` +
159 `clickCount: ${clickCount}`
162 let win = doc.defaultView;
163 let domUtils = win.windowUtils;
166 if (typeof modifiers != "undefined") {
167 mods = event.parseModifiers_(modifiers, win);
172 domUtils.sendMouseEvent(
185 action.Chain.prototype.emitTouchEvent = function(doc, type, touch) {
187 `Emitting Touch event of type ${type} ` +
188 `to element with id: ${touch.target.id} ` +
189 `and tag name: ${touch.target.tagName} ` +
190 `at coordinates (${touch.clientX}), ` +
191 `${touch.clientY}) relative to the viewport`
194 const win = doc.defaultView;
195 if (win.docShell.asyncPanZoomEnabled && this.scrolling) {
197 `Cannot emit touch event with asyncPanZoomEnabled and legacyactions.scrolling`
202 // we get here if we're not in asyncPacZoomEnabled land, or if we're
204 win.windowUtils.sendTouchEvent(
211 [touch.rotationAngle],
218 * Reset any persisted values after a command completes.
220 action.Chain.prototype.resetValues = function() {
221 this.container = null;
223 this.mouseEventsOnly = false;
227 * Function that performs a single tap.
229 action.Chain.prototype.singleTap = async function(
235 const doc = el.ownerDocument;
236 // after this block, the element will be scrolled into view
237 let visible = element.isVisible(el, corx, cory);
239 throw new error.ElementNotInteractableError(
240 "Element is not currently visible and may not be manipulated"
244 let a11y = accessibility.get(capabilities["moz:accessibilityChecks"]);
245 let acc = await a11y.getAccessible(el, true);
246 a11y.assertVisible(acc, el, visible);
247 a11y.assertActionable(acc, el);
248 if (!doc.createTouch) {
249 this.mouseEventsOnly = true;
251 let c = element.coordinates(el, corx, cory);
252 if (!this.mouseEventsOnly) {
253 let touchId = this.nextTouchId++;
254 let touch = this.createATouch(el, c.x, c.y, touchId);
255 this.emitTouchEvent(doc, "touchstart", touch);
256 this.emitTouchEvent(doc, "touchend", touch);
258 this.mouseTap(doc, c.x, c.y);
262 * Emit events for each action in the provided chain.
264 * To emit touch events for each finger, one might send a [["press", id],
265 * ["wait", 5], ["release"]] chain.
267 * @param {Array.<Array<?>>} chain
268 * A multi-dimensional array of actions.
269 * @param {Object.<string, number>} touchId
270 * Represents the finger ID.
272 * Keeps track of the current action of the chain.
273 * @param {Object.<string, boolean>} keyModifiers
274 * Keeps track of keyDown/keyUp pairs through an action chain.
275 * @param {function(?)} cb
278 * @return {Object.<string, number>}
279 * Last finger ID, or an empty object.
281 action.Chain.prototype.actions = function(chain, touchId, i, keyModifiers, cb) {
282 if (i == chain.length) {
289 let command = pack[0];
295 if (!["press", "wait", "keyDown", "keyUp", "click"].includes(command)) {
296 // if mouseEventsOnly, then touchIds isn't used
297 if (!(touchId in this.touchIds) && !this.mouseEventsOnly) {
299 throw new error.WebDriverError("Element has not been pressed");
305 event.sendKeyDown(pack[1], keyModifiers, this.container.frame);
306 this.actions(chain, touchId, i, keyModifiers, cb);
310 event.sendKeyUp(pack[1], keyModifiers, this.container.frame);
311 this.actions(chain, touchId, i, keyModifiers, cb);
315 webEl = WebElement.fromUUID(pack[1], "content");
316 el = this.seenEls.get(webEl);
317 let button = pack[2];
318 let clickCount = pack[3];
319 c = element.coordinates(el);
339 this.actions(chain, touchId, i, keyModifiers, cb);
343 if (this.lastCoordinates) {
346 this.lastCoordinates[0],
347 this.lastCoordinates[1],
353 throw new error.WebDriverError(
354 "Invalid Command: press cannot follow an active touch event"
358 // look ahead to check if we're scrolling,
359 // needed for APZ touch dispatching
360 if (i != chain.length && chain[i][0].includes("move")) {
361 this.scrolling = true;
363 webEl = WebElement.fromUUID(pack[1], "content");
364 el = this.seenEls.get(webEl);
365 c = element.coordinates(el, pack[2], pack[3]);
366 touchId = this.generateEvents("press", c.x, c.y, null, el, keyModifiers);
367 this.actions(chain, touchId, i, keyModifiers, cb);
373 this.lastCoordinates[0],
374 this.lastCoordinates[1],
379 this.actions(chain, null, i, keyModifiers, cb);
380 this.scrolling = false;
384 webEl = WebElement.fromUUID(pack[1], "content");
385 el = this.seenEls.get(webEl);
386 c = element.coordinates(el);
387 this.generateEvents("move", c.x, c.y, touchId, null, keyModifiers);
388 this.actions(chain, touchId, i, keyModifiers, cb);
394 this.lastCoordinates[0] + pack[1],
395 this.lastCoordinates[1] + pack[2],
400 this.actions(chain, touchId, i, keyModifiers, cb);
404 if (pack[1] != null) {
405 let time = pack[1] * 1000;
407 // standard waiting time to fire contextmenu
408 let standard = Preferences.get(
409 CONTEXT_MENU_DELAY_PREF,
410 DEFAULT_CONTEXT_MENU_DELAY
413 if (time >= standard && this.isTap) {
414 chain.splice(i, 0, ["longPress"], ["wait", (time - standard) / 1000]);
417 this.checkTimer.initWithCallback(
418 () => this.actions(chain, touchId, i, keyModifiers, cb),
420 Ci.nsITimer.TYPE_ONE_SHOT
423 this.actions(chain, touchId, i, keyModifiers, cb);
430 this.lastCoordinates[0],
431 this.lastCoordinates[1],
436 this.actions(chain, touchId, i, keyModifiers, cb);
437 this.scrolling = false;
443 this.lastCoordinates[0],
444 this.lastCoordinates[1],
449 this.actions(chain, touchId, i, keyModifiers, cb);
455 * Given an element and a pair of coordinates, returns an array of the
456 * form [clientX, clientY, pageX, pageY, screenX, screenY].
458 action.Chain.prototype.getCoordinateInfo = function(el, corx, cory) {
459 let win = el.ownerGlobal;
463 corx + win.pageXOffset, // pageX
464 cory + win.pageYOffset, // pageY
465 corx + win.mozInnerScreenX, // screenX
466 cory + win.mozInnerScreenY, // screenY
472 * X coordinate of the location to generate the event that is relative
475 * Y coordinate of the location to generate the event that is relative
478 action.Chain.prototype.generateEvents = function(
486 this.lastCoordinates = [x, y];
487 let doc = this.container.frame.document;
491 if (this.mouseEventsOnly) {
492 let touch = this.createATouch(target, x, y, touchId);
494 touch.target.ownerDocument,
502 touchId = this.nextTouchId++;
503 let touch = this.createATouch(target, x, y, touchId);
504 this.emitTouchEvent(doc, "touchstart", touch);
505 this.emitTouchEvent(doc, "touchend", touch);
507 touch.target.ownerDocument,
515 this.lastCoordinates = null;
520 if (this.mouseEventsOnly) {
521 this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers);
522 this.emitMouseEvent(doc, "mousedown", x, y, null, null, keyModifiers);
524 touchId = this.nextTouchId++;
525 let touch = this.createATouch(target, x, y, touchId);
526 this.emitTouchEvent(doc, "touchstart", touch);
527 this.touchIds[touchId] = touch;
533 if (this.mouseEventsOnly) {
534 let [x, y] = this.lastCoordinates;
535 this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers);
537 let touch = this.touchIds[touchId];
538 let [x, y] = this.lastCoordinates;
540 touch = this.createATouch(touch.target, x, y, touchId);
541 this.emitTouchEvent(doc, "touchend", touch);
545 touch.target.ownerDocument,
553 delete this.touchIds[touchId];
557 this.lastCoordinates = null;
562 if (this.mouseEventsOnly) {
563 let [x, y] = this.lastCoordinates;
564 this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers);
566 this.emitTouchEvent(doc, "touchcancel", this.touchIds[touchId]);
567 delete this.touchIds[touchId];
569 this.lastCoordinates = null;
574 if (this.mouseEventsOnly) {
575 this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers);
577 let touch = this.createATouch(
578 this.touchIds[touchId].target,
583 this.touchIds[touchId] = touch;
584 this.emitTouchEvent(doc, "touchmove", touch);
590 let event = this.container.frame.document.createEvent("MouseEvents");
591 if (this.mouseEventsOnly) {
592 target = doc.elementFromPoint(
593 this.lastCoordinates[0],
594 this.lastCoordinates[1]
597 target = this.touchIds[touchId].target;
600 let [clientX, clientY, , , screenX, screenY] = this.getCoordinateInfo(
606 event.initMouseEvent(
623 target.dispatchEvent(event);
627 throw new error.WebDriverError("Unknown event type: " + type);
632 action.Chain.prototype.mouseTap = function(doc, x, y, button, count, mod) {
633 this.emitMouseEvent(doc, "mousemove", x, y, button, count, mod);
634 this.emitMouseEvent(doc, "mousedown", x, y, button, count, mod);
635 this.emitMouseEvent(doc, "mouseup", x, y, button, count, mod);