Bug 1568157 - Part 5: Move the NodePicker initialization into a getter. r=yulia
[gecko.git] / testing / marionette / action.js
blobdf0ff9b1f07c40bcc82f3bba120942bfc6d97ca3
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 no-dupe-keys:off */
6 /* eslint-disable no-restricted-globals */
8 "use strict";
10 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
12 const { assert } = ChromeUtils.import("chrome://marionette/content/assert.js");
13 const { element } = ChromeUtils.import(
14   "chrome://marionette/content/element.js"
16 const {
17   InvalidArgumentError,
18   MoveTargetOutOfBoundsError,
19   UnsupportedOperationError,
20 } = ChromeUtils.import("chrome://marionette/content/error.js");
21 const { event } = ChromeUtils.import("chrome://marionette/content/event.js");
22 const { pprint } = ChromeUtils.import("chrome://marionette/content/format.js");
23 const { Sleep } = ChromeUtils.import("chrome://marionette/content/sync.js");
25 this.EXPORTED_SYMBOLS = ["action"];
27 // TODO? With ES 2016 and Symbol you can make a safer approximation
28 // to an enum e.g. https://gist.github.com/xmlking/e86e4f15ec32b12c4689
29 /**
30  * Implements WebDriver Actions API: a low-level interface for providing
31  * virtualised device input to the web browser.
32  *
33  * @namespace
34  */
35 this.action = {
36   Pause: "pause",
37   KeyDown: "keyDown",
38   KeyUp: "keyUp",
39   PointerDown: "pointerDown",
40   PointerUp: "pointerUp",
41   PointerMove: "pointerMove",
42   PointerCancel: "pointerCancel",
45 const ACTIONS = {
46   none: new Set([action.Pause]),
47   key: new Set([action.Pause, action.KeyDown, action.KeyUp]),
48   pointer: new Set([
49     action.Pause,
50     action.PointerDown,
51     action.PointerUp,
52     action.PointerMove,
53     action.PointerCancel,
54   ]),
57 /** Map from normalized key value to UI Events modifier key name */
58 const MODIFIER_NAME_LOOKUP = {
59   Alt: "alt",
60   Shift: "shift",
61   Control: "ctrl",
62   Meta: "meta",
65 /** Map from raw key (codepoint) to normalized key value */
66 const NORMALIZED_KEY_LOOKUP = {
67   "\uE000": "Unidentified",
68   "\uE001": "Cancel",
69   "\uE002": "Help",
70   "\uE003": "Backspace",
71   "\uE004": "Tab",
72   "\uE005": "Clear",
73   "\uE006": "Enter",
74   "\uE007": "Enter",
75   "\uE008": "Shift",
76   "\uE009": "Control",
77   "\uE00A": "Alt",
78   "\uE00B": "Pause",
79   "\uE00C": "Escape",
80   "\uE00D": " ",
81   "\uE00E": "PageUp",
82   "\uE00F": "PageDown",
83   "\uE010": "End",
84   "\uE011": "Home",
85   "\uE012": "ArrowLeft",
86   "\uE013": "ArrowUp",
87   "\uE014": "ArrowRight",
88   "\uE015": "ArrowDown",
89   "\uE016": "Insert",
90   "\uE017": "Delete",
91   "\uE018": ";",
92   "\uE019": "=",
93   "\uE01A": "0",
94   "\uE01B": "1",
95   "\uE01C": "2",
96   "\uE01D": "3",
97   "\uE01E": "4",
98   "\uE01F": "5",
99   "\uE020": "6",
100   "\uE021": "7",
101   "\uE022": "8",
102   "\uE023": "9",
103   "\uE024": "*",
104   "\uE025": "+",
105   "\uE026": ",",
106   "\uE027": "-",
107   "\uE028": ".",
108   "\uE029": "/",
109   "\uE031": "F1",
110   "\uE032": "F2",
111   "\uE033": "F3",
112   "\uE034": "F4",
113   "\uE035": "F5",
114   "\uE036": "F6",
115   "\uE037": "F7",
116   "\uE038": "F8",
117   "\uE039": "F9",
118   "\uE03A": "F10",
119   "\uE03B": "F11",
120   "\uE03C": "F12",
121   "\uE03D": "Meta",
122   "\uE040": "ZenkakuHankaku",
123   "\uE050": "Shift",
124   "\uE051": "Control",
125   "\uE052": "Alt",
126   "\uE053": "Meta",
127   "\uE054": "PageUp",
128   "\uE055": "PageDown",
129   "\uE056": "End",
130   "\uE057": "Home",
131   "\uE058": "ArrowLeft",
132   "\uE059": "ArrowUp",
133   "\uE05A": "ArrowRight",
134   "\uE05B": "ArrowDown",
135   "\uE05C": "Insert",
136   "\uE05D": "Delete",
139 /** Map from raw key (codepoint) to key location */
140 const KEY_LOCATION_LOOKUP = {
141   "\uE007": 1,
142   "\uE008": 1,
143   "\uE009": 1,
144   "\uE00A": 1,
145   "\uE01A": 3,
146   "\uE01B": 3,
147   "\uE01C": 3,
148   "\uE01D": 3,
149   "\uE01E": 3,
150   "\uE01F": 3,
151   "\uE020": 3,
152   "\uE021": 3,
153   "\uE022": 3,
154   "\uE023": 3,
155   "\uE024": 3,
156   "\uE025": 3,
157   "\uE026": 3,
158   "\uE027": 3,
159   "\uE028": 3,
160   "\uE029": 3,
161   "\uE03D": 1,
162   "\uE050": 2,
163   "\uE051": 2,
164   "\uE052": 2,
165   "\uE053": 2,
166   "\uE054": 3,
167   "\uE055": 3,
168   "\uE056": 3,
169   "\uE057": 3,
170   "\uE058": 3,
171   "\uE059": 3,
172   "\uE05A": 3,
173   "\uE05B": 3,
174   "\uE05C": 3,
175   "\uE05D": 3,
178 const KEY_CODE_LOOKUP = {
179   "\uE00A": "AltLeft",
180   "\uE052": "AltRight",
181   "\uE015": "ArrowDown",
182   "\uE012": "ArrowLeft",
183   "\uE014": "ArrowRight",
184   "\uE013": "ArrowUp",
185   "`": "Backquote",
186   "~": "Backquote",
187   "\\": "Backslash",
188   "|": "Backslash",
189   "\uE003": "Backspace",
190   "[": "BracketLeft",
191   "{": "BracketLeft",
192   "]": "BracketRight",
193   "}": "BracketRight",
194   ",": "Comma",
195   "<": "Comma",
196   "\uE009": "ControlLeft",
197   "\uE051": "ControlRight",
198   "\uE017": "Delete",
199   ")": "Digit0",
200   "0": "Digit0",
201   "!": "Digit1",
202   "1": "Digit1",
203   "2": "Digit2",
204   "@": "Digit2",
205   "#": "Digit3",
206   "3": "Digit3",
207   $: "Digit4",
208   "4": "Digit4",
209   "%": "Digit5",
210   "5": "Digit5",
211   "6": "Digit6",
212   "^": "Digit6",
213   "&": "Digit7",
214   "7": "Digit7",
215   "*": "Digit8",
216   "8": "Digit8",
217   "(": "Digit9",
218   "9": "Digit9",
219   "\uE010": "End",
220   "\uE006": "Enter",
221   "+": "Equal",
222   "=": "Equal",
223   "\uE00C": "Escape",
224   "\uE031": "F1",
225   "\uE03A": "F10",
226   "\uE03B": "F11",
227   "\uE03C": "F12",
228   "\uE032": "F2",
229   "\uE033": "F3",
230   "\uE034": "F4",
231   "\uE035": "F5",
232   "\uE036": "F6",
233   "\uE037": "F7",
234   "\uE038": "F8",
235   "\uE039": "F9",
236   "\uE002": "Help",
237   "\uE011": "Home",
238   "\uE016": "Insert",
239   "<": "IntlBackslash",
240   ">": "IntlBackslash",
241   A: "KeyA",
242   a: "KeyA",
243   B: "KeyB",
244   b: "KeyB",
245   C: "KeyC",
246   c: "KeyC",
247   D: "KeyD",
248   d: "KeyD",
249   E: "KeyE",
250   e: "KeyE",
251   F: "KeyF",
252   f: "KeyF",
253   G: "KeyG",
254   g: "KeyG",
255   H: "KeyH",
256   h: "KeyH",
257   I: "KeyI",
258   i: "KeyI",
259   J: "KeyJ",
260   j: "KeyJ",
261   K: "KeyK",
262   k: "KeyK",
263   L: "KeyL",
264   l: "KeyL",
265   M: "KeyM",
266   m: "KeyM",
267   N: "KeyN",
268   n: "KeyN",
269   O: "KeyO",
270   o: "KeyO",
271   P: "KeyP",
272   p: "KeyP",
273   Q: "KeyQ",
274   q: "KeyQ",
275   R: "KeyR",
276   r: "KeyR",
277   S: "KeyS",
278   s: "KeyS",
279   T: "KeyT",
280   t: "KeyT",
281   U: "KeyU",
282   u: "KeyU",
283   V: "KeyV",
284   v: "KeyV",
285   W: "KeyW",
286   w: "KeyW",
287   X: "KeyX",
288   x: "KeyX",
289   Y: "KeyY",
290   y: "KeyY",
291   Z: "KeyZ",
292   z: "KeyZ",
293   "-": "Minus",
294   _: "Minus",
295   "\uE01A": "Numpad0",
296   "\uE05C": "Numpad0",
297   "\uE01B": "Numpad1",
298   "\uE056": "Numpad1",
299   "\uE01C": "Numpad2",
300   "\uE05B": "Numpad2",
301   "\uE01D": "Numpad3",
302   "\uE055": "Numpad3",
303   "\uE01E": "Numpad4",
304   "\uE058": "Numpad4",
305   "\uE01F": "Numpad5",
306   "\uE020": "Numpad6",
307   "\uE05A": "Numpad6",
308   "\uE021": "Numpad7",
309   "\uE057": "Numpad7",
310   "\uE022": "Numpad8",
311   "\uE059": "Numpad8",
312   "\uE023": "Numpad9",
313   "\uE054": "Numpad9",
314   "\uE024": "NumpadAdd",
315   "\uE026": "NumpadComma",
316   "\uE028": "NumpadDecimal",
317   "\uE05D": "NumpadDecimal",
318   "\uE029": "NumpadDivide",
319   "\uE007": "NumpadEnter",
320   "\uE024": "NumpadMultiply",
321   "\uE026": "NumpadSubtract",
322   "\uE03D": "OSLeft",
323   "\uE053": "OSRight",
324   "\uE01E": "PageDown",
325   "\uE01F": "PageUp",
326   ".": "Period",
327   ">": "Period",
328   '"': "Quote",
329   "'": "Quote",
330   ":": "Semicolon",
331   ";": "Semicolon",
332   "\uE008": "ShiftLeft",
333   "\uE050": "ShiftRight",
334   "/": "Slash",
335   "?": "Slash",
336   "\uE00D": "Space",
337   "  ": "Space",
338   "\uE004": "Tab",
341 /** Represents possible values for a pointer-move origin. */
342 action.PointerOrigin = {
343   Viewport: "viewport",
344   Pointer: "pointer",
347 /** Flag for WebDriver spec conforming pointer origin calculation. */
348 action.specCompatPointerOrigin = true;
351  * Look up a PointerOrigin.
353  * @param {(string|Element)=} obj
354  *     Origin for a <code>pointerMove</code> action.  Must be one of
355  *     "viewport" (default), "pointer", or a DOM element.
357  * @return {action.PointerOrigin}
358  *     Pointer origin.
360  * @throws {InvalidArgumentError}
361  *     If <var>obj</var> is not a valid origin.
362  */
363 action.PointerOrigin.get = function(obj) {
364   let origin = obj;
365   if (typeof obj == "undefined") {
366     origin = this.Viewport;
367   } else if (typeof obj == "string") {
368     let name = capitalize(obj);
369     assert.in(name, this, pprint`Unknown pointer-move origin: ${obj}`);
370     origin = this[name];
371   } else if (!element.isDOMElement(obj)) {
372     throw new InvalidArgumentError(
373       "Expected 'origin' to be undefined, " +
374         '"viewport", "pointer", ' +
375         pprint`or an element, got: ${obj}`
376     );
377   }
378   return origin;
381 /** Represents possible subtypes for a pointer input source. */
382 action.PointerType = {
383   Mouse: "mouse",
384   // TODO For now, only mouse is supported
385   // Pen: "pen",
386   // Touch: "touch",
390  * Look up a PointerType.
392  * @param {string} str
393  *     Name of pointer type.
395  * @return {string}
396  *     A pointer type for processing pointer parameters.
398  * @throws {InvalidArgumentError}
399  *     If <code>str</code> is not a valid pointer type.
400  */
401 action.PointerType.get = function(str) {
402   let name = capitalize(str);
403   assert.in(name, this, pprint`Unknown pointerType: ${str}`);
404   return this[name];
408  * Input state associated with current session.  This is a map between
409  * input ID and the device state for that input source, with one entry
410  * for each active input source.
412  * Initialized in listener.js.
413  */
414 action.inputStateMap = undefined;
417  * List of {@link action.Action} associated with current session.  Used to
418  * manage dispatching events when resetting the state of the input sources.
419  * Reset operations are assumed to be idempotent.
421  * Initialized in listener.js
422  */
423 action.inputsToCancel = undefined;
426  * Represents device state for an input source.
427  */
428 class InputState {
429   constructor() {
430     this.type = this.constructor.name.toLowerCase();
431   }
433   /**
434    * Check equality of this InputState object with another.
435    *
436    * @param {InputState} other
437    *     Object representing an input state.
438    *
439    * @return {boolean}
440    *     True if <code>this</code> has the same <code>type</code>
441    *     as <code>other</code>.
442    */
443   is(other) {
444     if (typeof other == "undefined") {
445       return false;
446     }
447     return this.type === other.type;
448   }
450   toString() {
451     return `[object ${this.constructor.name}InputState]`;
452   }
454   /**
455    * @param {Object.<string, ?>} obj
456    *     Object with property <code>type</code> and optionally
457    *     <code>parameters</code> or <code>pointerType</code>,
458    *     representing an action sequence or an action item.
459    *
460    * @return {action.InputState}
461    *     An {@link InputState} object for the type of the
462    *     {@link actionSequence}.
463    *
464    * @throws {InvalidArgumentError}
465    *     If {@link actionSequence.type} is not valid.
466    */
467   static fromJSON(obj) {
468     let type = obj.type;
469     assert.in(type, ACTIONS, pprint`Unknown action type: ${type}`);
470     let name = type == "none" ? "Null" : capitalize(type);
471     if (name == "Pointer") {
472       if (
473         !obj.pointerType &&
474         (!obj.parameters || !obj.parameters.pointerType)
475       ) {
476         throw new InvalidArgumentError(
477           pprint`Expected obj to have pointerType, got ${obj}`
478         );
479       }
480       let pointerType = obj.pointerType || obj.parameters.pointerType;
481       return new action.InputState[name](pointerType);
482     }
483     return new action.InputState[name]();
484   }
487 /** Possible kinds of |InputState| for supported input sources. */
488 action.InputState = {};
491  * Input state associated with a keyboard-type device.
492  */
493 action.InputState.Key = class Key extends InputState {
494   constructor() {
495     super();
496     this.pressed = new Set();
497     this.alt = false;
498     this.shift = false;
499     this.ctrl = false;
500     this.meta = false;
501   }
503   /**
504    * Update modifier state according to |key|.
505    *
506    * @param {string} key
507    *     Normalized key value of a modifier key.
508    * @param {boolean} value
509    *     Value to set the modifier attribute to.
510    *
511    * @throws {InvalidArgumentError}
512    *     If |key| is not a modifier.
513    */
514   setModState(key, value) {
515     if (key in MODIFIER_NAME_LOOKUP) {
516       this[MODIFIER_NAME_LOOKUP[key]] = value;
517     } else {
518       throw new InvalidArgumentError(
519         "Expected 'key' to be one of " +
520           Object.keys(MODIFIER_NAME_LOOKUP) +
521           pprint`, got ${key}`
522       );
523     }
524   }
526   /**
527    * Check whether |key| is pressed.
528    *
529    * @param {string} key
530    *     Normalized key value.
531    *
532    * @return {boolean}
533    *     True if |key| is in set of pressed keys.
534    */
535   isPressed(key) {
536     return this.pressed.has(key);
537   }
539   /**
540    * Add |key| to the set of pressed keys.
541    *
542    * @param {string} key
543    *     Normalized key value.
544    *
545    * @return {boolean}
546    *     True if |key| is in list of pressed keys.
547    */
548   press(key) {
549     return this.pressed.add(key);
550   }
552   /**
553    * Remove |key| from the set of pressed keys.
554    *
555    * @param {string} key
556    *     Normalized key value.
557    *
558    * @return {boolean}
559    *     True if |key| was present before removal, false otherwise.
560    */
561   release(key) {
562     return this.pressed.delete(key);
563   }
567  * Input state not associated with a specific physical device.
568  */
569 action.InputState.Null = class Null extends InputState {
570   constructor() {
571     super();
572     this.type = "none";
573   }
577  * Input state associated with a pointer-type input device.
579  * @param {string} subtype
580  *     Kind of pointing device: mouse, pen, touch.
582  * @throws {InvalidArgumentError}
583  *     If subtype is undefined or an invalid pointer type.
584  */
585 action.InputState.Pointer = class Pointer extends InputState {
586   constructor(subtype) {
587     super();
588     this.pressed = new Set();
589     assert.defined(
590       subtype,
591       pprint`Expected subtype to be defined, got ${subtype}`
592     );
593     this.subtype = action.PointerType.get(subtype);
594     this.x = 0;
595     this.y = 0;
596   }
598   /**
599    * Check whether |button| is pressed.
600    *
601    * @param {number} button
602    *     Positive integer that refers to a mouse button.
603    *
604    * @return {boolean}
605    *     True if |button| is in set of pressed buttons.
606    */
607   isPressed(button) {
608     assert.positiveInteger(button);
609     return this.pressed.has(button);
610   }
612   /**
613    * Add |button| to the set of pressed keys.
614    *
615    * @param {number} button
616    *     Positive integer that refers to a mouse button.
617    *
618    * @return {Set}
619    *     Set of pressed buttons.
620    */
621   press(button) {
622     assert.positiveInteger(button);
623     return this.pressed.add(button);
624   }
626   /**
627    * Remove |button| from the set of pressed buttons.
628    *
629    * @param {number} button
630    *     A positive integer that refers to a mouse button.
631    *
632    * @return {boolean}
633    *     True if |button| was present before removals, false otherwise.
634    */
635   release(button) {
636     assert.positiveInteger(button);
637     return this.pressed.delete(button);
638   }
642  * Repesents an action for dispatch. Used in |action.Chain| and
643  * |action.Sequence|.
645  * @param {string} id
646  *     Input source ID.
647  * @param {string} type
648  *     Action type: none, key, pointer.
649  * @param {string} subtype
650  *     Action subtype: {@link action.Pause}, {@link action.KeyUp},
651  *     {@link action.KeyDown}, {@link action.PointerUp},
652  *     {@link action.PointerDown}, {@link action.PointerMove}, or
653  *     {@link action.PointerCancel}.
655  * @throws {InvalidArgumentError}
656  *      If any parameters are undefined.
657  */
658 action.Action = class {
659   constructor(id, type, subtype) {
660     if ([id, type, subtype].includes(undefined)) {
661       throw new InvalidArgumentError("Missing id, type or subtype");
662     }
663     for (let attr of [id, type, subtype]) {
664       assert.string(attr, pprint`Expected string, got ${attr}`);
665     }
666     this.id = id;
667     this.type = type;
668     this.subtype = subtype;
669   }
671   toString() {
672     return `[action ${this.type}]`;
673   }
675   /**
676    * @param {action.Sequence} actionSequence
677    *     Object representing sequence of actions from one input source.
678    * @param {action.Action} actionItem
679    *     Object representing a single action from |actionSequence|.
680    *
681    * @return {action.Action}
682    *     An action that can be dispatched; corresponds to |actionItem|.
683    *
684    * @throws {InvalidArgumentError}
685    *     If any <code>actionSequence</code> or <code>actionItem</code>
686    *     attributes are invalid.
687    * @throws {UnsupportedOperationError}
688    *     If <code>actionItem.type</code> is {@link action.PointerCancel}.
689    */
690   static fromJSON(actionSequence, actionItem) {
691     let type = actionSequence.type;
692     let id = actionSequence.id;
693     let subtypes = ACTIONS[type];
694     if (!subtypes) {
695       throw new InvalidArgumentError("Unknown type: " + type);
696     }
697     let subtype = actionItem.type;
698     if (!subtypes.has(subtype)) {
699       throw new InvalidArgumentError(
700         `Unknown subtype for ${type} action: ${subtype}`
701       );
702     }
704     let item = new action.Action(id, type, subtype);
705     if (type === "pointer") {
706       action.processPointerAction(
707         id,
708         action.PointerParameters.fromJSON(actionSequence.parameters),
709         item
710       );
711     }
713     switch (item.subtype) {
714       case action.KeyUp:
715       case action.KeyDown:
716         let key = actionItem.value;
717         // TODO countGraphemes
718         // TODO key.value could be a single code point like "\uE012"
719         // (see rawKey) or "grapheme cluster"
720         assert.string(
721           key,
722           "Expected 'value' to be a string that represents single code point " +
723             pprint`or grapheme cluster, got ${key}`
724         );
725         item.value = key;
726         break;
728       case action.PointerDown:
729       case action.PointerUp:
730         assert.positiveInteger(
731           actionItem.button,
732           pprint`Expected 'button' (${actionItem.button}) to be >= 0`
733         );
734         item.button = actionItem.button;
735         break;
737       case action.PointerMove:
738         item.duration = actionItem.duration;
739         if (typeof item.duration != "undefined") {
740           assert.positiveInteger(
741             item.duration,
742             pprint`Expected 'duration' (${item.duration}) to be >= 0`
743           );
744         }
745         item.origin = action.PointerOrigin.get(actionItem.origin);
746         item.x = actionItem.x;
747         if (typeof item.x != "undefined") {
748           assert.integer(
749             item.x,
750             pprint`Expected 'x' (${item.x}) to be an Integer`
751           );
752         }
753         item.y = actionItem.y;
754         if (typeof item.y != "undefined") {
755           assert.integer(
756             item.y,
757             pprint`Expected 'y' (${item.y}) to be an Integer`
758           );
759         }
760         break;
762       case action.PointerCancel:
763         throw new UnsupportedOperationError();
765       case action.Pause:
766         item.duration = actionItem.duration;
767         if (typeof item.duration != "undefined") {
768           // eslint-disable-next-line
769           assert.positiveInteger(item.duration,
770             pprint`Expected 'duration' (${item.duration}) to be >= 0`
771           );
772         }
773         break;
774     }
776     return item;
777   }
781  * Represents a series of ticks, specifying which actions to perform at
782  * each tick.
783  */
784 action.Chain = class extends Array {
785   toString() {
786     return `[chain ${super.toString()}]`;
787   }
789   /**
790    * @param {Array.<?>} actions
791    *     Array of objects that each represent an action sequence.
792    *
793    * @return {action.Chain}
794    *     Transpose of <var>actions</var> such that actions to be performed
795    *     in a single tick are grouped together.
796    *
797    * @throws {InvalidArgumentError}
798    *     If <var>actions</var> is not an Array.
799    */
800   static fromJSON(actions) {
801     assert.array(
802       actions,
803       pprint`Expected 'actions' to be an array, got ${actions}`
804     );
806     let actionsByTick = new action.Chain();
807     for (let actionSequence of actions) {
808       // TODO(maja_zf): Check that each actionSequence in actions refers
809       // to a different input ID.
810       let inputSourceActions = action.Sequence.fromJSON(actionSequence);
811       for (let i = 0; i < inputSourceActions.length; i++) {
812         // new tick
813         if (actionsByTick.length < i + 1) {
814           actionsByTick.push([]);
815         }
816         actionsByTick[i].push(inputSourceActions[i]);
817       }
818     }
819     return actionsByTick;
820   }
824  * Represents one input source action sequence; this is essentially an
825  * |Array.<action.Action>|.
826  */
827 action.Sequence = class extends Array {
828   toString() {
829     return `[sequence ${super.toString()}]`;
830   }
832   /**
833    * @param {Object.<string, ?>} actionSequence
834    *     Object that represents a sequence action items for one input source.
835    *
836    * @return {action.Sequence}
837    *     Sequence of actions that can be dispatched.
838    *
839    * @throws {InvalidArgumentError}
840    *     If <code>actionSequence.id</code> is not a
841    *     string or it's aleady mapped to an |action.InputState}
842    *     incompatible with <code>actionSequence.type</code>, or if
843    *     <code>actionSequence.actions</code> is not an <code>Array</code>.
844    */
845   static fromJSON(actionSequence) {
846     // used here to validate 'type' in addition to InputState type below
847     let inputSourceState = InputState.fromJSON(actionSequence);
848     let id = actionSequence.id;
849     assert.defined(id, "Expected 'id' to be defined");
850     assert.string(id, pprint`Expected 'id' to be a string, got ${id}`);
851     let actionItems = actionSequence.actions;
852     assert.array(
853       actionItems,
854       "Expected 'actionSequence.actions' to be an array, " +
855         pprint`got ${actionSequence.actions}`
856     );
858     if (!action.inputStateMap.has(id)) {
859       action.inputStateMap.set(id, inputSourceState);
860     } else if (!action.inputStateMap.get(id).is(inputSourceState)) {
861       throw new InvalidArgumentError(
862         `Expected ${id} to be mapped to ${inputSourceState}, ` +
863           `got ${action.inputStateMap.get(id)}`
864       );
865     }
867     let actions = new action.Sequence();
868     for (let actionItem of actionItems) {
869       actions.push(action.Action.fromJSON(actionSequence, actionItem));
870     }
872     return actions;
873   }
877  * Represents parameters in an action for a pointer input source.
879  * @param {string=} pointerType
880  *     Type of pointing device.  If the parameter is undefined, "mouse"
881  *     is used.
882  */
883 action.PointerParameters = class {
884   constructor(pointerType = "mouse") {
885     this.pointerType = action.PointerType.get(pointerType);
886   }
888   toString() {
889     return `[pointerParameters ${this.pointerType}]`;
890   }
892   /**
893    * @param {Object.<string, ?>} parametersData
894    *     Object that represents pointer parameters.
895    *
896    * @return {action.PointerParameters}
897    *     Validated pointer paramters.
898    */
899   static fromJSON(parametersData) {
900     if (typeof parametersData == "undefined") {
901       return new action.PointerParameters();
902     }
903     return new action.PointerParameters(parametersData.pointerType);
904   }
908  * Adds <var>pointerType</var> attribute to Action <var>act</var>.
910  * Helper function for {@link action.Action.fromJSON}.
912  * @param {string} id
913  *     Input source ID.
914  * @param {action.PointerParams} pointerParams
915  *     Input source pointer parameters.
916  * @param {action.Action} act
917  *     Action to be updated.
919  * @throws {InvalidArgumentError}
920  *     If <var>id</var> is already mapped to an
921  *     {@link action.InputState} that is not compatible with
922  *     <code>act.type</code> or <code>pointerParams.pointerType</code>.
923  */
924 action.processPointerAction = function(id, pointerParams, act) {
925   if (
926     action.inputStateMap.has(id) &&
927     action.inputStateMap.get(id).type !== act.type
928   ) {
929     throw new InvalidArgumentError(
930       `Expected 'id' ${id} to be mapped to InputState whose type is ` +
931         action.inputStateMap.get(id).type +
932         pprint` , got ${act.type}`
933     );
934   }
935   let pointerType = pointerParams.pointerType;
936   if (
937     action.inputStateMap.has(id) &&
938     action.inputStateMap.get(id).subtype !== pointerType
939   ) {
940     throw new InvalidArgumentError(
941       `Expected 'id' ${id} to be mapped to InputState whose subtype is ` +
942         action.inputStateMap.get(id).subtype +
943         pprint` , got ${pointerType}`
944     );
945   }
946   act.pointerType = pointerParams.pointerType;
949 /** Collect properties associated with KeyboardEvent */
950 action.Key = class {
951   constructor(rawKey) {
952     this.key = NORMALIZED_KEY_LOOKUP[rawKey] || rawKey;
953     this.code = KEY_CODE_LOOKUP[rawKey];
954     this.location = KEY_LOCATION_LOOKUP[rawKey] || 0;
955     this.altKey = false;
956     this.shiftKey = false;
957     this.ctrlKey = false;
958     this.metaKey = false;
959     this.repeat = false;
960     this.isComposing = false;
961     // keyCode will be computed by event.sendKeyDown
962   }
964   update(inputState) {
965     this.altKey = inputState.alt;
966     this.shiftKey = inputState.shift;
967     this.ctrlKey = inputState.ctrl;
968     this.metaKey = inputState.meta;
969   }
972 /** Collect properties associated with MouseEvent */
973 action.Mouse = class {
974   constructor(type, button = 0) {
975     this.type = type;
976     assert.positiveInteger(button);
977     this.button = button;
978     this.buttons = 0;
979     this.altKey = false;
980     this.shiftKey = false;
981     this.metaKey = false;
982     this.ctrlKey = false;
983     // set modifier properties based on whether any corresponding keys are
984     // pressed on any key input source
985     for (let inputState of action.inputStateMap.values()) {
986       if (inputState.type == "key") {
987         this.altKey = inputState.alt || this.altKey;
988         this.ctrlKey = inputState.ctrl || this.ctrlKey;
989         this.metaKey = inputState.meta || this.metaKey;
990         this.shiftKey = inputState.shift || this.shiftKey;
991       }
992     }
993   }
995   update(inputState) {
996     let allButtons = Array.from(inputState.pressed);
997     this.buttons = allButtons.reduce((a, i) => a + Math.pow(2, i), 0);
998   }
1002  * Dispatch a chain of actions over |chain.length| ticks.
1004  * This is done by creating a Promise for each tick that resolves once
1005  * all the Promises for individual tick-actions are resolved.  The next
1006  * tick's actions are not dispatched until the Promise for the current
1007  * tick is resolved.
1009  * @param {action.Chain} chain
1010  *     Actions grouped by tick; each element in |chain| is a sequence of
1011  *     actions for one tick.
1012  * @param {WindowProxy} win
1013  *     Current window global.
1014  * @param {boolean=} [specCompatPointerOrigin=true] specCompatPointerOrigin
1015  *     Flag to turn off the WebDriver spec conforming pointer origin
1016  *     calculation. It has to be kept until all Selenium bindings can
1017  *     successfully handle the WebDriver spec conforming Pointer Origin
1018  *     calculation. See https://bugzilla.mozilla.org/show_bug.cgi?id=1429338.
1020  * @return {Promise}
1021  *     Promise for dispatching all actions in |chain|.
1022  */
1023 action.dispatch = function(chain, win, specCompatPointerOrigin = true) {
1024   action.specCompatPointerOrigin = specCompatPointerOrigin;
1026   let chainEvents = (async () => {
1027     for (let tickActions of chain) {
1028       await action.dispatchTickActions(
1029         tickActions,
1030         action.computeTickDuration(tickActions),
1031         win
1032       );
1033     }
1034   })();
1035   return chainEvents;
1039  * Dispatch sequence of actions for one tick.
1041  * This creates a Promise for one tick that resolves once the Promise
1042  * for each tick-action is resolved, which takes at least |tickDuration|
1043  * milliseconds.  The resolved set of events for each tick is followed by
1044  * firing of pending DOM events.
1046  * Note that the tick-actions are dispatched in order, but they may have
1047  * different durations and therefore may not end in the same order.
1049  * @param {Array.<action.Action>} tickActions
1050  *     List of actions for one tick.
1051  * @param {number} tickDuration
1052  *     Duration in milliseconds of this tick.
1053  * @param {WindowProxy} win
1054  *     Current window global.
1056  * @return {Promise}
1057  *     Promise for dispatching all tick-actions and pending DOM events.
1058  */
1059 action.dispatchTickActions = function(tickActions, tickDuration, win) {
1060   let pendingEvents = tickActions.map(toEvents(tickDuration, win));
1061   return Promise.all(pendingEvents);
1065  * Compute tick duration in milliseconds for a collection of actions.
1067  * @param {Array.<action.Action>} tickActions
1068  *     List of actions for one tick.
1070  * @return {number}
1071  *     Longest action duration in |tickActions| if any, or 0.
1072  */
1073 action.computeTickDuration = function(tickActions) {
1074   let max = 0;
1075   for (let a of tickActions) {
1076     let affectsWallClockTime =
1077       a.subtype == action.Pause ||
1078       (a.type == "pointer" && a.subtype == action.PointerMove);
1079     if (affectsWallClockTime && a.duration) {
1080       max = Math.max(a.duration, max);
1081     }
1082   }
1083   return max;
1087  * Compute viewport coordinates of pointer target based on given origin.
1089  * @param {action.Action} a
1090  *     Action that specifies pointer origin and x and y coordinates of target.
1091  * @param {action.InputState} inputState
1092  *     Input state that specifies current x and y coordinates of pointer.
1093  * @param {Map.<string, number>=} center
1094  *     Object representing x and y coordinates of an element center-point.
1095  *     This is only used if |a.origin| is a web element reference.
1097  * @return {Map.<string, number>}
1098  *     x and y coordinates of pointer destination.
1099  */
1100 action.computePointerDestination = function(a, inputState, center = undefined) {
1101   let { x, y } = a;
1102   switch (a.origin) {
1103     case action.PointerOrigin.Viewport:
1104       break;
1105     case action.PointerOrigin.Pointer:
1106       x += inputState.x;
1107       y += inputState.y;
1108       break;
1109     default:
1110       // origin represents web element
1111       assert.defined(center);
1112       assert.in("x", center);
1113       assert.in("y", center);
1114       x += center.x;
1115       y += center.y;
1116   }
1117   return { x, y };
1121  * Create a closure to use as a map from action definitions to Promise events.
1123  * @param {number} tickDuration
1124  *     Duration in milliseconds of this tick.
1125  * @param {WindowProxy} win
1126  *     Current window global.
1128  * @return {function(action.Action): Promise}
1129  *     Function that takes an action and returns a Promise for dispatching
1130  *     the event that corresponds to that action.
1131  */
1132 function toEvents(tickDuration, win) {
1133   return a => {
1134     let inputState = action.inputStateMap.get(a.id);
1136     switch (a.subtype) {
1137       case action.KeyUp:
1138         return dispatchKeyUp(a, inputState, win);
1140       case action.KeyDown:
1141         return dispatchKeyDown(a, inputState, win);
1143       case action.PointerDown:
1144         return dispatchPointerDown(a, inputState, win);
1146       case action.PointerUp:
1147         return dispatchPointerUp(a, inputState, win);
1149       case action.PointerMove:
1150         return dispatchPointerMove(a, inputState, tickDuration, win);
1152       case action.PointerCancel:
1153         throw new UnsupportedOperationError();
1155       case action.Pause:
1156         return dispatchPause(a, tickDuration);
1157     }
1159     return undefined;
1160   };
1164  * Dispatch a keyDown action equivalent to pressing a key on a keyboard.
1166  * @param {action.Action} a
1167  *     Action to dispatch.
1168  * @param {action.InputState} inputState
1169  *     Input state for this action's input source.
1170  * @param {WindowProxy} win
1171  *     Current window global.
1173  * @return {Promise}
1174  *     Promise to dispatch at least a keydown event, and keypress if
1175  *     appropriate.
1176  */
1177 function dispatchKeyDown(a, inputState, win) {
1178   return new Promise(resolve => {
1179     let keyEvent = new action.Key(a.value);
1180     keyEvent.repeat = inputState.isPressed(keyEvent.key);
1181     inputState.press(keyEvent.key);
1182     if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
1183       inputState.setModState(keyEvent.key, true);
1184     }
1186     // Append a copy of |a| with keyUp subtype
1187     action.inputsToCancel.push(Object.assign({}, a, { subtype: action.KeyUp }));
1188     keyEvent.update(inputState);
1189     event.sendKeyDown(a.value, keyEvent, win);
1191     resolve();
1192   });
1196  * Dispatch a keyUp action equivalent to releasing a key on a keyboard.
1198  * @param {action.Action} a
1199  *     Action to dispatch.
1200  * @param {action.InputState} inputState
1201  *     Input state for this action's input source.
1202  * @param {WindowProxy} win
1203  *     Current window global.
1205  * @return {Promise}
1206  *     Promise to dispatch a keyup event.
1207  */
1208 function dispatchKeyUp(a, inputState, win) {
1209   return new Promise(resolve => {
1210     let keyEvent = new action.Key(a.value);
1212     if (!inputState.isPressed(keyEvent.key)) {
1213       resolve();
1214       return;
1215     }
1217     if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
1218       inputState.setModState(keyEvent.key, false);
1219     }
1220     inputState.release(keyEvent.key);
1221     keyEvent.update(inputState);
1223     event.sendKeyUp(a.value, keyEvent, win);
1224     resolve();
1225   });
1229  * Dispatch a pointerDown action equivalent to pressing a pointer-device
1230  * button.
1232  * @param {action.Action} a
1233  *     Action to dispatch.
1234  * @param {action.InputState} inputState
1235  *     Input state for this action's input source.
1236  * @param {WindowProxy} win
1237  *     Current window global.
1239  * @return {Promise}
1240  *     Promise to dispatch at least a pointerdown event.
1241  */
1242 function dispatchPointerDown(a, inputState, win) {
1243   return new Promise(resolve => {
1244     if (inputState.isPressed(a.button)) {
1245       resolve();
1246       return;
1247     }
1249     inputState.press(a.button);
1250     // Append a copy of |a| with pointerUp subtype
1251     let copy = Object.assign({}, a, { subtype: action.PointerUp });
1252     action.inputsToCancel.push(copy);
1254     switch (inputState.subtype) {
1255       case action.PointerType.Mouse:
1256         let mouseEvent = new action.Mouse("mousedown", a.button);
1257         mouseEvent.update(inputState);
1258         if (mouseEvent.ctrlKey) {
1259           if (Services.appinfo.OS === "Darwin") {
1260             mouseEvent.button = 2;
1261             event.DoubleClickTracker.resetClick();
1262           }
1263         } else if (event.DoubleClickTracker.isClicked()) {
1264           mouseEvent = Object.assign({}, mouseEvent, { clickCount: 2 });
1265         }
1266         event.synthesizeMouseAtPoint(
1267           inputState.x,
1268           inputState.y,
1269           mouseEvent,
1270           win
1271         );
1272         if (
1273           event.MouseButton.isSecondary(a.button) ||
1274           (mouseEvent.ctrlKey && Services.appinfo.OS === "Darwin")
1275         ) {
1276           let contextMenuEvent = Object.assign({}, mouseEvent, {
1277             type: "contextmenu",
1278           });
1279           event.synthesizeMouseAtPoint(
1280             inputState.x,
1281             inputState.y,
1282             contextMenuEvent,
1283             win
1284           );
1285         }
1286         break;
1288       case action.PointerType.Pen:
1289       case action.PointerType.Touch:
1290         throw new UnsupportedOperationError(
1291           "Only 'mouse' pointer type is supported"
1292         );
1294       default:
1295         throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
1296     }
1298     resolve();
1299   });
1303  * Dispatch a pointerUp action equivalent to releasing a pointer-device
1304  * button.
1306  * @param {action.Action} a
1307  *     Action to dispatch.
1308  * @param {action.InputState} inputState
1309  *     Input state for this action's input source.
1310  * @param {WindowProxy} win
1311  *     Current window global.
1313  * @return {Promise}
1314  *     Promise to dispatch at least a pointerup event.
1315  */
1316 function dispatchPointerUp(a, inputState, win) {
1317   return new Promise(resolve => {
1318     if (!inputState.isPressed(a.button)) {
1319       resolve();
1320       return;
1321     }
1323     inputState.release(a.button);
1325     switch (inputState.subtype) {
1326       case action.PointerType.Mouse:
1327         let mouseEvent = new action.Mouse("mouseup", a.button);
1328         mouseEvent.update(inputState);
1329         if (event.DoubleClickTracker.isClicked()) {
1330           mouseEvent = Object.assign({}, mouseEvent, { clickCount: 2 });
1331         }
1332         event.synthesizeMouseAtPoint(
1333           inputState.x,
1334           inputState.y,
1335           mouseEvent,
1336           win
1337         );
1338         break;
1340       case action.PointerType.Pen:
1341       case action.PointerType.Touch:
1342         throw new UnsupportedOperationError(
1343           "Only 'mouse' pointer type is supported"
1344         );
1346       default:
1347         throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
1348     }
1350     resolve();
1351   });
1355  * Dispatch a pointerMove action equivalent to moving pointer device
1356  * in a line.
1358  * If the action duration is 0, the pointer jumps immediately to the
1359  * target coordinates.  Otherwise, events are synthesized to mimic a
1360  * pointer travelling in a discontinuous, approximately straight line,
1361  * with the pointer coordinates being updated around 60 times per second.
1363  * @param {action.Action} a
1364  *     Action to dispatch.
1365  * @param {action.InputState} inputState
1366  *     Input state for this action's input source.
1367  * @param {WindowProxy} win
1368  *     Current window global.
1370  * @return {Promise}
1371  *     Promise to dispatch at least one pointermove event, as well as
1372  *     mousemove events as appropriate.
1373  */
1374 function dispatchPointerMove(a, inputState, tickDuration, win) {
1375   const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
1376   // interval between pointermove increments in ms, based on common vsync
1377   const fps60 = 17;
1379   return new Promise((resolve, reject) => {
1380     const start = Date.now();
1381     const [startX, startY] = [inputState.x, inputState.y];
1383     let coords = getElementCenter(a.origin, win);
1384     let target = action.computePointerDestination(a, inputState, coords);
1385     const [targetX, targetY] = [target.x, target.y];
1387     if (!inViewPort(targetX, targetY, win)) {
1388       throw new MoveTargetOutOfBoundsError(
1389         `(${targetX}, ${targetY}) is out of bounds of viewport ` +
1390           `width (${win.innerWidth}) ` +
1391           `and height (${win.innerHeight})`
1392       );
1393     }
1395     const duration =
1396       typeof a.duration == "undefined" ? tickDuration : a.duration;
1397     if (duration === 0) {
1398       // move pointer to destination in one step
1399       performOnePointerMove(inputState, targetX, targetY, win);
1400       resolve();
1401       return;
1402     }
1404     const distanceX = targetX - startX;
1405     const distanceY = targetY - startY;
1406     const ONE_SHOT = Ci.nsITimer.TYPE_ONE_SHOT;
1407     let intermediatePointerEvents = (async () => {
1408       // wait |fps60| ms before performing first incremental pointer move
1409       await new Promise(resolveTimer =>
1410         timer.initWithCallback(resolveTimer, fps60, ONE_SHOT)
1411       );
1413       let durationRatio = Math.floor(Date.now() - start) / duration;
1414       const epsilon = fps60 / duration / 10;
1415       while (1 - durationRatio > epsilon) {
1416         let x = Math.floor(durationRatio * distanceX + startX);
1417         let y = Math.floor(durationRatio * distanceY + startY);
1418         performOnePointerMove(inputState, x, y, win);
1419         // wait |fps60| ms before performing next pointer move
1420         await new Promise(resolveTimer =>
1421           timer.initWithCallback(resolveTimer, fps60, ONE_SHOT)
1422         );
1424         durationRatio = Math.floor(Date.now() - start) / duration;
1425       }
1426     })();
1428     // perform last pointer move after all incremental moves are resolved and
1429     // durationRatio is close enough to 1
1430     intermediatePointerEvents
1431       .then(() => {
1432         performOnePointerMove(inputState, targetX, targetY, win);
1433         resolve();
1434       })
1435       .catch(err => {
1436         reject(err);
1437       });
1438   });
1441 function performOnePointerMove(inputState, targetX, targetY, win) {
1442   if (targetX == inputState.x && targetY == inputState.y) {
1443     return;
1444   }
1446   switch (inputState.subtype) {
1447     case action.PointerType.Mouse:
1448       let mouseEvent = new action.Mouse("mousemove");
1449       mouseEvent.update(inputState);
1450       // TODO both pointermove (if available) and mousemove
1451       event.synthesizeMouseAtPoint(targetX, targetY, mouseEvent, win);
1452       break;
1454     case action.PointerType.Pen:
1455     case action.PointerType.Touch:
1456       throw new UnsupportedOperationError(
1457         "Only 'mouse' pointer type is supported"
1458       );
1460     default:
1461       throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
1462   }
1464   inputState.x = targetX;
1465   inputState.y = targetY;
1469  * Dispatch a pause action equivalent waiting for `a.duration`
1470  * milliseconds, or a default time interval of `tickDuration`.
1472  * @param {action.Action} a
1473  *     Action to dispatch.
1474  * @param {number} tickDuration
1475  *     Duration in milliseconds of this tick.
1477  * @return {Promise}
1478  *     Promise that is resolved after the specified time interval.
1479  */
1480 function dispatchPause(a, tickDuration) {
1481   let ms = typeof a.duration == "undefined" ? tickDuration : a.duration;
1482   return Sleep(ms);
1485 // helpers
1487 function capitalize(str) {
1488   assert.string(str);
1489   return str.charAt(0).toUpperCase() + str.slice(1);
1492 function inViewPort(x, y, win) {
1493   assert.number(x, `Expected x to be finite number`);
1494   assert.number(y, `Expected y to be finite number`);
1495   // Viewport includes scrollbars if rendered.
1496   return !(x < 0 || y < 0 || x > win.innerWidth || y > win.innerHeight);
1499 function getElementCenter(el, win) {
1500   if (element.isDOMElement(el)) {
1501     if (action.specCompatPointerOrigin) {
1502       return element.getInViewCentrePoint(el.getClientRects()[0], win);
1503     }
1504     return element.coordinates(el);
1505   }
1506   return {};