tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Actions.sys.mjs (86768B)


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