Bug 1761003 [wpt PR 33324] - Add comment pointing to followup bug, and fix typos...
[gecko.git] / remote / marionette / legacyaction.js
blobd83c269b195787fddb2165106d1e157be79e9f35
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 */
7 "use strict";
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",
25 });
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
34 /* global action */
35 /** @namespace */
36 this.legacyaction = this.action = {};
38 /**
39  * Functionality for (single finger) action chains.
40  */
41 action.Chain = function() {
42   // for assigning unique ids to all touches
43   this.nextTouchId = 1000;
44   // keep track of active Touches
45   this.touchIds = {};
46   // last touch for each fingerId
47   this.lastCoordinates = null;
48   this.isTap = false;
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;
58 /**
59  * Create a touch based event.
60  *
61  * @param {Element} elem
62  *        The Element on which the touch event should be created.
63  * @param {Number} x
64  *        x coordinate relative to the viewport.
65  * @param {Number} y
66  *        y coordinate relative to the viewport.
67  * @param {Number} touchId
68  *        Touch event id used by legacyactions.
69  */
70 action.Chain.prototype.createATouch = function(elem, x, y, touchId) {
71   const doc = elem.ownerDocument;
72   const win = doc.defaultView;
73   const [
74     clientX,
75     clientY,
76     pageX,
77     pageY,
78     screenX,
79     screenY,
80   ] = this.getCoordinateInfo(elem, x, y);
81   const atouch = doc.createTouch(
82     win,
83     elem,
84     touchId,
85     pageX,
86     pageY,
87     screenX,
88     screenY,
89     clientX,
90     clientY
91   );
92   return atouch;
95 action.Chain.prototype.dispatchActions = function(
96   args,
97   touchId,
98   container,
99   seenEls
100 ) {
101   this.seenEls = seenEls;
102   this.container = container;
103   let commandArray = evaluate.fromJSON({
104     obj: args,
105     seenEls,
106     win: container.frame,
107   });
109   if (touchId == null) {
110     touchId = this.nextTSouchId++;
111   }
113   if (!container.frame.document.createTouch) {
114     this.mouseEventsOnly = true;
115   }
117   let keyModifiers = {
118     shiftKey: false,
119     ctrlKey: false,
120     altKey: false,
121     metaKey: false,
122   };
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
133  *     Current document.
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.
144  */
145 action.Chain.prototype.emitMouseEvent = function(
146   doc,
147   type,
148   elClientX,
149   elClientY,
150   button,
151   clickCount,
152   modifiers
153 ) {
154   logger.debug(
155     `Emitting ${type} mouse event ` +
156       `at coordinates (${elClientX}, ${elClientY}) ` +
157       `relative to the viewport, ` +
158       `button: ${button}, ` +
159       `clickCount: ${clickCount}`
160   );
162   let win = doc.defaultView;
163   let domUtils = win.windowUtils;
165   let mods;
166   if (typeof modifiers != "undefined") {
167     mods = event.parseModifiers_(modifiers, win);
168   } else {
169     mods = 0;
170   }
172   domUtils.sendMouseEvent(
173     type,
174     elClientX,
175     elClientY,
176     button || 0,
177     clickCount || 1,
178     mods,
179     false,
180     0,
181     this.inputSource
182   );
185 action.Chain.prototype.emitTouchEvent = function(doc, type, touch) {
186   logger.info(
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`
192   );
194   const win = doc.defaultView;
195   if (win.docShell.asyncPanZoomEnabled && this.scrolling) {
196     logger.debug(
197       `Cannot emit touch event with asyncPanZoomEnabled and legacyactions.scrolling`
198     );
199     return;
200   }
202   // we get here if we're not in asyncPacZoomEnabled land, or if we're
203   // the main process
204   win.windowUtils.sendTouchEvent(
205     type,
206     [touch.identifier],
207     [touch.clientX],
208     [touch.clientY],
209     [touch.radiusX],
210     [touch.radiusY],
211     [touch.rotationAngle],
212     [touch.force],
213     0
214   );
218  * Reset any persisted values after a command completes.
219  */
220 action.Chain.prototype.resetValues = function() {
221   this.container = null;
222   this.seenEls = null;
223   this.mouseEventsOnly = false;
227  * Function that performs a single tap.
228  */
229 action.Chain.prototype.singleTap = async function(
230   el,
231   corx,
232   cory,
233   capabilities
234 ) {
235   const doc = el.ownerDocument;
236   // after this block, the element will be scrolled into view
237   let visible = element.isVisible(el, corx, cory);
238   if (!visible) {
239     throw new error.ElementNotInteractableError(
240       "Element is not currently visible and may not be manipulated"
241     );
242   }
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;
250   }
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);
257   }
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.
271  * @param {number} i
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
276  *     Called on success.
278  * @return {Object.<string, number>}
279  *     Last finger ID, or an empty object.
280  */
281 action.Chain.prototype.actions = function(chain, touchId, i, keyModifiers, cb) {
282   if (i == chain.length) {
283     cb(touchId || null);
284     this.resetValues();
285     return;
286   }
288   let pack = chain[i];
289   let command = pack[0];
290   let webEl;
291   let el;
292   let c;
293   i++;
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) {
298       this.resetValues();
299       throw new error.WebDriverError("Element has not been pressed");
300     }
301   }
303   switch (command) {
304     case "keyDown":
305       event.sendKeyDown(pack[1], keyModifiers, this.container.frame);
306       this.actions(chain, touchId, i, keyModifiers, cb);
307       break;
309     case "keyUp":
310       event.sendKeyUp(pack[1], keyModifiers, this.container.frame);
311       this.actions(chain, touchId, i, keyModifiers, cb);
312       break;
314     case "click":
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);
320       this.mouseTap(
321         el.ownerDocument,
322         c.x,
323         c.y,
324         button,
325         clickCount,
326         keyModifiers
327       );
328       if (button == 2) {
329         this.emitMouseEvent(
330           el.ownerDocument,
331           "contextmenu",
332           c.x,
333           c.y,
334           button,
335           clickCount,
336           keyModifiers
337         );
338       }
339       this.actions(chain, touchId, i, keyModifiers, cb);
340       break;
342     case "press":
343       if (this.lastCoordinates) {
344         this.generateEvents(
345           "cancel",
346           this.lastCoordinates[0],
347           this.lastCoordinates[1],
348           touchId,
349           null,
350           keyModifiers
351         );
352         this.resetValues();
353         throw new error.WebDriverError(
354           "Invalid Command: press cannot follow an active touch event"
355         );
356       }
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;
362       }
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);
368       break;
370     case "release":
371       this.generateEvents(
372         "release",
373         this.lastCoordinates[0],
374         this.lastCoordinates[1],
375         touchId,
376         null,
377         keyModifiers
378       );
379       this.actions(chain, null, i, keyModifiers, cb);
380       this.scrolling = false;
381       break;
383     case "move":
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);
389       break;
391     case "moveByOffset":
392       this.generateEvents(
393         "move",
394         this.lastCoordinates[0] + pack[1],
395         this.lastCoordinates[1] + pack[2],
396         touchId,
397         null,
398         keyModifiers
399       );
400       this.actions(chain, touchId, i, keyModifiers, cb);
401       break;
403     case "wait":
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
411         );
413         if (time >= standard && this.isTap) {
414           chain.splice(i, 0, ["longPress"], ["wait", (time - standard) / 1000]);
415           time = standard;
416         }
417         this.checkTimer.initWithCallback(
418           () => this.actions(chain, touchId, i, keyModifiers, cb),
419           time,
420           Ci.nsITimer.TYPE_ONE_SHOT
421         );
422       } else {
423         this.actions(chain, touchId, i, keyModifiers, cb);
424       }
425       break;
427     case "cancel":
428       this.generateEvents(
429         "cancel",
430         this.lastCoordinates[0],
431         this.lastCoordinates[1],
432         touchId,
433         null,
434         keyModifiers
435       );
436       this.actions(chain, touchId, i, keyModifiers, cb);
437       this.scrolling = false;
438       break;
440     case "longPress":
441       this.generateEvents(
442         "contextmenu",
443         this.lastCoordinates[0],
444         this.lastCoordinates[1],
445         touchId,
446         null,
447         keyModifiers
448       );
449       this.actions(chain, touchId, i, keyModifiers, cb);
450       break;
451   }
455  * Given an element and a pair of coordinates, returns an array of the
456  * form [clientX, clientY, pageX, pageY, screenX, screenY].
457  */
458 action.Chain.prototype.getCoordinateInfo = function(el, corx, cory) {
459   let win = el.ownerGlobal;
460   return [
461     corx, // clientX
462     cory, // clientY
463     corx + win.pageXOffset, // pageX
464     cory + win.pageYOffset, // pageY
465     corx + win.mozInnerScreenX, // screenX
466     cory + win.mozInnerScreenY, // screenY
467   ];
471  * @param {number} x
472  *     X coordinate of the location to generate the event that is relative
473  *     to the viewport.
474  * @param {number} y
475  *     Y coordinate of the location to generate the event that is relative
476  *     to the viewport.
477  */
478 action.Chain.prototype.generateEvents = function(
479   type,
480   x,
481   y,
482   touchId,
483   target,
484   keyModifiers
485 ) {
486   this.lastCoordinates = [x, y];
487   let doc = this.container.frame.document;
489   switch (type) {
490     case "tap":
491       if (this.mouseEventsOnly) {
492         let touch = this.createATouch(target, x, y, touchId);
493         this.mouseTap(
494           touch.target.ownerDocument,
495           touch.clientX,
496           touch.clientY,
497           null,
498           null,
499           keyModifiers
500         );
501       } else {
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);
506         this.mouseTap(
507           touch.target.ownerDocument,
508           touch.clientX,
509           touch.clientY,
510           null,
511           null,
512           keyModifiers
513         );
514       }
515       this.lastCoordinates = null;
516       break;
518     case "press":
519       this.isTap = true;
520       if (this.mouseEventsOnly) {
521         this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers);
522         this.emitMouseEvent(doc, "mousedown", x, y, null, null, keyModifiers);
523       } else {
524         touchId = this.nextTouchId++;
525         let touch = this.createATouch(target, x, y, touchId);
526         this.emitTouchEvent(doc, "touchstart", touch);
527         this.touchIds[touchId] = touch;
528         return touchId;
529       }
530       break;
532     case "release":
533       if (this.mouseEventsOnly) {
534         let [x, y] = this.lastCoordinates;
535         this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers);
536       } else {
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);
543         if (this.isTap) {
544           this.mouseTap(
545             touch.target.ownerDocument,
546             touch.clientX,
547             touch.clientY,
548             null,
549             null,
550             keyModifiers
551           );
552         }
553         delete this.touchIds[touchId];
554       }
556       this.isTap = false;
557       this.lastCoordinates = null;
558       break;
560     case "cancel":
561       this.isTap = false;
562       if (this.mouseEventsOnly) {
563         let [x, y] = this.lastCoordinates;
564         this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers);
565       } else {
566         this.emitTouchEvent(doc, "touchcancel", this.touchIds[touchId]);
567         delete this.touchIds[touchId];
568       }
569       this.lastCoordinates = null;
570       break;
572     case "move":
573       this.isTap = false;
574       if (this.mouseEventsOnly) {
575         this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers);
576       } else {
577         let touch = this.createATouch(
578           this.touchIds[touchId].target,
579           x,
580           y,
581           touchId
582         );
583         this.touchIds[touchId] = touch;
584         this.emitTouchEvent(doc, "touchmove", touch);
585       }
586       break;
588     case "contextmenu":
589       this.isTap = false;
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]
595         );
596       } else {
597         target = this.touchIds[touchId].target;
598       }
600       let [clientX, clientY, , , screenX, screenY] = this.getCoordinateInfo(
601         target,
602         x,
603         y
604       );
606       event.initMouseEvent(
607         "contextmenu",
608         true,
609         true,
610         target.ownerGlobal,
611         1,
612         screenX,
613         screenY,
614         clientX,
615         clientY,
616         false,
617         false,
618         false,
619         false,
620         0,
621         null
622       );
623       target.dispatchEvent(event);
624       break;
626     default:
627       throw new error.WebDriverError("Unknown event type: " + type);
628   }
629   return null;
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);