Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / remote / shared / webdriver / Actions.sys.mjs
blobf2a8f0ae488c916196919f09aa9597853811ae65
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 const lazy = {};
10 ChromeUtils.defineESModuleGetters(lazy, {
11   clearTimeout: "resource://gre/modules/Timer.sys.mjs",
12   setTimeout: "resource://gre/modules/Timer.sys.mjs",
14   AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
15   assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
16   AsyncQueue: "chrome://remote/content/shared/AsyncQueue.sys.mjs",
17   error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
18   event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
19   keyData: "chrome://remote/content/shared/webdriver/KeyData.sys.mjs",
20   Log: "chrome://remote/content/shared/Log.sys.mjs",
21   pprint: "chrome://remote/content/shared/Format.sys.mjs",
22   Sleep: "chrome://remote/content/marionette/sync.sys.mjs",
23 });
25 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
26   lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
29 // TODO? With ES 2016 and Symbol you can make a safer approximation
30 // to an enum e.g. https://gist.github.com/xmlking/e86e4f15ec32b12c4689
31 /**
32  * Implements WebDriver Actions API: a low-level interface for providing
33  * virtualized device input to the web browser.
34  *
35  * Typical usage is to construct an action chain and then dispatch it:
36  * const state = new actions.State();
37  * const chain = await actions.Chain.fromJSON(state, protocolData);
38  * await chain.dispatch(state, window);
39  *
40  * @namespace
41  */
42 export const actions = {};
44 // Max interval between two clicks that should result in a dblclick or a tripleclick (in ms)
45 export const CLICK_INTERVAL = 640;
47 /** Map from normalized key value to UI Events modifier key name */
48 const MODIFIER_NAME_LOOKUP = {
49   Alt: "alt",
50   Shift: "shift",
51   Control: "ctrl",
52   Meta: "meta",
55 /**
56  * Object containing various callback functions to be used when deserializing
57  * action sequences and dispatching these.
58  *
59  * @typedef {object} ActionsOptions
60  * @property {Function} isElementOrigin
61  *     Function to check if it's a valid origin element.
62  * @property {Function} getElementOrigin
63  *     Function to retrieve the element reference for an element origin.
64  * @property {Function} assertInViewPort
65  *     Function to check if the coordinates [x, y] are in the visible viewport.
66  * @property {Function} dispatchEvent
67  *     Function to use for dispatching events.
68  * @property {Function} getClientRects
69  *     Function that retrieves the client rects for an element.
70  * @property {Function} getInViewCentrePoint
71  *     Function that calculates the in-view center point for the given
72  *     coordinates [x, y].
73  */
75 /**
76  * State associated with actions.
77  *
78  * Typically each top-level navigable in a WebDriver session should have a
79  * single State object.
80  */
81 actions.State = class {
82   #actionsQueue;
84   /**
85    * Creates a new {@link State} instance.
86    */
87   constructor() {
88     // A queue that ensures that access to the input state is serialized.
89     this.#actionsQueue = new lazy.AsyncQueue();
91     // Tracker for mouse button clicks.
92     this.clickTracker = new ClickTracker();
94     /**
95      * A map between input ID and the device state for that input
96      * source, with one entry for each active input source.
97      *
98      * Maps string => InputSource
99      */
100     this.inputStateMap = new Map();
102     /**
103      * List of {@link Action} associated with current session.  Used to
104      * manage dispatching events when resetting the state of the input sources.
105      * Reset operations are assumed to be idempotent.
106      */
107     this.inputsToCancel = new TickActions();
109     // Map between string input id and numeric pointer id.
110     this.pointerIdMap = new Map();
111   }
113   /**
114    * Returns the list of inputs to cancel when releasing the actions.
115    *
116    * @returns {TickActions}
117    *     The inputs to cancel.
118    */
119   get inputCancelList() {
120     return this.inputsToCancel;
121   }
123   toString() {
124     return `[object ${this.constructor.name} ${JSON.stringify(this)}]`;
125   }
127   /**
128    * Enqueue a new action task.
129    *
130    * @param {Function} task
131    *     The task to queue.
132    *
133    * @returns {Promise}
134    *     Promise that resolves when the task is completed, with the resolved
135    *     value being the result of the task.
136    */
137   enqueueAction(task) {
138     return this.#actionsQueue.enqueue(task);
139   }
141   /**
142    * Get the state for a given input source.
143    *
144    * @param {string} id
145    *     Id of the input source.
146    *
147    * @returns {InputSource}
148    *     State of the input source.
149    */
150   getInputSource(id) {
151     return this.inputStateMap.get(id);
152   }
154   /**
155    * Find or add state for an input source.
156    *
157    * The caller should verify that the returned state is the expected type.
158    *
159    * @param {string} id
160    *     Id of the input source.
161    * @param {InputSource} newInputSource
162    *     State of the input source.
163    *
164    * @returns {InputSource}
165    *     The input source.
166    */
167   getOrAddInputSource(id, newInputSource) {
168     let inputSource = this.getInputSource(id);
170     if (inputSource === undefined) {
171       this.inputStateMap.set(id, newInputSource);
172       inputSource = newInputSource;
173     }
175     return inputSource;
176   }
178   /**
179    * Iterate over all input states of a given type.
180    *
181    * @param {string} type
182    *     Type name of the input source (e.g. "pointer").
183    *
184    * @returns {Iterator<string, InputSource>}
185    *     Iterator over id and input source.
186    */
187   *inputSourcesByType(type) {
188     for (const [id, inputSource] of this.inputStateMap) {
189       if (inputSource.type === type) {
190         yield [id, inputSource];
191       }
192     }
193   }
195   /**
196    * Get a numerical pointer id for a given pointer.
197    *
198    * Pointer ids are positive integers. Mouse pointers are typically
199    * ids 0 or 1. Non-mouse pointers never get assigned id < 2. Each
200    * pointer gets a unique id.
201    *
202    * @param {string} id
203    *     Id of the pointer.
204    * @param {string} type
205    *     Type of the pointer.
206    *
207    * @returns {number}
208    *     The numerical pointer id.
209    */
210   getPointerId(id, type) {
211     let pointerId = this.pointerIdMap.get(id);
213     if (pointerId === undefined) {
214       // Reserve pointer ids 0 and 1 for mouse pointers
215       const idValues = Array.from(this.pointerIdMap.values());
217       if (type === "mouse") {
218         for (const mouseId of [0, 1]) {
219           if (!idValues.includes(mouseId)) {
220             pointerId = mouseId;
221             break;
222           }
223         }
224       }
226       if (pointerId === undefined) {
227         pointerId = Math.max(1, ...idValues) + 1;
228       }
229       this.pointerIdMap.set(id, pointerId);
230     }
232     return pointerId;
233   }
237  * Tracker for mouse button clicks.
238  */
239 export class ClickTracker {
240   #count;
241   #lastButtonClicked;
242   #timer;
244   /**
245    * Creates a new {@link ClickTracker} instance.
246    */
247   constructor() {
248     this.#count = 0;
249     this.#lastButtonClicked = null;
250   }
252   get count() {
253     return this.#count;
254   }
256   #cancelTimer() {
257     lazy.clearTimeout(this.#timer);
258   }
260   #startTimer() {
261     this.#timer = lazy.setTimeout(this.reset.bind(this), CLICK_INTERVAL);
262   }
264   /**
265    * Reset tracking mouse click counter.
266    */
267   reset() {
268     this.#cancelTimer();
269     this.#count = 0;
270     this.#lastButtonClicked = null;
271   }
273   /**
274    * Track |button| click to identify possible double or triple click.
275    *
276    * @param {number} button
277    *     A positive integer that refers to a mouse button.
278    */
279   setClick(button) {
280     this.#cancelTimer();
282     if (
283       this.#lastButtonClicked === null ||
284       this.#lastButtonClicked === button
285     ) {
286       this.#count++;
287     } else {
288       this.#count = 1;
289     }
291     this.#lastButtonClicked = button;
292     this.#startTimer();
293   }
297  * Device state for an input source.
298  */
299 class InputSource {
300   #id;
301   static type = null;
303   /**
304    * Creates a new {@link InputSource} instance.
305    *
306    * @param {string} id
307    *     Id of {@link InputSource}.
308    */
309   constructor(id) {
310     this.#id = id;
311     this.type = this.constructor.type;
312   }
314   toString() {
315     return `[object ${this.constructor.name} id: ${this.#id} type: ${
316       this.type
317     }]`;
318   }
320   /**
321    * Unmarshals a JSON Object to an {@link InputSource}.
322    *
323    * @see https://w3c.github.io/webdriver/#dfn-get-or-create-an-input-source
324    *
325    * @param {State} actionState
326    *     Actions state.
327    * @param {Sequence} actionSequence
328    *     Actions for a specific input source.
329    *
330    * @returns {InputSource}
331    *     An {@link InputSource} object for the type of the
332    *     action {@link Sequence}.
333    *
334    * @throws {InvalidArgumentError}
335    *     If the <code>actionSequence</code> is invalid.
336    */
337   static fromJSON(actionState, actionSequence) {
338     const { id, type } = actionSequence;
340     lazy.assert.string(
341       id,
342       lazy.pprint`Expected "id" to be a string, got ${id}`
343     );
345     const cls = inputSourceTypes.get(type);
346     if (cls === undefined) {
347       throw new lazy.error.InvalidArgumentError(
348         lazy.pprint`Expected known action type, got ${type}`
349       );
350     }
352     const sequenceInputSource = cls.fromJSON(actionState, actionSequence);
353     const inputSource = actionState.getOrAddInputSource(
354       id,
355       sequenceInputSource
356     );
358     if (inputSource.type !== type) {
359       throw new lazy.error.InvalidArgumentError(
360         lazy.pprint`Expected input source ${id} to be ` +
361           `type ${inputSource.type}, got ${type}`
362       );
363     }
364   }
368  * Input state not associated with a specific physical device.
369  */
370 class NullInputSource extends InputSource {
371   static type = "none";
373   /**
374    * Unmarshals a JSON Object to a {@link NullInputSource}.
375    *
376    * @param {State} actionState
377    *     Actions state.
378    * @param {Sequence} actionSequence
379    *     Actions for a specific input source.
380    *
381    * @returns {NullInputSource}
382    *     A {@link NullInputSource} object for the type of the
383    *     action {@link Sequence}.
384    *
385    * @throws {InvalidArgumentError}
386    *     If the <code>actionSequence</code> is invalid.
387    */
388   static fromJSON(actionState, actionSequence) {
389     const { id } = actionSequence;
391     return new this(id);
392   }
396  * Input state associated with a keyboard-type device.
397  */
398 class KeyInputSource extends InputSource {
399   static type = "key";
401   /**
402    * Creates a new {@link KeyInputSource} instance.
403    *
404    * @param {string} id
405    *     Id of {@link InputSource}.
406    */
407   constructor(id) {
408     super(id);
410     this.pressed = new Set();
411     this.alt = false;
412     this.shift = false;
413     this.ctrl = false;
414     this.meta = false;
415   }
417   /**
418    * Unmarshals a JSON Object to a {@link KeyInputSource}.
419    *
420    * @param {State} actionState
421    *     Actions state.
422    * @param {Sequence} actionSequence
423    *     Actions for a specific input source.
424    *
425    * @returns {KeyInputSource}
426    *     A {@link KeyInputSource} object for the type of the
427    *     action {@link Sequence}.
428    *
429    * @throws {InvalidArgumentError}
430    *     If the <code>actionSequence</code> is invalid.
431    */
432   static fromJSON(actionState, actionSequence) {
433     const { id } = actionSequence;
435     return new this(id);
436   }
438   /**
439    * Update modifier state according to |key|.
440    *
441    * @param {string} key
442    *     Normalized key value of a modifier key.
443    * @param {boolean} value
444    *     Value to set the modifier attribute to.
445    *
446    * @throws {InvalidArgumentError}
447    *     If |key| is not a modifier.
448    */
449   setModState(key, value) {
450     if (key in MODIFIER_NAME_LOOKUP) {
451       this[MODIFIER_NAME_LOOKUP[key]] = value;
452     } else {
453       throw new lazy.error.InvalidArgumentError(
454         lazy.pprint`Expected "key" to be one of ${Object.keys(
455           MODIFIER_NAME_LOOKUP
456         )}, got ${key}`
457       );
458     }
459   }
461   /**
462    * Check whether |key| is pressed.
463    *
464    * @param {string} key
465    *     Normalized key value.
466    *
467    * @returns {boolean}
468    *     True if |key| is in set of pressed keys.
469    */
470   isPressed(key) {
471     return this.pressed.has(key);
472   }
474   /**
475    * Add |key| to the set of pressed keys.
476    *
477    * @param {string} key
478    *     Normalized key value.
479    *
480    * @returns {boolean}
481    *     True if |key| is in list of pressed keys.
482    */
483   press(key) {
484     return this.pressed.add(key);
485   }
487   /**
488    * Remove |key| from the set of pressed keys.
489    *
490    * @param {string} key
491    *     Normalized key value.
492    *
493    * @returns {boolean}
494    *     True if |key| was present before removal, false otherwise.
495    */
496   release(key) {
497     return this.pressed.delete(key);
498   }
502  * Input state associated with a pointer-type device.
503  */
504 class PointerInputSource extends InputSource {
505   static type = "pointer";
507   /**
508    * Creates a new {@link PointerInputSource} instance.
509    *
510    * @param {string} id
511    *     Id of {@link InputSource}.
512    * @param {Pointer} pointer
513    *     The specific {@link Pointer} type associated with this input source.
514    */
515   constructor(id, pointer) {
516     super(id);
518     this.pointer = pointer;
519     this.x = 0;
520     this.y = 0;
521     this.pressed = new Set();
522   }
524   /**
525    * Check whether |button| is pressed.
526    *
527    * @param {number} button
528    *     Positive integer that refers to a mouse button.
529    *
530    * @returns {boolean}
531    *     True if |button| is in set of pressed buttons.
532    */
533   isPressed(button) {
534     lazy.assert.positiveInteger(
535       button,
536       lazy.pprint`Expected "button" to be a positive integer, got ${button}`
537     );
539     return this.pressed.has(button);
540   }
542   /**
543    * Add |button| to the set of pressed keys.
544    *
545    * @param {number} button
546    *     Positive integer that refers to a mouse button.
547    */
548   press(button) {
549     lazy.assert.positiveInteger(
550       button,
551       lazy.pprint`Expected "button" to be a positive integer, got ${button}`
552     );
554     this.pressed.add(button);
555   }
557   /**
558    * Remove |button| from the set of pressed buttons.
559    *
560    * @param {number} button
561    *     A positive integer that refers to a mouse button.
562    *
563    * @returns {boolean}
564    *     True if |button| was present before removals, false otherwise.
565    */
566   release(button) {
567     lazy.assert.positiveInteger(
568       button,
569       lazy.pprint`Expected "button" to be a positive integer, got ${button}`
570     );
572     return this.pressed.delete(button);
573   }
575   /**
576    * Unmarshals a JSON Object to a {@link PointerInputSource}.
577    *
578    * @param {State} actionState
579    *     Actions state.
580    * @param {Sequence} actionSequence
581    *     Actions for a specific input source.
582    *
583    * @returns {PointerInputSource}
584    *     A {@link PointerInputSource} object for the type of the
585    *     action {@link Sequence}.
586    *
587    * @throws {InvalidArgumentError}
588    *     If the <code>actionSequence</code> is invalid.
589    */
590   static fromJSON(actionState, actionSequence) {
591     const { id, parameters } = actionSequence;
592     let pointerType = "mouse";
594     if (parameters !== undefined) {
595       lazy.assert.object(
596         parameters,
597         lazy.pprint`Expected "parameters" to be an object, got ${parameters}`
598       );
600       if (parameters.pointerType !== undefined) {
601         pointerType = lazy.assert.string(
602           parameters.pointerType,
603           lazy.pprint(
604             `Expected "pointerType" to be a string, got ${parameters.pointerType}`
605           )
606         );
608         if (!["mouse", "pen", "touch"].includes(pointerType)) {
609           throw new lazy.error.InvalidArgumentError(
610             lazy.pprint`Expected "pointerType" to be one of "mouse", "pen", or "touch"`
611           );
612         }
613       }
614     }
616     const pointerId = actionState.getPointerId(id, pointerType);
617     const pointer = Pointer.fromJSON(pointerId, pointerType);
619     return new this(id, pointer);
620   }
624  * Input state associated with a wheel-type device.
625  */
626 class WheelInputSource extends InputSource {
627   static type = "wheel";
629   /**
630    * Unmarshals a JSON Object to a {@link WheelInputSource}.
631    *
632    * @param {State} actionState
633    *     Actions state.
634    * @param {Sequence} actionSequence
635    *     Actions for a specific input source.
636    *
637    * @returns {WheelInputSource}
638    *     A {@link WheelInputSource} object for the type of the
639    *     action {@link Sequence}.
640    *
641    * @throws {InvalidArgumentError}
642    *     If the <code>actionSequence</code> is invalid.
643    */
644   static fromJSON(actionState, actionSequence) {
645     const { id } = actionSequence;
647     return new this(id);
648   }
651 const inputSourceTypes = new Map();
652 for (const cls of [
653   NullInputSource,
654   KeyInputSource,
655   PointerInputSource,
656   WheelInputSource,
657 ]) {
658   inputSourceTypes.set(cls.type, cls);
662  * Representation of a coordinate origin
663  */
664 class Origin {
665   /**
666    * Viewport coordinates of the origin of this coordinate system.
667    *
668    * This is overridden in subclasses to provide a class-specific origin.
669    */
670   getOriginCoordinates() {
671     throw new Error(
672       `originCoordinates not defined for ${this.constructor.name}`
673     );
674   }
676   /**
677    * Convert [x, y] coordinates to viewport coordinates.
678    *
679    * @param {InputSource} inputSource
680    *     State of the current input device
681    * @param {Array<number>} coords
682    *     Coordinates [x, y] of the target relative to the origin.
683    * @param {ActionsOptions} options
684    *     Configuration of actions dispatch.
685    *
686    * @returns {Array<number>}
687    *     Viewport coordinates [x, y].
688    */
689   async getTargetCoordinates(inputSource, coords, options) {
690     const [x, y] = coords;
691     const origin = await this.getOriginCoordinates(inputSource, options);
693     return [origin.x + x, origin.y + y];
694   }
696   /**
697    * Unmarshals a JSON Object to an {@link Origin}.
698    *
699    * @param {string|Element=} origin
700    *     Type of origin, one of "viewport", "pointer", {@link Element}
701    *     or undefined.
702    * @param {ActionsOptions} options
703    *     Configuration for actions.
704    *
705    * @returns {Promise<Origin>}
706    *     Promise that resolves to an {@link Origin} object
707    *     representing the origin.
708    *
709    * @throws {InvalidArgumentError}
710    *     If <code>origin</code> isn't a valid origin.
711    */
712   static async fromJSON(origin, options) {
713     const { context, getElementOrigin, isElementOrigin } = options;
715     if (origin === undefined || origin === "viewport") {
716       return new ViewportOrigin();
717     }
719     if (origin === "pointer") {
720       return new PointerOrigin();
721     }
723     if (isElementOrigin(origin)) {
724       const element = await getElementOrigin(origin, context);
726       return new ElementOrigin(element);
727     }
729     throw new lazy.error.InvalidArgumentError(
730       `Expected "origin" to be undefined, "viewport", "pointer", ` +
731         lazy.pprint`or an element, got: ${origin}`
732     );
733   }
736 class ViewportOrigin extends Origin {
737   getOriginCoordinates() {
738     return { x: 0, y: 0 };
739   }
742 class PointerOrigin extends Origin {
743   getOriginCoordinates(inputSource) {
744     return { x: inputSource.x, y: inputSource.y };
745   }
749  * Representation of an element origin.
750  */
751 class ElementOrigin extends Origin {
752   /**
753    * Creates a new {@link ElementOrigin} instance.
754    *
755    * @param {Element} element
756    *     The element providing the coordinate origin.
757    */
758   constructor(element) {
759     super();
761     this.element = element;
762   }
764   /**
765    * Retrieve the coordinates of the origin's in-view center point.
766    *
767    * @param {InputSource} _inputSource
768    *     [unused] Current input device.
769    * @param {ActionsOptions} options
770    *
771    * @returns {Promise<Array<number>>}
772    *     Promise that resolves to the coordinates [x, y].
773    */
774   async getOriginCoordinates(_inputSource, options) {
775     const { context, getClientRects, getInViewCentrePoint } = options;
777     const clientRects = await getClientRects(this.element, context);
779     // The spec doesn't handle this case: https://github.com/w3c/webdriver/issues/1642
780     if (!clientRects.length) {
781       throw new lazy.error.MoveTargetOutOfBoundsError(
782         lazy.pprint`Origin element ${this.element} is not displayed`
783       );
784     }
786     return getInViewCentrePoint(clientRects[0], context);
787   }
791  * Represents the behavior of a single input source at a single
792  * point in time.
793  */
794 class Action {
795   /** Type of the input source associated with this action */
796   static type = null;
797   /** Type of action specific to the input source */
798   static subtype = null;
799   /** Whether this kind of action affects the overall duration of a tick */
800   affectsWallClockTime = false;
802   /**
803    * Creates a new {@link Action} instance.
804    *
805    * @param {string} id
806    *     Id of {@link InputSource}.
807    */
808   constructor(id) {
809     this.id = id;
810     this.type = this.constructor.type;
811     this.subtype = this.constructor.subtype;
812   }
814   toString() {
815     return `[${this.constructor.name} ${this.type}:${this.subtype}]`;
816   }
818   /**
819    * Dispatch the action to the relevant window.
820    *
821    * This is overridden by subclasses to implement the type-specific
822    * dispatch of the action.
823    *
824    * @returns {Promise}
825    *     Promise that is resolved once the action is complete.
826    */
827   dispatch() {
828     throw new Error(
829       `Action subclass ${this.constructor.name} must override dispatch()`
830     );
831   }
833   /**
834    * Unmarshals a JSON Object to an {@link Action}.
835    *
836    * @param {string} type
837    *     Type of {@link InputSource}.
838    * @param {string} id
839    *     Id of {@link InputSource}.
840    * @param {object} actionItem
841    *     Object representing a single action.
842    * @param {ActionsOptions} options
843    *     Configuration for actions.
844    *
845    * @returns {Promise<Action>}
846    *     Promise that resolves to an action that can be dispatched.
847    *
848    * @throws {InvalidArgumentError}
849    *     If the <code>actionItem</code> attribute is invalid.
850    */
851   static fromJSON(type, id, actionItem, options) {
852     lazy.assert.object(
853       actionItem,
854       lazy.pprint`Expected "action" to be an object, got ${actionItem}`
855     );
857     const subtype = actionItem.type;
858     const subtypeMap = actionTypes.get(type);
860     if (subtypeMap === undefined) {
861       throw new lazy.error.InvalidArgumentError(
862         lazy.pprint`Expected known action type, got ${type}`
863       );
864     }
866     let cls = subtypeMap.get(subtype);
867     // Non-device specific actions can happen for any action type
868     if (cls === undefined) {
869       cls = actionTypes.get("none").get(subtype);
870     }
871     if (cls === undefined) {
872       throw new lazy.error.InvalidArgumentError(
873         lazy.pprint`Expected known subtype for type ${type}, got ${subtype}`
874       );
875     }
877     return cls.fromJSON(id, actionItem, options);
878   }
882  * Action not associated with a specific input device.
883  */
884 class NullAction extends Action {
885   static type = "none";
889  * Action that waits for a given duration.
890  */
891 class PauseAction extends NullAction {
892   static subtype = "pause";
893   affectsWallClockTime = true;
895   /**
896    * Creates a new {@link PauseAction} instance.
897    *
898    * @param {string} id
899    *     Id of {@link InputSource}.
900    * @param {object} options
901    * @param {number} options.duration
902    *     Time to pause, in ms.
903    */
904   constructor(id, options) {
905     super(id);
907     const { duration } = options;
908     this.duration = duration;
909   }
911   /**
912    * Dispatch pause action.
913    *
914    * @param {State} state
915    *     The {@link State} of the action.
916    * @param {InputSource} inputSource
917    *     Current input device.
918    * @param {number} tickDuration
919    *     Length of the current tick, in ms.
920    *
921    * @returns {Promise}
922    *     Promise that is resolved once the action is complete.
923    */
924   dispatch(state, inputSource, tickDuration) {
925     const ms = this.duration ?? tickDuration;
927     lazy.logger.trace(
928       ` Dispatch ${this.constructor.name} with ${this.id} ${ms}`
929     );
931     return lazy.Sleep(ms);
932   }
934   /**
935    * Unmarshals a JSON Object to a {@link PauseAction}.
936    *
937    * @see https://w3c.github.io/webdriver/#dfn-process-a-null-action
938    *
939    * @param {string} id
940    *     Id of {@link InputSource}.
941    * @param {object} actionItem
942    *     Object representing a single action.
943    *
944    * @returns {PauseAction}
945    *     A pause action that can be dispatched.
946    *
947    * @throws {InvalidArgumentError}
948    *     If the <code>actionItem</code> attribute is invalid.
949    */
950   static fromJSON(id, actionItem) {
951     const { duration } = actionItem;
953     if (duration !== undefined) {
954       lazy.assert.positiveInteger(
955         duration,
956         lazy.pprint`Expected "duration" to be a positive integer, got ${duration}`
957       );
958     }
960     return new this(id, { duration });
961   }
965  * Action associated with a keyboard input device
966  */
967 class KeyAction extends Action {
968   static type = "key";
970   /**
971    * Creates a new {@link KeyAction} instance.
972    *
973    * @param {string} id
974    *     Id of {@link InputSource}.
975    * @param {object} options
976    * @param {string} options.value
977    *     The key character.
978    */
979   constructor(id, options) {
980     super(id);
982     const { value } = options;
984     this.value = value;
985   }
987   getEventData(inputSource) {
988     let value = this.value;
990     if (inputSource.shift) {
991       value = lazy.keyData.getShiftedKey(value);
992     }
994     return new KeyEventData(value);
995   }
997   /**
998    * Unmarshals a JSON Object to a {@link KeyAction}.
999    *
1000    * @see https://w3c.github.io/webdriver/#dfn-process-a-key-action
1001    *
1002    * @param {string} id
1003    *     Id of {@link InputSource}.
1004    * @param {object} actionItem
1005    *     Object representing a single action.
1006    *
1007    * @returns {KeyAction}
1008    *     A key action that can be dispatched.
1009    *
1010    * @throws {InvalidArgumentError}
1011    *     If the <code>actionItem</code> attribute is invalid.
1012    */
1013   static fromJSON(id, actionItem) {
1014     const { value } = actionItem;
1016     lazy.assert.string(
1017       value,
1018       'Expected "value" to be a string that represents single code point ' +
1019         lazy.pprint`or grapheme cluster, got ${value}`
1020     );
1022     let segmenter = new Intl.Segmenter();
1023     lazy.assert.that(v => {
1024       let graphemeIterator = segmenter.segment(v)[Symbol.iterator]();
1025       // We should have exactly one grapheme cluster, so the first iterator
1026       // value must be defined, but the second one must be undefined
1027       return (
1028         graphemeIterator.next().value !== undefined &&
1029         graphemeIterator.next().value === undefined
1030       );
1031     }, `Expected "value" to be a string that represents single code point or grapheme cluster, got "${value}"`)(
1032       value
1033     );
1035     return new this(id, { value });
1036   }
1040  * Action equivalent to pressing a key on a keyboard.
1042  * @param {string} id
1043  *     Id of {@link InputSource}.
1044  * @param {object} options
1045  * @param {string} options.value
1046  *     The key character.
1047  */
1048 class KeyDownAction extends KeyAction {
1049   static subtype = "keyDown";
1051   /**
1052    * Dispatch a keydown action.
1053    *
1054    * @param {State} state
1055    *     The {@link State} of the action.
1056    * @param {InputSource} inputSource
1057    *     Current input device.
1058    * @param {number} tickDuration
1059    *     [unused] Length of the current tick, in ms.
1060    * @param {ActionsOptions} options
1061    *     Configuration of actions dispatch.
1062    *
1063    * @returns {Promise}
1064    *     Promise that is resolved once the action is complete.
1065    */
1066   async dispatch(state, inputSource, tickDuration, options) {
1067     const { context, dispatchEvent } = options;
1069     lazy.logger.trace(
1070       ` Dispatch ${this.constructor.name} with ${this.id} ${this.value}`
1071     );
1073     const keyEvent = this.getEventData(inputSource);
1074     keyEvent.repeat = inputSource.isPressed(keyEvent.key);
1075     inputSource.press(keyEvent.key);
1077     if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
1078       inputSource.setModState(keyEvent.key, true);
1079     }
1081     keyEvent.update(state, inputSource);
1083     await dispatchEvent("synthesizeKeyDown", context, {
1084       x: inputSource.x,
1085       y: inputSource.y,
1086       eventData: keyEvent,
1087     });
1089     // Append a copy of |this| with keyUp subtype if event dispatched
1090     state.inputsToCancel.push(new KeyUpAction(this.id, this));
1091   }
1095  * Action equivalent to releasing a key on a keyboard.
1097  * @param {string} id
1098  *     Id of {@link InputSource}.
1099  * @param {object} options
1100  * @param {string} options.value
1101  *     The key character.
1102  */
1103 class KeyUpAction extends KeyAction {
1104   static subtype = "keyUp";
1106   /**
1107    * Dispatch a keyup action.
1108    *
1109    * @param {State} state
1110    *     The {@link State} of the action.
1111    * @param {InputSource} inputSource
1112    *     Current input device.
1113    * @param {number} tickDuration
1114    *     [unused] Length of the current tick, in ms.
1115    * @param {ActionsOptions} options
1116    *     Configuration of actions dispatch.
1117    *
1118    * @returns {Promise}
1119    *     Promise that is resolved once the action is complete.
1120    */
1121   async dispatch(state, inputSource, tickDuration, options) {
1122     const { context, dispatchEvent } = options;
1124     lazy.logger.trace(
1125       ` Dispatch ${this.constructor.name} with ${this.id} ${this.value}`
1126     );
1128     const keyEvent = this.getEventData(inputSource);
1130     if (!inputSource.isPressed(keyEvent.key)) {
1131       return;
1132     }
1134     if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
1135       inputSource.setModState(keyEvent.key, false);
1136     }
1138     inputSource.release(keyEvent.key);
1139     keyEvent.update(state, inputSource);
1141     await dispatchEvent("synthesizeKeyUp", context, {
1142       x: inputSource.x,
1143       y: inputSource.y,
1144       eventData: keyEvent,
1145     });
1146   }
1150  * Action associated with a pointer input device
1151  */
1152 class PointerAction extends Action {
1153   static type = "pointer";
1155   /**
1156    * Creates a new {@link PointerAction} instance.
1157    *
1158    * @param {string} id
1159    *     Id of {@link InputSource}.
1160    * @param {object} options
1161    * @param {number=} options.width
1162    *     Width of pointer in pixels.
1163    * @param {number=} options.height
1164    *     Height of pointer in pixels.
1165    * @param {number=} options.pressure
1166    *     Pressure of pointer.
1167    * @param {number=} options.tangentialPressure
1168    *     Tangential pressure of pointer.
1169    * @param {number=} options.tiltX
1170    *     X tilt angle of pointer.
1171    * @param {number=} options.tiltY
1172    *     Y tilt angle of pointer.
1173    * @param {number=} options.twist
1174    *     Twist angle of pointer.
1175    * @param {number=} options.altitudeAngle
1176    *     Altitude angle of pointer.
1177    * @param {number=} options.azimuthAngle
1178    *     Azimuth angle of pointer.
1179    */
1180   constructor(id, options) {
1181     super(id);
1183     const {
1184       width,
1185       height,
1186       pressure,
1187       tangentialPressure,
1188       tiltX,
1189       tiltY,
1190       twist,
1191       altitudeAngle,
1192       azimuthAngle,
1193     } = options;
1195     this.width = width;
1196     this.height = height;
1197     this.pressure = pressure;
1198     this.tangentialPressure = tangentialPressure;
1199     this.tiltX = tiltX;
1200     this.tiltY = tiltY;
1201     this.twist = twist;
1202     this.altitudeAngle = altitudeAngle;
1203     this.azimuthAngle = azimuthAngle;
1204   }
1206   /**
1207    * Validate properties common to all pointer types.
1208    *
1209    * @param {object} actionItem
1210    *     Object representing a single pointer action.
1211    *
1212    * @returns {object}
1213    *     Properties of the pointer action; contains `width`, `height`,
1214    *     `pressure`, `tangentialPressure`, `tiltX`, `tiltY`, `twist`,
1215    *     `altitudeAngle`, and `azimuthAngle`.
1216    */
1217   static validateCommon(actionItem) {
1218     const {
1219       width,
1220       height,
1221       pressure,
1222       tangentialPressure,
1223       tiltX,
1224       tiltY,
1225       twist,
1226       altitudeAngle,
1227       azimuthAngle,
1228     } = actionItem;
1230     if (width !== undefined) {
1231       lazy.assert.positiveInteger(
1232         width,
1233         lazy.pprint`Expected "width" to be a positive integer, got ${width}`
1234       );
1235     }
1236     if (height !== undefined) {
1237       lazy.assert.positiveInteger(
1238         height,
1239         lazy.pprint`Expected "height" to be a positive integer, got ${height}`
1240       );
1241     }
1242     if (pressure !== undefined) {
1243       lazy.assert.numberInRange(
1244         pressure,
1245         [0, 1],
1246         lazy.pprint`Expected "pressure" to be in range 0 to 1, got ${pressure}`
1247       );
1248     }
1249     if (tangentialPressure !== undefined) {
1250       lazy.assert.numberInRange(
1251         tangentialPressure,
1252         [-1, 1],
1253         'Expected "tangentialPressure" to be in range -1 to 1, ' +
1254           lazy.pprint`got ${tangentialPressure}`
1255       );
1256     }
1257     if (tiltX !== undefined) {
1258       lazy.assert.integerInRange(
1259         tiltX,
1260         [-90, 90],
1261         lazy.pprint`Expected "tiltX" to be in range -90 to 90, got ${tiltX}`
1262       );
1263     }
1264     if (tiltY !== undefined) {
1265       lazy.assert.integerInRange(
1266         tiltY,
1267         [-90, 90],
1268         lazy.pprint`Expected "tiltY" to be in range -90 to 90, got ${tiltY}`
1269       );
1270     }
1271     if (twist !== undefined) {
1272       lazy.assert.integerInRange(
1273         twist,
1274         [0, 359],
1275         lazy.pprint`Expected "twist" to be in range 0 to 359, got ${twist}`
1276       );
1277     }
1278     if (altitudeAngle !== undefined) {
1279       lazy.assert.numberInRange(
1280         altitudeAngle,
1281         [0, Math.PI / 2],
1282         'Expected "altitudeAngle" to be in range 0 to ${Math.PI / 2}, ' +
1283           lazy.pprint`got ${altitudeAngle}`
1284       );
1285     }
1286     if (azimuthAngle !== undefined) {
1287       lazy.assert.numberInRange(
1288         azimuthAngle,
1289         [0, 2 * Math.PI],
1290         'Expected "azimuthAngle" to be in range 0 to ${2 * Math.PI}, ' +
1291           lazy.pprint`got ${azimuthAngle}`
1292       );
1293     }
1295     return {
1296       width,
1297       height,
1298       pressure,
1299       tangentialPressure,
1300       tiltX,
1301       tiltY,
1302       twist,
1303       altitudeAngle,
1304       azimuthAngle,
1305     };
1306   }
1310  * Action associated with a pointer input device being depressed.
1311  */
1312 class PointerDownAction extends PointerAction {
1313   static subtype = "pointerDown";
1315   /**
1316    * Creates a new {@link PointerAction} instance.
1317    *
1318    * @param {string} id
1319    *     Id of {@link InputSource}.
1320    * @param {object} options
1321    * @param {number} options.button
1322    *     Button being pressed. For devices without buttons (e.g. touch),
1323    *     this should be 0.
1324    * @param {number=} options.width
1325    *     Width of pointer in pixels.
1326    * @param {number=} options.height
1327    *     Height of pointer in pixels.
1328    * @param {number=} options.pressure
1329    *     Pressure of pointer.
1330    * @param {number=} options.tangentialPressure
1331    *     Tangential pressure of pointer.
1332    * @param {number=} options.tiltX
1333    *     X tilt angle of pointer.
1334    * @param {number=} options.tiltY
1335    *     Y tilt angle of pointer.
1336    * @param {number=} options.twist
1337    *     Twist angle of pointer.
1338    * @param {number=} options.altitudeAngle
1339    *     Altitude angle of pointer.
1340    * @param {number=} options.azimuthAngle
1341    *     Azimuth angle of pointer.
1342    */
1343   constructor(id, options) {
1344     super(id, options);
1346     const { button } = options;
1347     this.button = button;
1348   }
1350   /**
1351    * Dispatch a pointerdown action.
1352    *
1353    * @param {State} state
1354    *     The {@link State} of the action.
1355    * @param {InputSource} inputSource
1356    *     Current input device.
1357    * @param {number} tickDuration
1358    *     [unused] Length of the current tick, in ms.
1359    * @param {ActionsOptions} options
1360    *     Configuration of actions dispatch.
1361    *
1362    * @returns {Promise}
1363    *     Promise that is resolved once the action is complete.
1364    */
1365   async dispatch(state, inputSource, tickDuration, options) {
1366     lazy.logger.trace(
1367       `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} button: ${this.button}`
1368     );
1370     if (inputSource.isPressed(this.button)) {
1371       return;
1372     }
1374     inputSource.press(this.button);
1376     await inputSource.pointer.pointerDown(state, inputSource, this, options);
1378     // Append a copy of |this| with pointerUp subtype if event dispatched
1379     state.inputsToCancel.push(new PointerUpAction(this.id, this));
1380   }
1382   /**
1383    * Unmarshals a JSON Object to a {@link PointerDownAction}.
1384    *
1385    * @see https://w3c.github.io/webdriver/#dfn-process-a-pointer-up-or-pointer-down-action
1386    *
1387    * @param {string} id
1388    *     Id of {@link InputSource}.
1389    * @param {object} actionItem
1390    *     Object representing a single action.
1391    *
1392    * @returns {PointerDownAction}
1393    *     A pointer down action that can be dispatched.
1394    *
1395    * @throws {InvalidArgumentError}
1396    *     If the <code>actionItem</code> attribute is invalid.
1397    */
1398   static fromJSON(id, actionItem) {
1399     const { button } = actionItem;
1400     const props = PointerAction.validateCommon(actionItem);
1402     lazy.assert.positiveInteger(
1403       button,
1404       lazy.pprint`Expected "button" to be a positive integer, got ${button}`
1405     );
1407     props.button = button;
1409     return new this(id, props);
1410   }
1414  * Action associated with a pointer input device being released.
1415  */
1416 class PointerUpAction extends PointerAction {
1417   static subtype = "pointerUp";
1419   /**
1420    * Creates a new {@link PointerUpAction} instance.
1421    *
1422    * @param {string} id
1423    *     Id of {@link InputSource}.
1424    * @param {object} options
1425    * @param {number} options.button
1426    *     Button being pressed. For devices without buttons (e.g. touch),
1427    *     this should be 0.
1428    * @param {number=} options.width
1429    *     Width of pointer in pixels.
1430    * @param {number=} options.height
1431    *     Height of pointer in pixels.
1432    * @param {number=} options.pressure
1433    *     Pressure of pointer.
1434    * @param {number=} options.tangentialPressure
1435    *     Tangential pressure of pointer.
1436    * @param {number=} options.tiltX
1437    *     X tilt angle of pointer.
1438    * @param {number=} options.tiltY
1439    *     Y tilt angle of pointer.
1440    * @param {number=} options.twist
1441    *     Twist angle of pointer.
1442    * @param {number=} options.altitudeAngle
1443    *     Altitude angle of pointer.
1444    * @param {number=} options.azimuthAngle
1445    *     Azimuth angle of pointer.
1446    */
1447   constructor(id, options) {
1448     super(id, options);
1450     const { button } = options;
1451     this.button = button;
1452   }
1454   /**
1455    * Dispatch a pointerup action.
1456    *
1457    * @param {State} state
1458    *     The {@link State} of the action.
1459    * @param {InputSource} inputSource
1460    *     Current input device.
1461    * @param {number} tickDuration
1462    *     [unused] Length of the current tick, in ms.
1463    * @param {ActionsOptions} options
1464    *     Configuration of actions dispatch.
1465    *
1466    * @returns {Promise}
1467    *     Promise that is resolved once the action is complete.
1468    */
1469   async dispatch(state, inputSource, tickDuration, options) {
1470     lazy.logger.trace(
1471       `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} button: ${this.button}`
1472     );
1474     if (!inputSource.isPressed(this.button)) {
1475       return;
1476     }
1478     inputSource.release(this.button);
1480     await inputSource.pointer.pointerUp(state, inputSource, this, options);
1481   }
1483   /**
1484    * Unmarshals a JSON Object to a {@link PointerUpAction}.
1485    *
1486    * @see https://w3c.github.io/webdriver/#dfn-process-a-pointer-up-or-pointer-down-action
1487    *
1488    * @param {string} id
1489    *     Id of {@link InputSource}.
1490    * @param {object} actionItem
1491    *     Object representing a single action.
1492    *
1493    * @returns {PointerUpAction}
1494    *     A pointer up action that can be dispatched.
1495    *
1496    * @throws {InvalidArgumentError}
1497    *     If the <code>actionItem</code> attribute is invalid.
1498    */
1499   static fromJSON(id, actionItem) {
1500     const { button } = actionItem;
1501     const props = PointerAction.validateCommon(actionItem);
1503     lazy.assert.positiveInteger(
1504       button,
1505       lazy.pprint`Expected "button" to be a positive integer, got ${button}`
1506     );
1508     props.button = button;
1510     return new this(id, props);
1511   }
1515  * Action associated with a pointer input device being moved.
1516  */
1517 class PointerMoveAction extends PointerAction {
1518   static subtype = "pointerMove";
1519   affectsWallClockTime = true;
1521   /**
1522    * Creates a new {@link PointerMoveAction} instance.
1523    *
1524    * @param {string} id
1525    *     Id of {@link InputSource}.
1526    * @param {object} options
1527    * @param {number} options.button
1528    *     Button being pressed. For devices without buttons (e.g. touch),
1529    *     this should be 0.
1530    * @param {number=} options.width
1531    *     Width of pointer in pixels.
1532    * @param {number=} options.height
1533    *     Height of pointer in pixels.
1534    * @param {number=} options.pressure
1535    *     Pressure of pointer.
1536    * @param {number=} options.tangentialPressure
1537    *     Tangential pressure of pointer.
1538    * @param {number=} options.tiltX
1539    *     X tilt angle of pointer.
1540    * @param {number=} options.tiltY
1541    *     Y tilt angle of pointer.
1542    * @param {number=} options.twist
1543    *     Twist angle of pointer.
1544    * @param {number=} options.altitudeAngle
1545    *     Altitude angle of pointer.
1546    * @param {number=} options.azimuthAngle
1547    *     Azimuth angle of pointer.
1548    */
1549   constructor(id, options) {
1550     super(id, options);
1552     const { duration, origin, x, y } = options;
1553     this.duration = duration;
1555     this.origin = origin;
1556     this.x = x;
1557     this.y = y;
1558   }
1560   /**
1561    * Dispatch a pointermove action.
1562    *
1563    * @param {State} state
1564    *     The {@link State} of the action.
1565    * @param {InputSource} inputSource
1566    *     Current input device.
1567    * @param {number} tickDuration
1568    *     [unused] Length of the current tick, in ms.
1569    * @param {ActionsOptions} options
1570    *     Configuration of actions dispatch.
1571    *
1572    * @returns {Promise}
1573    *     Promise that is resolved once the action is complete.
1574    */
1575   async dispatch(state, inputSource, tickDuration, options) {
1576     const { assertInViewPort, context } = options;
1578     lazy.logger.trace(
1579       `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} x: ${this.x} y: ${this.y}`
1580     );
1582     const target = await this.origin.getTargetCoordinates(
1583       inputSource,
1584       [this.x, this.y],
1585       options
1586     );
1588     await assertInViewPort(target, context);
1590     return moveOverTime(
1591       [[inputSource.x, inputSource.y]],
1592       [target],
1593       this.duration ?? tickDuration,
1594       async _target =>
1595         await this.performPointerMoveStep(state, inputSource, _target, options)
1596     );
1597   }
1599   /**
1600    * Perform one part of a pointer move corresponding to a specific emitted event.
1601    *
1602    * @param {State} state
1603    *     The {@link State} of actions.
1604    * @param {InputSource} inputSource
1605    *     Current input device.
1606    * @param {Array<Array<number>>} targets
1607    *     Array of [x, y] arrays specifying the viewport coordinates to move to.
1608    * @param {ActionsOptions} options
1609    *     Configuration of actions dispatch.
1610    *
1611    * @returns {Promise}
1612    */
1613   async performPointerMoveStep(state, inputSource, targets, options) {
1614     if (targets.length !== 1) {
1615       throw new Error(
1616         "PointerMoveAction.performPointerMoveStep requires a single target"
1617       );
1618     }
1620     const target = targets[0];
1621     lazy.logger.trace(
1622       `PointerMoveAction.performPointerMoveStep ${JSON.stringify(target)}`
1623     );
1624     if (target[0] == inputSource.x && target[1] == inputSource.y) {
1625       return;
1626     }
1628     await inputSource.pointer.pointerMove(
1629       state,
1630       inputSource,
1631       this,
1632       target[0],
1633       target[1],
1634       options
1635     );
1637     inputSource.x = target[0];
1638     inputSource.y = target[1];
1639   }
1641   /**
1642    * Unmarshals a JSON Object to a {@link PointerMoveAction}.
1643    *
1644    * @see https://w3c.github.io/webdriver/#dfn-process-a-pointer-move-action
1645    *
1646    * @param {string} id
1647    *     Id of {@link InputSource}.
1648    * @param {object} actionItem
1649    *     Object representing a single action.
1650    * @param {ActionsOptions} options
1651    *     Configuration for actions.
1652    *
1653    * @returns {Promise<PointerMoveAction>}
1654    *     A pointer move action that can be dispatched.
1655    *
1656    * @throws {InvalidArgumentError}
1657    *     If the <code>actionItem</code> attribute is invalid.
1658    */
1659   static async fromJSON(id, actionItem, options) {
1660     const { duration, origin, x, y } = actionItem;
1662     if (duration !== undefined) {
1663       lazy.assert.positiveInteger(
1664         duration,
1665         lazy.pprint`Expected "duration" to be a positive integer, got ${duration}`
1666       );
1667     }
1669     const originObject = await Origin.fromJSON(origin, options);
1671     lazy.assert.integer(
1672       x,
1673       lazy.pprint`Expected "x" to be an integer, got ${x}`
1674     );
1675     lazy.assert.integer(
1676       y,
1677       lazy.pprint`Expected "y" to be an integer, got ${y}`
1678     );
1680     const props = PointerAction.validateCommon(actionItem);
1681     props.duration = duration;
1682     props.origin = originObject;
1683     props.x = x;
1684     props.y = y;
1686     return new this(id, props);
1687   }
1691  * Action associated with a wheel input device.
1692  */
1693 class WheelAction extends Action {
1694   static type = "wheel";
1698  * Action associated with scrolling a scroll wheel
1699  */
1700 class WheelScrollAction extends WheelAction {
1701   static subtype = "scroll";
1702   affectsWallClockTime = true;
1704   /**
1705    * Creates a new {@link WheelScrollAction} instance.
1706    *
1707    * @param {number} id
1708    *     Id of {@link InputSource}.
1709    * @param {object} options
1710    * @param {Origin} options.origin
1711    *     {@link Origin} of target coordinates.
1712    * @param {number} options.x
1713    *     X value of scroll coordinates.
1714    * @param {number} options.y
1715    *     Y value of scroll coordinates.
1716    * @param {number} options.deltaX
1717    *     Number of CSS pixels to scroll in X direction.
1718    * @param {number} options.deltaY
1719    *     Number of CSS pixels to scroll in Y direction.
1720    */
1721   constructor(id, options) {
1722     super(id);
1724     const { duration, origin, x, y, deltaX, deltaY } = options;
1726     this.duration = duration;
1727     this.origin = origin;
1728     this.x = x;
1729     this.y = y;
1730     this.deltaX = deltaX;
1731     this.deltaY = deltaY;
1732   }
1734   /**
1735    * Unmarshals a JSON Object to a {@link WheelScrollAction}.
1736    *
1737    * @param {string} id
1738    *     Id of {@link InputSource}.
1739    * @param {object} actionItem
1740    *     Object representing a single action.
1741    * @param {ActionsOptions} options
1742    *     Configuration for actions.
1743    *
1744    * @returns {Promise<WheelScrollAction>}
1745    *     Promise that resolves to a wheel scroll action
1746    *     that can be dispatched.
1747    *
1748    * @throws {InvalidArgumentError}
1749    *     If the <code>actionItem</code> attribute is invalid.
1750    */
1751   static async fromJSON(id, actionItem, options) {
1752     const { duration, origin, x, y, deltaX, deltaY } = actionItem;
1754     if (duration !== undefined) {
1755       lazy.assert.positiveInteger(
1756         duration,
1757         lazy.pprint`Expected "duration" to be a positive integer, got ${duration}`
1758       );
1759     }
1761     const originObject = await Origin.fromJSON(origin, options);
1763     if (originObject instanceof PointerOrigin) {
1764       throw new lazy.error.InvalidArgumentError(
1765         `"pointer" origin not supported for "wheel" input source.`
1766       );
1767     }
1769     lazy.assert.integer(
1770       x,
1771       lazy.pprint`Expected "x" to be an Integer, got ${x}`
1772     );
1773     lazy.assert.integer(
1774       y,
1775       lazy.pprint`Expected "y" to be an Integer, got ${y}`
1776     );
1777     lazy.assert.integer(
1778       deltaX,
1779       lazy.pprint`Expected "deltaX" to be an Integer, got ${deltaX}`
1780     );
1781     lazy.assert.integer(
1782       deltaY,
1783       lazy.pprint`Expected "deltaY" to be an Integer, got ${deltaY}`
1784     );
1786     return new this(id, {
1787       duration,
1788       origin: originObject,
1789       x,
1790       y,
1791       deltaX,
1792       deltaY,
1793     });
1794   }
1796   /**
1797    * Dispatch a wheel scroll action.
1798    *
1799    * @param {State} state
1800    *     The {@link State} of the action.
1801    * @param {InputSource} inputSource
1802    *     Current input device.
1803    * @param {number} tickDuration
1804    *     [unused] Length of the current tick, in ms.
1805    * @param {ActionsOptions} options
1806    *     Configuration of actions dispatch.
1807    *
1808    * @returns {Promise}
1809    *     Promise that is resolved once the action is complete.
1810    */
1811   async dispatch(state, inputSource, tickDuration, options) {
1812     const { assertInViewPort, context } = options;
1814     lazy.logger.trace(
1815       `Dispatch ${this.constructor.name} with id: ${this.id} deltaX: ${this.deltaX} deltaY: ${this.deltaY}`
1816     );
1818     const scrollCoordinates = await this.origin.getTargetCoordinates(
1819       inputSource,
1820       [this.x, this.y],
1821       options
1822     );
1824     await assertInViewPort(scrollCoordinates, context);
1826     const startX = 0;
1827     const startY = 0;
1828     // This is an action-local state that holds the amount of scroll completed
1829     const deltaPosition = [startX, startY];
1831     return moveOverTime(
1832       [[startX, startY]],
1833       [[this.deltaX, this.deltaY]],
1834       this.duration ?? tickDuration,
1835       async deltaTarget =>
1836         await this.performOneWheelScroll(
1837           state,
1838           scrollCoordinates,
1839           deltaPosition,
1840           deltaTarget,
1841           options
1842         )
1843     );
1844   }
1846   /**
1847    * Perform one part of a wheel scroll corresponding to a specific emitted event.
1848    *
1849    * @param {State} state
1850    *     The {@link State} of actions.
1851    * @param {Array<number>} scrollCoordinates
1852    *     The viewport coordinates [x, y] of the scroll action.
1853    * @param {Array<number>} deltaPosition
1854    *     [deltaX, deltaY] coordinates of the scroll before this event.
1855    * @param {Array<Array<number>>} deltaTargets
1856    *     Array of [deltaX, deltaY] coordinates to scroll to.
1857    * @param {ActionsOptions} options
1858    *     Configuration of actions dispatch.
1859    *
1860    * @returns {Promise}
1861    */
1862   async performOneWheelScroll(
1863     state,
1864     scrollCoordinates,
1865     deltaPosition,
1866     deltaTargets,
1867     options
1868   ) {
1869     const { context, dispatchEvent } = options;
1871     if (deltaTargets.length !== 1) {
1872       throw new Error("Can only scroll one wheel at a time");
1873     }
1874     if (deltaPosition[0] == this.deltaX && deltaPosition[1] == this.deltaY) {
1875       return;
1876     }
1878     const deltaTarget = deltaTargets[0];
1879     const deltaX = deltaTarget[0] - deltaPosition[0];
1880     const deltaY = deltaTarget[1] - deltaPosition[1];
1881     const eventData = new WheelEventData({
1882       deltaX,
1883       deltaY,
1884       deltaZ: 0,
1885     });
1886     eventData.update(state);
1888     await dispatchEvent("synthesizeWheelAtPoint", context, {
1889       x: scrollCoordinates[0],
1890       y: scrollCoordinates[1],
1891       eventData,
1892     });
1894     // Update the current scroll position for the caller
1895     deltaPosition[0] = deltaTarget[0];
1896     deltaPosition[1] = deltaTarget[1];
1897   }
1901  * Group of actions representing behavior of all touch pointers during
1902  * a single tick.
1904  * For touch pointers, we need to call into the platform once with all
1905  * the actions so that they are regarded as simultaneous. This means
1906  * we don't use the `dispatch()` method on the underlying actions, but
1907  * instead use one on this group object.
1908  */
1909 class TouchActionGroup {
1910   static type = null;
1912   /**
1913    * Creates a new {@link TouchActionGroup} instance.
1914    */
1915   constructor() {
1916     this.type = this.constructor.type;
1917     this.actions = new Map();
1918   }
1920   static forType(type) {
1921     const cls = touchActionGroupTypes.get(type);
1923     return new cls();
1924   }
1926   /**
1927    * Add action corresponding to a specific pointer to the group.
1928    *
1929    * @param {InputSource} inputSource
1930    *     Current input device.
1931    * @param {Action} action
1932    *     Action to add to the group.
1933    */
1934   addPointer(inputSource, action) {
1935     if (action.subtype !== this.type) {
1936       throw new Error(
1937         `Added action of unexpected type, got ${action.subtype}, expected ${this.type}`
1938       );
1939     }
1941     this.actions.set(action.id, [inputSource, action]);
1942   }
1944   /**
1945    * Dispatch the action group to the relevant window.
1946    *
1947    * This is overridden by subclasses to implement the type-specific
1948    * dispatch of the action.
1949    *
1950    * @returns {Promise}
1951    *     Promise that is resolved once the action is complete.
1952    */
1953   dispatch() {
1954     throw new Error(
1955       "TouchActionGroup subclass missing dispatch implementation"
1956     );
1957   }
1961  * Group of actions representing behavior of all touch pointers
1962  * depressed during a single tick.
1963  */
1964 class PointerDownTouchActionGroup extends TouchActionGroup {
1965   static type = "pointerDown";
1967   /**
1968    * Dispatch a pointerdown touch action.
1969    *
1970    * @param {State} state
1971    *     The {@link State} of the action.
1972    * @param {InputSource} inputSource
1973    *     Current input device.
1974    * @param {number} tickDuration
1975    *     [unused] Length of the current tick, in ms.
1976    * @param {ActionsOptions} options
1977    *     Configuration of actions dispatch.
1978    *
1979    * @returns {Promise}
1980    *     Promise that is resolved once the action is complete.
1981    */
1982   async dispatch(state, inputSource, tickDuration, options) {
1983     const { context, dispatchEvent } = options;
1985     lazy.logger.trace(
1986       `Dispatch ${this.constructor.name} with ${Array.from(
1987         this.actions.values()
1988       ).map(x => x[1].id)}`
1989     );
1991     if (inputSource !== null) {
1992       throw new Error(
1993         "Expected null inputSource for PointerDownTouchActionGroup.dispatch"
1994       );
1995     }
1997     // Only include pointers that are not already depressed
1998     const filteredActions = Array.from(this.actions.values()).filter(
1999       ([actionInputSource, action]) =>
2000         !actionInputSource.isPressed(action.button)
2001     );
2003     if (filteredActions.length) {
2004       const eventData = new MultiTouchEventData("touchstart");
2006       for (const [actionInputSource, action] of filteredActions) {
2007         eventData.addPointerEventData(actionInputSource, action);
2008         actionInputSource.press(action.button);
2009         eventData.update(state, actionInputSource);
2010       }
2012       // Touch start events must include all depressed touch pointers
2013       for (const [id, pointerInputSource] of state.inputSourcesByType(
2014         "pointer"
2015       )) {
2016         if (
2017           pointerInputSource.pointer.type === "touch" &&
2018           !this.actions.has(id) &&
2019           pointerInputSource.isPressed(0)
2020         ) {
2021           eventData.addPointerEventData(pointerInputSource, {});
2022           eventData.update(state, pointerInputSource);
2023         }
2024       }
2026       await dispatchEvent("synthesizeMultiTouch", context, { eventData });
2028       for (const [, action] of filteredActions) {
2029         // Append a copy of |action| with pointerUp subtype if event dispatched
2030         state.inputsToCancel.push(new PointerUpAction(action.id, action));
2031       }
2032     }
2033   }
2037  * Group of actions representing behavior of all touch pointers
2038  * released during a single tick.
2039  */
2040 class PointerUpTouchActionGroup extends TouchActionGroup {
2041   static type = "pointerUp";
2043   /**
2044    * Dispatch a pointerup touch action.
2045    *
2046    * @param {State} state
2047    *     The {@link State} of the action.
2048    * @param {InputSource} inputSource
2049    *     Current input device.
2050    * @param {number} tickDuration
2051    *     [unused] Length of the current tick, in ms.
2052    * @param {ActionsOptions} options
2053    *     Configuration of actions dispatch.
2054    *
2055    * @returns {Promise}
2056    *     Promise that is resolved once the action is complete.
2057    */
2058   async dispatch(state, inputSource, tickDuration, options) {
2059     const { context, dispatchEvent } = options;
2061     lazy.logger.trace(
2062       `Dispatch ${this.constructor.name} with ${Array.from(
2063         this.actions.values()
2064       ).map(x => x[1].id)}`
2065     );
2067     if (inputSource !== null) {
2068       throw new Error(
2069         "Expected null inputSource for PointerUpTouchActionGroup.dispatch"
2070       );
2071     }
2073     // Only include pointers that are not already depressed
2074     const filteredActions = Array.from(this.actions.values()).filter(
2075       ([actionInputSource, action]) =>
2076         actionInputSource.isPressed(action.button)
2077     );
2079     if (filteredActions.length) {
2080       const eventData = new MultiTouchEventData("touchend");
2081       for (const [actionInputSource, action] of filteredActions) {
2082         eventData.addPointerEventData(actionInputSource, action);
2083         actionInputSource.release(action.button);
2084         eventData.update(state, actionInputSource);
2085       }
2087       await dispatchEvent("synthesizeMultiTouch", context, { eventData });
2088     }
2089   }
2093  * Group of actions representing behavior of all touch pointers
2094  * moved during a single tick.
2095  */
2096 class PointerMoveTouchActionGroup extends TouchActionGroup {
2097   static type = "pointerMove";
2099   /**
2100    * Dispatch a pointermove touch action.
2101    *
2102    * @param {State} state
2103    *     The {@link State} of the action.
2104    * @param {InputSource} inputSource
2105    *     Current input device.
2106    * @param {number} tickDuration
2107    *     [unused] Length of the current tick, in ms.
2108    * @param {ActionsOptions} options
2109    *     Configuration of actions dispatch.
2110    *
2111    * @returns {Promise}
2112    *     Promise that is resolved once the action is complete.
2113    */
2114   async dispatch(state, inputSource, tickDuration, options) {
2115     const { assertInViewPort, context } = options;
2117     lazy.logger.trace(
2118       `Dispatch ${this.constructor.name} with ${Array.from(this.actions).map(
2119         x => x[1].id
2120       )}`
2121     );
2122     if (inputSource !== null) {
2123       throw new Error(
2124         "Expected null inputSource for PointerMoveTouchActionGroup.dispatch"
2125       );
2126     }
2128     let startCoords = [];
2129     let targetCoords = [];
2131     for (const [actionInputSource, action] of this.actions.values()) {
2132       const target = await action.origin.getTargetCoordinates(
2133         actionInputSource,
2134         [action.x, action.y],
2135         options
2136       );
2138       await assertInViewPort(target, context);
2140       startCoords.push([actionInputSource.x, actionInputSource.y]);
2141       targetCoords.push(target);
2142     }
2144     // Touch move events must include all depressed touch pointers, even if they are static
2145     // This can end up generating pointermove events even for static pointers, but Gecko
2146     // seems to generate a lot of pointermove events anyway, so this seems like the lesser
2147     // problem.
2148     // See https://bugzilla.mozilla.org/show_bug.cgi?id=1779206
2149     const staticTouchPointers = [];
2150     for (const [id, pointerInputSource] of state.inputSourcesByType(
2151       "pointer"
2152     )) {
2153       if (
2154         pointerInputSource.pointer.type === "touch" &&
2155         !this.actions.has(id) &&
2156         pointerInputSource.isPressed(0)
2157       ) {
2158         staticTouchPointers.push(pointerInputSource);
2159       }
2160     }
2162     return moveOverTime(
2163       startCoords,
2164       targetCoords,
2165       this.duration ?? tickDuration,
2166       async currentTargetCoords =>
2167         await this.performPointerMoveStep(
2168           state,
2169           staticTouchPointers,
2170           currentTargetCoords,
2171           options
2172         )
2173     );
2174   }
2176   /**
2177    * Perform one part of a pointer move corresponding to a specific emitted event.
2178    *
2179    * @param {State} state
2180    *     The {@link State} of actions.
2181    * @param {Array<PointerInputSource>} staticTouchPointers
2182    *     Array of PointerInputSource objects for pointers that aren't
2183    *     involved in the touch move.
2184    * @param {Array<Array<number>>} targetCoords
2185    *     Array of [x, y] arrays specifying the viewport coordinates to move to.
2186    * @param {ActionsOptions} options
2187    *     Configuration of actions dispatch.
2188    */
2189   async performPointerMoveStep(
2190     state,
2191     staticTouchPointers,
2192     targetCoords,
2193     options
2194   ) {
2195     const { context, dispatchEvent } = options;
2197     if (targetCoords.length !== this.actions.size) {
2198       throw new Error("Expected one target per pointer");
2199     }
2201     const perPointerData = Array.from(this.actions.values()).map(
2202       ([inputSource, action], i) => {
2203         const target = targetCoords[i];
2204         return [inputSource, action, target];
2205       }
2206     );
2207     const reachedTarget = perPointerData.every(
2208       ([inputSource, , target]) =>
2209         target[0] === inputSource.x && target[1] === inputSource.y
2210     );
2212     if (reachedTarget) {
2213       return;
2214     }
2216     const eventData = new MultiTouchEventData("touchmove");
2217     for (const [inputSource, action, target] of perPointerData) {
2218       inputSource.x = target[0];
2219       inputSource.y = target[1];
2220       eventData.addPointerEventData(inputSource, action);
2221       eventData.update(state, inputSource);
2222     }
2224     for (const inputSource of staticTouchPointers) {
2225       eventData.addPointerEventData(inputSource, {});
2226       eventData.update(state, inputSource);
2227     }
2229     await dispatchEvent("synthesizeMultiTouch", context, { eventData });
2230   }
2233 const touchActionGroupTypes = new Map();
2234 for (const cls of [
2235   PointerDownTouchActionGroup,
2236   PointerUpTouchActionGroup,
2237   PointerMoveTouchActionGroup,
2238 ]) {
2239   touchActionGroupTypes.set(cls.type, cls);
2243  * Split a transition from startCoord to targetCoord linearly over duration.
2245  * startCoords and targetCoords are lists of [x,y] positions in some space
2246  * (e.g. screen position or scroll delta). This function will linearly
2247  * interpolate intermediate positions, sending out roughly one event
2248  * per frame to simulate moving between startCoord and targetCoord in
2249  * a time of tickDuration milliseconds. The callback function is
2250  * responsible for actually emitting the event, given the current
2251  * position in the coordinate space.
2253  * @param {Array<Array>} startCoords
2254  *     Array of initial [x, y] coordinates for each input source involved
2255  *     in the move.
2256  * @param {Array<Array<number>>} targetCoords
2257  *     Array of target [x, y] coordinates for each input source involved
2258  *     in the move.
2259  * @param {number} duration
2260  *     Time in ms the move will take.
2261  * @param {Function} callback
2262  *     Function that actually performs the move. This takes a single parameter
2263  *     which is an array of [x, y] coordinates corresponding to the move
2264  *     targets.
2265  */
2266 async function moveOverTime(startCoords, targetCoords, duration, callback) {
2267   lazy.logger.trace(
2268     `moveOverTime start: ${startCoords} target: ${targetCoords} duration: ${duration}`
2269   );
2271   if (startCoords.length !== targetCoords.length) {
2272     throw new Error(
2273       "Expected equal number of start coordinates and target coordinates"
2274     );
2275   }
2277   if (
2278     !startCoords.every(item => item.length == 2) ||
2279     !targetCoords.every(item => item.length == 2)
2280   ) {
2281     throw new Error(
2282       "Expected start coordinates target coordinates to be Array of multiple [x,y] coordinates."
2283     );
2284   }
2286   if (duration === 0) {
2287     // transition to destination in one step
2288     await callback(targetCoords);
2289     return;
2290   }
2292   const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
2293   // interval between transitions in ms, based on common vsync
2294   const fps60 = 17;
2296   const distances = targetCoords.map((targetCoord, i) => {
2297     const startCoord = startCoords[i];
2298     return [targetCoord[0] - startCoord[0], targetCoord[1] - startCoord[1]];
2299   });
2300   const ONE_SHOT = Ci.nsITimer.TYPE_ONE_SHOT;
2301   const startTime = Date.now();
2302   const transitions = (async () => {
2303     // wait |fps60| ms before performing first incremental transition
2304     await new Promise(resolveTimer =>
2305       timer.initWithCallback(resolveTimer, fps60, ONE_SHOT)
2306     );
2308     let durationRatio = Math.floor(Date.now() - startTime) / duration;
2309     const epsilon = fps60 / duration / 10;
2310     while (1 - durationRatio > epsilon) {
2311       const intermediateTargets = startCoords.map((startCoord, i) => {
2312         let distance = distances[i];
2313         return [
2314           Math.floor(durationRatio * distance[0] + startCoord[0]),
2315           Math.floor(durationRatio * distance[1] + startCoord[1]),
2316         ];
2317       });
2319       await Promise.all([
2320         callback(intermediateTargets),
2322         // wait |fps60| ms before performing next transition
2323         new Promise(resolveTimer =>
2324           timer.initWithCallback(resolveTimer, fps60, ONE_SHOT)
2325         ),
2326       ]);
2328       durationRatio = Math.floor(Date.now() - startTime) / duration;
2329     }
2330   })();
2332   await transitions;
2334   // perform last transition after all incremental moves are resolved and
2335   // durationRatio is close enough to 1
2336   await callback(targetCoords);
2339 const actionTypes = new Map();
2340 for (const cls of [
2341   KeyDownAction,
2342   KeyUpAction,
2343   PauseAction,
2344   PointerDownAction,
2345   PointerUpAction,
2346   PointerMoveAction,
2347   WheelScrollAction,
2348 ]) {
2349   if (!actionTypes.has(cls.type)) {
2350     actionTypes.set(cls.type, new Map());
2351   }
2352   actionTypes.get(cls.type).set(cls.subtype, cls);
2356  * Implementation of the behavior of a specific type of pointer.
2358  * @abstract
2359  */
2360 class Pointer {
2361   /** Type of pointer */
2362   static type = null;
2364   /**
2365    * Creates a new {@link Pointer} instance.
2366    *
2367    * @param {number} id
2368    *     Numeric pointer id.
2369    */
2370   constructor(id) {
2371     this.id = id;
2372     this.type = this.constructor.type;
2373   }
2375   /**
2376    * Implementation of depressing the pointer.
2377    */
2378   pointerDown() {
2379     throw new Error(`Unimplemented pointerDown for pointerType ${this.type}`);
2380   }
2382   /**
2383    * Implementation of releasing the pointer.
2384    */
2385   pointerUp() {
2386     throw new Error(`Unimplemented pointerUp for pointerType ${this.type}`);
2387   }
2389   /**
2390    * Implementation of moving the pointer.
2391    */
2392   pointerMove() {
2393     throw new Error(`Unimplemented pointerMove for pointerType ${this.type}`);
2394   }
2396   /**
2397    * Unmarshals a JSON Object to a {@link Pointer}.
2398    *
2399    * @param {number} pointerId
2400    *     Numeric pointer id.
2401    * @param {string} pointerType
2402    *     Pointer type.
2403    *
2404    * @returns {Pointer}
2405    *     An instance of the Pointer class for {@link pointerType}.
2406    *
2407    * @throws {InvalidArgumentError}
2408    *     If {@link pointerType} is not a valid pointer type.
2409    */
2410   static fromJSON(pointerId, pointerType) {
2411     const cls = pointerTypes.get(pointerType);
2413     if (cls === undefined) {
2414       throw new lazy.error.InvalidArgumentError(
2415         'Expected "pointerType" type to be one of ' +
2416           lazy.pprint`${pointerTypes}, got ${pointerType}`
2417       );
2418     }
2420     return new cls(pointerId);
2421   }
2425  * Implementation of mouse pointer behavior.
2426  */
2427 class MousePointer extends Pointer {
2428   static type = "mouse";
2430   /**
2431    * Emits a pointer down event.
2432    *
2433    * @param {State} state
2434    *     The {@link State} of the action.
2435    * @param {InputSource} inputSource
2436    *     Current input device.
2437    * @param {PointerDownAction} action
2438    *     The pointer down action to perform.
2439    * @param {ActionsOptions} options
2440    *     Configuration of actions dispatch.
2441    *
2442    * @returns {Promise}
2443    *     Promise that resolves when the event has been dispatched.
2444    */
2445   async pointerDown(state, inputSource, action, options) {
2446     const { context, dispatchEvent } = options;
2448     const mouseEvent = new MouseEventData("mousedown", {
2449       button: action.button,
2450     });
2451     mouseEvent.update(state, inputSource);
2453     if (mouseEvent.ctrlKey) {
2454       if (lazy.AppInfo.isMac) {
2455         mouseEvent.button = 2;
2456         state.clickTracker.reset();
2457       }
2458     } else {
2459       mouseEvent.clickCount = state.clickTracker.count + 1;
2460     }
2462     await dispatchEvent("synthesizeMouseAtPoint", context, {
2463       x: inputSource.x,
2464       y: inputSource.y,
2465       eventData: mouseEvent,
2466     });
2468     if (
2469       lazy.event.MouseButton.isSecondary(mouseEvent.button) ||
2470       (mouseEvent.ctrlKey && lazy.AppInfo.isMac)
2471     ) {
2472       const contextMenuEvent = { ...mouseEvent, type: "contextmenu" };
2474       await dispatchEvent("synthesizeMouseAtPoint", context, {
2475         x: inputSource.x,
2476         y: inputSource.y,
2477         eventData: contextMenuEvent,
2478       });
2479     }
2480   }
2482   /**
2483    * Emits a pointer up event.
2484    *
2485    * @param {State} state
2486    *     The {@link State} of the action.
2487    * @param {InputSource} inputSource
2488    *     Current input device.
2489    * @param {PointerUpAction} action
2490    *     The pointer up action to perform.
2491    * @param {ActionsOptions} options
2492    *     Configuration of actions dispatch.
2493    *
2494    * @returns {Promise}
2495    *     Promise that resolves when the event has been dispatched.
2496    */
2497   async pointerUp(state, inputSource, action, options) {
2498     const { context, dispatchEvent } = options;
2500     const mouseEvent = new MouseEventData("mouseup", {
2501       button: action.button,
2502     });
2503     mouseEvent.update(state, inputSource);
2505     state.clickTracker.setClick(action.button);
2506     mouseEvent.clickCount = state.clickTracker.count;
2508     await dispatchEvent("synthesizeMouseAtPoint", context, {
2509       x: inputSource.x,
2510       y: inputSource.y,
2511       eventData: mouseEvent,
2512     });
2513   }
2515   /**
2516    * Emits a pointer down event.
2517    *
2518    * @param {State} state
2519    *     The {@link State} of the action.
2520    * @param {InputSource} inputSource
2521    *     Current input device.
2522    * @param {PointerMoveAction} action
2523    *     The pointer down action to perform.
2524    * @param {number} targetX
2525    *     Target x position to move the pointer to.
2526    * @param {number} targetY
2527    *     Target y position to move the pointer to.
2528    * @param {ActionsOptions} options
2529    *     Configuration of actions dispatch.
2530    *
2531    * @returns {Promise}
2532    *     Promise that resolves when the event has been dispatched.
2533    */
2534   async pointerMove(state, inputSource, action, targetX, targetY, options) {
2535     const { context, dispatchEvent } = options;
2537     const mouseEvent = new MouseEventData("mousemove");
2538     mouseEvent.update(state, inputSource);
2540     await dispatchEvent("synthesizeMouseAtPoint", context, {
2541       x: targetX,
2542       y: targetY,
2543       eventData: mouseEvent,
2544     });
2546     state.clickTracker.reset();
2547   }
2551  * The implementation here is empty because touch actions have to go via the
2552  * TouchActionGroup. So if we end up calling these methods that's a bug in
2553  * the code.
2554  */
2555 class TouchPointer extends Pointer {
2556   static type = "touch";
2560  * Placeholder for future pen type pointer support.
2561  */
2562 class PenPointer extends Pointer {
2563   static type = "pen";
2566 const pointerTypes = new Map();
2567 for (const cls of [MousePointer, TouchPointer, PenPointer]) {
2568   pointerTypes.set(cls.type, cls);
2572  * Represents a series of ticks, specifying which actions to perform at
2573  * each tick.
2574  */
2575 actions.Chain = class extends Array {
2576   toString() {
2577     return `[chain ${super.toString()}]`;
2578   }
2580   /**
2581    * Dispatch the action chain to the relevant window.
2582    *
2583    * @param {State} state
2584    *     The {@link State} of actions.
2585    * @param {ActionsOptions} options
2586    *     Configuration of actions dispatch.
2587    *
2588    * @returns {Promise}
2589    *     Promise that is resolved once the action chain is complete.
2590    */
2591   dispatch(state, options) {
2592     let i = 1;
2594     const chainEvents = (async () => {
2595       for (const tickActions of this) {
2596         lazy.logger.trace(`Dispatching tick ${i++}/${this.length}`);
2597         await tickActions.dispatch(state, options);
2598       }
2599     })();
2601     // Reset the current click tracker counter. We shouldn't be able to simulate
2602     // a double click with multiple action chains.
2603     state.clickTracker.reset();
2605     return chainEvents;
2606   }
2608   /* eslint-disable no-shadow */ // Shadowing is intentional for `actions`.
2609   /**
2610    *
2611    * Unmarshals a JSON Object to a {@link Chain}.
2612    *
2613    * @see https://w3c.github.io/webdriver/#dfn-extract-an-action-sequence
2614    *
2615    * @param {State} actionState
2616    *     The {@link State} of actions.
2617    * @param {Array<object>} actions
2618    *     Array of objects that each represent an action sequence.
2619    * @param {ActionsOptions} options
2620    *     Configuration for actions.
2621    *
2622    * @returns {Promise<Chain>}
2623    *     Promise resolving to an object that allows dispatching
2624    *     a chain of actions.
2625    *
2626    * @throws {InvalidArgumentError}
2627    *     If <code>actions</code> doesn't correspond to a valid action chain.
2628    */
2629   static async fromJSON(actionState, actions, options) {
2630     lazy.assert.array(
2631       actions,
2632       lazy.pprint`Expected "actions" to be an array, got ${actions}`
2633     );
2635     const actionsByTick = new this();
2636     for (const actionSequence of actions) {
2637       lazy.assert.object(
2638         actionSequence,
2639         'Expected "actions" item to be an object, ' +
2640           lazy.pprint`got ${actionSequence}`
2641       );
2643       const inputSourceActions = await Sequence.fromJSON(
2644         actionState,
2645         actionSequence,
2646         options
2647       );
2649       for (let i = 0; i < inputSourceActions.length; i++) {
2650         // new tick
2651         if (actionsByTick.length < i + 1) {
2652           actionsByTick.push(new TickActions());
2653         }
2654         actionsByTick[i].push(inputSourceActions[i]);
2655       }
2656     }
2658     return actionsByTick;
2659   }
2660   /* eslint-enable no-shadow */
2664  * Represents the action for each input device to perform in a single tick.
2665  */
2666 class TickActions extends Array {
2667   /**
2668    * Tick duration in milliseconds.
2669    *
2670    * @returns {number}
2671    *     Longest action duration in |tickActions| if any, or 0.
2672    */
2673   getDuration() {
2674     let max = 0;
2676     for (const action of this) {
2677       if (action.affectsWallClockTime && action.duration) {
2678         max = Math.max(action.duration, max);
2679       }
2680     }
2682     return max;
2683   }
2685   /**
2686    * Dispatch sequence of actions for this tick.
2687    *
2688    * This creates a Promise for one tick that resolves once the Promise
2689    * for each tick-action is resolved, which takes at least |tickDuration|
2690    * milliseconds.  The resolved set of events for each tick is followed by
2691    * firing of pending DOM events.
2692    *
2693    * Note that the tick-actions are dispatched in order, but they may have
2694    * different durations and therefore may not end in the same order.
2695    *
2696    * @param {State} state
2697    *     The {@link State} of actions.
2698    * @param {ActionsOptions} options
2699    *     Configuration of actions dispatch.
2700    *
2701    * @returns {Promise}
2702    *     Promise that resolves when tick is complete.
2703    */
2704   dispatch(state, options) {
2705     const tickDuration = this.getDuration();
2706     const tickActions = this.groupTickActions(state);
2707     const pendingEvents = tickActions.map(([inputSource, action]) =>
2708       action.dispatch(state, inputSource, tickDuration, options)
2709     );
2711     return Promise.all(pendingEvents);
2712   }
2714   /**
2715    * Group together actions from input sources that have to be
2716    * dispatched together.
2717    *
2718    * The actual transformation here is to group together touch pointer
2719    * actions into {@link TouchActionGroup} instances.
2720    *
2721    * @param {State} state
2722    *     The {@link State} of actions.
2723    *
2724    * @returns {Array<Array<InputSource?,Action|TouchActionGroup>>}
2725    *    Array of pairs. For ungrouped actions each element is
2726    *    [InputSource, Action] For touch actions there are multiple
2727    *    pointers handled at once, so the first item of the array is
2728    *    null, meaning the group has to perform its own handling of the
2729    *    relevant state, and the second element is a TouchActionGroup.
2730    */
2731   groupTickActions(state) {
2732     const touchActions = new Map();
2733     const groupedActions = [];
2735     for (const action of this) {
2736       const inputSource = state.getInputSource(action.id);
2737       if (action.type == "pointer" && inputSource.pointer.type === "touch") {
2738         lazy.logger.debug(
2739           `Grouping action ${action.type} ${action.id} ${action.subtype}`
2740         );
2741         let group = touchActions.get(action.subtype);
2742         if (group === undefined) {
2743           group = TouchActionGroup.forType(action.subtype);
2744           touchActions.set(action.subtype, group);
2745           groupedActions.push([null, group]);
2746         }
2747         group.addPointer(inputSource, action);
2748       } else {
2749         groupedActions.push([inputSource, action]);
2750       }
2751     }
2753     return groupedActions;
2754   }
2758  * Represents one input source action sequence; this is essentially an
2759  * |Array<Action>|.
2761  * This is a temporary object only used when constructing an {@link
2762  * action.Chain}.
2763  */
2764 class Sequence extends Array {
2765   toString() {
2766     return `[sequence ${super.toString()}]`;
2767   }
2769   /**
2770    * Unmarshals a JSON Object to a {@link Sequence}.
2771    *
2772    * @see https://w3c.github.io/webdriver/#dfn-process-an-input-source-action-sequence
2773    *
2774    * @param {State} actionState
2775    *     The {@link State} of actions.
2776    * @param {object} actionSequence
2777    *     Protocol representation of the actions for a specific input source.
2778    * @param {ActionsOptions} options
2779    *     Configuration for actions.
2780    *
2781    * @returns {Promise<Array<Array<InputSource, Action | TouchActionGroup>>>}
2782    *     Promise that resolves to an object that allows dispatching a
2783    *     sequence of actions.
2784    *
2785    * @throws {InvalidArgumentError}
2786    *     If the <code>actionSequence</code> doesn't correspond to a valid action sequence.
2787    */
2788   static async fromJSON(actionState, actionSequence, options) {
2789     // used here to validate 'type' in addition to InputSource type below
2790     const { actions: actionsFromSequence, id, type } = actionSequence;
2792     // type and id get validated in InputSource.fromJSON
2793     lazy.assert.array(
2794       actionsFromSequence,
2795       'Expected "actionSequence.actions" to be an array, ' +
2796         lazy.pprint`got ${actionSequence.actions}`
2797     );
2799     // This sets the input state in the global state map, if it's new.
2800     InputSource.fromJSON(actionState, actionSequence);
2802     const sequence = new this();
2803     for (const actionItem of actionsFromSequence) {
2804       sequence.push(await Action.fromJSON(type, id, actionItem, options));
2805     }
2807     return sequence;
2808   }
2812  * Representation of an input event.
2813  */
2814 class InputEventData {
2815   /**
2816    * Creates a new {@link InputEventData} instance.
2817    */
2818   constructor() {
2819     this.altKey = false;
2820     this.shiftKey = false;
2821     this.ctrlKey = false;
2822     this.metaKey = false;
2823   }
2825   /**
2826    * Update the input data based on global and input state
2827    */
2828   update() {}
2830   toString() {
2831     return `${this.constructor.name} ${JSON.stringify(this)}`;
2832   }
2836  * Representation of a key input event.
2837  */
2838 class KeyEventData extends InputEventData {
2839   /**
2840    * Creates a new {@link KeyEventData} instance.
2841    *
2842    * @param {string} rawKey
2843    *     The key value.
2844    */
2845   constructor(rawKey) {
2846     super();
2848     const { key, code, location, printable } = lazy.keyData.getData(rawKey);
2850     this.key = key;
2851     this.code = code;
2852     this.location = location;
2853     this.printable = printable;
2854     this.repeat = false;
2855     // keyCode will be computed by event.sendKeyDown
2856   }
2858   update(state, inputSource) {
2859     this.altKey = inputSource.alt;
2860     this.shiftKey = inputSource.shift;
2861     this.ctrlKey = inputSource.ctrl;
2862     this.metaKey = inputSource.meta;
2863   }
2867  * Representation of a pointer input event.
2868  */
2869 class PointerEventData extends InputEventData {
2870   /**
2871    * Creates a new {@link PointerEventData} instance.
2872    *
2873    * @param {string} type
2874    *     The event type.
2875    */
2876   constructor(type) {
2877     super();
2879     this.type = type;
2880     this.buttons = 0;
2881   }
2883   update(state, inputSource) {
2884     // set modifier properties based on whether any corresponding keys are
2885     // pressed on any key input source
2886     for (const [, otherInputSource] of state.inputSourcesByType("key")) {
2887       this.altKey = otherInputSource.alt || this.altKey;
2888       this.ctrlKey = otherInputSource.ctrl || this.ctrlKey;
2889       this.metaKey = otherInputSource.meta || this.metaKey;
2890       this.shiftKey = otherInputSource.shift || this.shiftKey;
2891     }
2892     const allButtons = Array.from(inputSource.pressed);
2893     this.buttons = allButtons.reduce(
2894       (a, i) => a + PointerEventData.getButtonFlag(i),
2895       0
2896     );
2897   }
2899   /**
2900    * Return a flag for buttons which indicates a button is pressed.
2901    *
2902    * @param {integer} button
2903    *     The mouse button number.
2904    */
2905   static getButtonFlag(button) {
2906     switch (button) {
2907       case 1:
2908         return 4;
2909       case 2:
2910         return 2;
2911       default:
2912         return Math.pow(2, button);
2913     }
2914   }
2918  * Representation of a mouse input event.
2919  */
2920 class MouseEventData extends PointerEventData {
2921   /**
2922    * Creates a new {@link MouseEventData} instance.
2923    *
2924    * @param {string} type
2925    *     The event type.
2926    * @param {object=} options
2927    * @param {number=} options.button
2928    *     The number of the mouse button. Defaults to 0.
2929    */
2930   constructor(type, options = {}) {
2931     super(type);
2933     const { button = 0 } = options;
2935     this.button = button;
2936     this.buttons = 0;
2938     // Some WPTs try to synthesize DnD only with mouse events.  However,
2939     // Gecko waits DnD events directly and non-WPT-tests use Gecko specific
2940     // test API to synthesize DnD.  Therefore, we want new path only for
2941     // synthesized events coming from the webdriver.
2942     this.allowToHandleDragDrop = true;
2943   }
2945   update(state, inputSource) {
2946     super.update(state, inputSource);
2948     this.id = inputSource.pointer.id;
2949   }
2953  * Representation of a wheel scroll event.
2954  */
2955 class WheelEventData extends InputEventData {
2956   /**
2957    * Creates a new {@link WheelEventData} instance.
2958    *
2959    * @param {object} options
2960    * @param {number} options.deltaX
2961    *     Scroll delta X.
2962    * @param {number} options.deltaY
2963    *     Scroll delta Y.
2964    * @param {number} options.deltaZ
2965    *     Scroll delta Z (current always 0).
2966    * @param {number=} options.deltaMode
2967    *     Scroll delta mode (current always 0).
2968    */
2969   constructor(options) {
2970     super();
2972     const { deltaX, deltaY, deltaZ, deltaMode = 0 } = options;
2974     this.deltaX = deltaX;
2975     this.deltaY = deltaY;
2976     this.deltaZ = deltaZ;
2977     this.deltaMode = deltaMode;
2979     this.altKey = false;
2980     this.ctrlKey = false;
2981     this.metaKey = false;
2982     this.shiftKey = false;
2983   }
2985   update(state) {
2986     // set modifier properties based on whether any corresponding keys are
2987     // pressed on any key input source
2988     for (const [, otherInputSource] of state.inputSourcesByType("key")) {
2989       this.altKey = otherInputSource.alt || this.altKey;
2990       this.ctrlKey = otherInputSource.ctrl || this.ctrlKey;
2991       this.metaKey = otherInputSource.meta || this.metaKey;
2992       this.shiftKey = otherInputSource.shift || this.shiftKey;
2993     }
2994   }
2998  * Representation of a multi touch event.
2999  */
3000 class MultiTouchEventData extends PointerEventData {
3001   #setGlobalState;
3003   /**
3004    * Creates a new {@link MultiTouchEventData} instance.
3005    *
3006    * @param {string} type
3007    *     The event type.
3008    */
3009   constructor(type) {
3010     super(type);
3012     this.id = [];
3013     this.x = [];
3014     this.y = [];
3015     this.rx = [];
3016     this.ry = [];
3017     this.angle = [];
3018     this.force = [];
3019     this.tiltx = [];
3020     this.tilty = [];
3021     this.twist = [];
3022     this.#setGlobalState = false;
3023   }
3025   /**
3026    * Add the data from one pointer to the event.
3027    *
3028    * @param {InputSource} inputSource
3029    *     The state of the pointer.
3030    * @param {PointerAction} action
3031    *     Action for the pointer.
3032    */
3033   addPointerEventData(inputSource, action) {
3034     this.x.push(inputSource.x);
3035     this.y.push(inputSource.y);
3036     this.id.push(inputSource.pointer.id);
3037     this.rx.push(action.width || 1);
3038     this.ry.push(action.height || 1);
3039     this.angle.push(0);
3040     this.force.push(action.pressure || (this.type === "touchend" ? 0 : 1));
3041     this.tiltx.push(action.tiltX || 0);
3042     this.tilty.push(action.tiltY || 0);
3043     this.twist.push(action.twist || 0);
3044   }
3046   update(state, inputSource) {
3047     // We call update once per input source, but only want to update global state once.
3048     // Instead of introducing a new lifecycle method, or changing the API to allow multiple
3049     // input sources in a single call, use a small bit of state to avoid repeatedly setting
3050     // global state.
3051     if (!this.#setGlobalState) {
3052       // set modifier properties based on whether any corresponding keys are
3053       // pressed on any key input source
3054       for (const [, otherInputSource] of state.inputSourcesByType("key")) {
3055         this.altKey = otherInputSource.alt || this.altKey;
3056         this.ctrlKey = otherInputSource.ctrl || this.ctrlKey;
3057         this.metaKey = otherInputSource.meta || this.metaKey;
3058         this.shiftKey = otherInputSource.shift || this.shiftKey;
3059       }
3060       this.#setGlobalState = true;
3061     }
3063     // Note that we currently emit Touch events that don't have this property
3064     // but pointer events should have a `buttons` property, so we'll compute it
3065     // anyway.
3066     const allButtons = Array.from(inputSource.pressed);
3067     this.buttons =
3068       this.buttons |
3069       allButtons.reduce((a, i) => a + PointerEventData.getButtonFlag(i), 0);
3070   }
3073 // Helpers
3076  * Assert that target is in the viewport of win.
3078  * @param {Array<number>} target
3079  *     Coordinates [x, y] of the target relative to the viewport.
3080  * @param {WindowProxy} win
3081  *     The target window.
3083  * @throws {MoveTargetOutOfBoundsError}
3084  *     If target is outside the viewport.
3085  */
3086 export function assertTargetInViewPort(target, win) {
3087   const [x, y] = target;
3089   lazy.assert.number(
3090     x,
3091     lazy.pprint`Expected "x" to be finite number, got ${x}`
3092   );
3093   lazy.assert.number(
3094     y,
3095     lazy.pprint`Expected "y" to be finite number, got ${y}`
3096   );
3098   // Viewport includes scrollbars if rendered.
3099   if (x < 0 || y < 0 || x > win.innerWidth || y > win.innerHeight) {
3100     throw new lazy.error.MoveTargetOutOfBoundsError(
3101       `Move target (${x}, ${y}) is out of bounds of viewport dimensions ` +
3102         `(${win.innerWidth}, ${win.innerHeight})`
3103     );
3104   }