1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* eslint no-dupe-keys:off */
6 /* eslint-disable no-restricted-globals */
10 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
12 const { assert } = ChromeUtils.import("chrome://marionette/content/assert.js");
13 const { element } = ChromeUtils.import(
14 "chrome://marionette/content/element.js"
18 MoveTargetOutOfBoundsError,
19 UnsupportedOperationError,
20 } = ChromeUtils.import("chrome://marionette/content/error.js");
21 const { event } = ChromeUtils.import("chrome://marionette/content/event.js");
22 const { pprint } = ChromeUtils.import("chrome://marionette/content/format.js");
23 const { Sleep } = ChromeUtils.import("chrome://marionette/content/sync.js");
25 this.EXPORTED_SYMBOLS = ["action"];
27 // TODO? With ES 2016 and Symbol you can make a safer approximation
28 // to an enum e.g. https://gist.github.com/xmlking/e86e4f15ec32b12c4689
30 * Implements WebDriver Actions API: a low-level interface for providing
31 * virtualised device input to the web browser.
39 PointerDown: "pointerDown",
40 PointerUp: "pointerUp",
41 PointerMove: "pointerMove",
42 PointerCancel: "pointerCancel",
46 none: new Set([action.Pause]),
47 key: new Set([action.Pause, action.KeyDown, action.KeyUp]),
57 /** Map from normalized key value to UI Events modifier key name */
58 const MODIFIER_NAME_LOOKUP = {
65 /** Map from raw key (codepoint) to normalized key value */
66 const NORMALIZED_KEY_LOOKUP = {
67 "\uE000": "Unidentified",
70 "\uE003": "Backspace",
85 "\uE012": "ArrowLeft",
87 "\uE014": "ArrowRight",
88 "\uE015": "ArrowDown",
122 "\uE040": "ZenkakuHankaku",
128 "\uE055": "PageDown",
131 "\uE058": "ArrowLeft",
133 "\uE05A": "ArrowRight",
134 "\uE05B": "ArrowDown",
139 /** Map from raw key (codepoint) to key location */
140 const KEY_LOCATION_LOOKUP = {
178 const KEY_CODE_LOOKUP = {
180 "\uE052": "AltRight",
181 "\uE015": "ArrowDown",
182 "\uE012": "ArrowLeft",
183 "\uE014": "ArrowRight",
189 "\uE003": "Backspace",
196 "\uE009": "ControlLeft",
197 "\uE051": "ControlRight",
239 "<": "IntlBackslash",
240 ">": "IntlBackslash",
314 "\uE024": "NumpadAdd",
315 "\uE026": "NumpadComma",
316 "\uE028": "NumpadDecimal",
317 "\uE05D": "NumpadDecimal",
318 "\uE029": "NumpadDivide",
319 "\uE007": "NumpadEnter",
320 "\uE024": "NumpadMultiply",
321 "\uE026": "NumpadSubtract",
324 "\uE01E": "PageDown",
332 "\uE008": "ShiftLeft",
333 "\uE050": "ShiftRight",
341 /** Represents possible values for a pointer-move origin. */
342 action.PointerOrigin = {
343 Viewport: "viewport",
347 /** Flag for WebDriver spec conforming pointer origin calculation. */
348 action.specCompatPointerOrigin = true;
351 * Look up a PointerOrigin.
353 * @param {(string|Element)=} obj
354 * Origin for a <code>pointerMove</code> action. Must be one of
355 * "viewport" (default), "pointer", or a DOM element.
357 * @return {action.PointerOrigin}
360 * @throws {InvalidArgumentError}
361 * If <var>obj</var> is not a valid origin.
363 action.PointerOrigin.get = function(obj) {
365 if (typeof obj == "undefined") {
366 origin = this.Viewport;
367 } else if (typeof obj == "string") {
368 let name = capitalize(obj);
369 assert.in(name, this, pprint`Unknown pointer-move origin: ${obj}`);
371 } else if (!element.isDOMElement(obj)) {
372 throw new InvalidArgumentError(
373 "Expected 'origin' to be undefined, " +
374 '"viewport", "pointer", ' +
375 pprint`or an element, got: ${obj}`
381 /** Represents possible subtypes for a pointer input source. */
382 action.PointerType = {
384 // TODO For now, only mouse is supported
390 * Look up a PointerType.
392 * @param {string} str
393 * Name of pointer type.
396 * A pointer type for processing pointer parameters.
398 * @throws {InvalidArgumentError}
399 * If <code>str</code> is not a valid pointer type.
401 action.PointerType.get = function(str) {
402 let name = capitalize(str);
403 assert.in(name, this, pprint`Unknown pointerType: ${str}`);
408 * Input state associated with current session. This is a map between
409 * input ID and the device state for that input source, with one entry
410 * for each active input source.
412 * Initialized in listener.js.
414 action.inputStateMap = undefined;
417 * List of {@link action.Action} associated with current session. Used to
418 * manage dispatching events when resetting the state of the input sources.
419 * Reset operations are assumed to be idempotent.
421 * Initialized in listener.js
423 action.inputsToCancel = undefined;
426 * Represents device state for an input source.
430 this.type = this.constructor.name.toLowerCase();
434 * Check equality of this InputState object with another.
436 * @param {InputState} other
437 * Object representing an input state.
440 * True if <code>this</code> has the same <code>type</code>
441 * as <code>other</code>.
444 if (typeof other == "undefined") {
447 return this.type === other.type;
451 return `[object ${this.constructor.name}InputState]`;
455 * @param {Object.<string, ?>} obj
456 * Object with property <code>type</code> and optionally
457 * <code>parameters</code> or <code>pointerType</code>,
458 * representing an action sequence or an action item.
460 * @return {action.InputState}
461 * An {@link InputState} object for the type of the
462 * {@link actionSequence}.
464 * @throws {InvalidArgumentError}
465 * If {@link actionSequence.type} is not valid.
467 static fromJSON(obj) {
469 assert.in(type, ACTIONS, pprint`Unknown action type: ${type}`);
470 let name = type == "none" ? "Null" : capitalize(type);
471 if (name == "Pointer") {
474 (!obj.parameters || !obj.parameters.pointerType)
476 throw new InvalidArgumentError(
477 pprint`Expected obj to have pointerType, got ${obj}`
480 let pointerType = obj.pointerType || obj.parameters.pointerType;
481 return new action.InputState[name](pointerType);
483 return new action.InputState[name]();
487 /** Possible kinds of |InputState| for supported input sources. */
488 action.InputState = {};
491 * Input state associated with a keyboard-type device.
493 action.InputState.Key = class Key extends InputState {
496 this.pressed = new Set();
504 * Update modifier state according to |key|.
506 * @param {string} key
507 * Normalized key value of a modifier key.
508 * @param {boolean} value
509 * Value to set the modifier attribute to.
511 * @throws {InvalidArgumentError}
512 * If |key| is not a modifier.
514 setModState(key, value) {
515 if (key in MODIFIER_NAME_LOOKUP) {
516 this[MODIFIER_NAME_LOOKUP[key]] = value;
518 throw new InvalidArgumentError(
519 "Expected 'key' to be one of " +
520 Object.keys(MODIFIER_NAME_LOOKUP) +
527 * Check whether |key| is pressed.
529 * @param {string} key
530 * Normalized key value.
533 * True if |key| is in set of pressed keys.
536 return this.pressed.has(key);
540 * Add |key| to the set of pressed keys.
542 * @param {string} key
543 * Normalized key value.
546 * True if |key| is in list of pressed keys.
549 return this.pressed.add(key);
553 * Remove |key| from the set of pressed keys.
555 * @param {string} key
556 * Normalized key value.
559 * True if |key| was present before removal, false otherwise.
562 return this.pressed.delete(key);
567 * Input state not associated with a specific physical device.
569 action.InputState.Null = class Null extends InputState {
577 * Input state associated with a pointer-type input device.
579 * @param {string} subtype
580 * Kind of pointing device: mouse, pen, touch.
582 * @throws {InvalidArgumentError}
583 * If subtype is undefined or an invalid pointer type.
585 action.InputState.Pointer = class Pointer extends InputState {
586 constructor(subtype) {
588 this.pressed = new Set();
591 pprint`Expected subtype to be defined, got ${subtype}`
593 this.subtype = action.PointerType.get(subtype);
599 * Check whether |button| is pressed.
601 * @param {number} button
602 * Positive integer that refers to a mouse button.
605 * True if |button| is in set of pressed buttons.
608 assert.positiveInteger(button);
609 return this.pressed.has(button);
613 * Add |button| to the set of pressed keys.
615 * @param {number} button
616 * Positive integer that refers to a mouse button.
619 * Set of pressed buttons.
622 assert.positiveInteger(button);
623 return this.pressed.add(button);
627 * Remove |button| from the set of pressed buttons.
629 * @param {number} button
630 * A positive integer that refers to a mouse button.
633 * True if |button| was present before removals, false otherwise.
636 assert.positiveInteger(button);
637 return this.pressed.delete(button);
642 * Repesents an action for dispatch. Used in |action.Chain| and
647 * @param {string} type
648 * Action type: none, key, pointer.
649 * @param {string} subtype
650 * Action subtype: {@link action.Pause}, {@link action.KeyUp},
651 * {@link action.KeyDown}, {@link action.PointerUp},
652 * {@link action.PointerDown}, {@link action.PointerMove}, or
653 * {@link action.PointerCancel}.
655 * @throws {InvalidArgumentError}
656 * If any parameters are undefined.
658 action.Action = class {
659 constructor(id, type, subtype) {
660 if ([id, type, subtype].includes(undefined)) {
661 throw new InvalidArgumentError("Missing id, type or subtype");
663 for (let attr of [id, type, subtype]) {
664 assert.string(attr, pprint`Expected string, got ${attr}`);
668 this.subtype = subtype;
672 return `[action ${this.type}]`;
676 * @param {action.Sequence} actionSequence
677 * Object representing sequence of actions from one input source.
678 * @param {action.Action} actionItem
679 * Object representing a single action from |actionSequence|.
681 * @return {action.Action}
682 * An action that can be dispatched; corresponds to |actionItem|.
684 * @throws {InvalidArgumentError}
685 * If any <code>actionSequence</code> or <code>actionItem</code>
686 * attributes are invalid.
687 * @throws {UnsupportedOperationError}
688 * If <code>actionItem.type</code> is {@link action.PointerCancel}.
690 static fromJSON(actionSequence, actionItem) {
691 let type = actionSequence.type;
692 let id = actionSequence.id;
693 let subtypes = ACTIONS[type];
695 throw new InvalidArgumentError("Unknown type: " + type);
697 let subtype = actionItem.type;
698 if (!subtypes.has(subtype)) {
699 throw new InvalidArgumentError(
700 `Unknown subtype for ${type} action: ${subtype}`
704 let item = new action.Action(id, type, subtype);
705 if (type === "pointer") {
706 action.processPointerAction(
708 action.PointerParameters.fromJSON(actionSequence.parameters),
713 switch (item.subtype) {
716 let key = actionItem.value;
717 // TODO countGraphemes
718 // TODO key.value could be a single code point like "\uE012"
719 // (see rawKey) or "grapheme cluster"
722 "Expected 'value' to be a string that represents single code point " +
723 pprint`or grapheme cluster, got ${key}`
728 case action.PointerDown:
729 case action.PointerUp:
730 assert.positiveInteger(
732 pprint`Expected 'button' (${actionItem.button}) to be >= 0`
734 item.button = actionItem.button;
737 case action.PointerMove:
738 item.duration = actionItem.duration;
739 if (typeof item.duration != "undefined") {
740 assert.positiveInteger(
742 pprint`Expected 'duration' (${item.duration}) to be >= 0`
745 item.origin = action.PointerOrigin.get(actionItem.origin);
746 item.x = actionItem.x;
747 if (typeof item.x != "undefined") {
750 pprint`Expected 'x' (${item.x}) to be an Integer`
753 item.y = actionItem.y;
754 if (typeof item.y != "undefined") {
757 pprint`Expected 'y' (${item.y}) to be an Integer`
762 case action.PointerCancel:
763 throw new UnsupportedOperationError();
766 item.duration = actionItem.duration;
767 if (typeof item.duration != "undefined") {
768 // eslint-disable-next-line
769 assert.positiveInteger(item.duration,
770 pprint`Expected 'duration' (${item.duration}) to be >= 0`
781 * Represents a series of ticks, specifying which actions to perform at
784 action.Chain = class extends Array {
786 return `[chain ${super.toString()}]`;
790 * @param {Array.<?>} actions
791 * Array of objects that each represent an action sequence.
793 * @return {action.Chain}
794 * Transpose of <var>actions</var> such that actions to be performed
795 * in a single tick are grouped together.
797 * @throws {InvalidArgumentError}
798 * If <var>actions</var> is not an Array.
800 static fromJSON(actions) {
803 pprint`Expected 'actions' to be an array, got ${actions}`
806 let actionsByTick = new action.Chain();
807 for (let actionSequence of actions) {
808 // TODO(maja_zf): Check that each actionSequence in actions refers
809 // to a different input ID.
810 let inputSourceActions = action.Sequence.fromJSON(actionSequence);
811 for (let i = 0; i < inputSourceActions.length; i++) {
813 if (actionsByTick.length < i + 1) {
814 actionsByTick.push([]);
816 actionsByTick[i].push(inputSourceActions[i]);
819 return actionsByTick;
824 * Represents one input source action sequence; this is essentially an
825 * |Array.<action.Action>|.
827 action.Sequence = class extends Array {
829 return `[sequence ${super.toString()}]`;
833 * @param {Object.<string, ?>} actionSequence
834 * Object that represents a sequence action items for one input source.
836 * @return {action.Sequence}
837 * Sequence of actions that can be dispatched.
839 * @throws {InvalidArgumentError}
840 * If <code>actionSequence.id</code> is not a
841 * string or it's aleady mapped to an |action.InputState}
842 * incompatible with <code>actionSequence.type</code>, or if
843 * <code>actionSequence.actions</code> is not an <code>Array</code>.
845 static fromJSON(actionSequence) {
846 // used here to validate 'type' in addition to InputState type below
847 let inputSourceState = InputState.fromJSON(actionSequence);
848 let id = actionSequence.id;
849 assert.defined(id, "Expected 'id' to be defined");
850 assert.string(id, pprint`Expected 'id' to be a string, got ${id}`);
851 let actionItems = actionSequence.actions;
854 "Expected 'actionSequence.actions' to be an array, " +
855 pprint`got ${actionSequence.actions}`
858 if (!action.inputStateMap.has(id)) {
859 action.inputStateMap.set(id, inputSourceState);
860 } else if (!action.inputStateMap.get(id).is(inputSourceState)) {
861 throw new InvalidArgumentError(
862 `Expected ${id} to be mapped to ${inputSourceState}, ` +
863 `got ${action.inputStateMap.get(id)}`
867 let actions = new action.Sequence();
868 for (let actionItem of actionItems) {
869 actions.push(action.Action.fromJSON(actionSequence, actionItem));
877 * Represents parameters in an action for a pointer input source.
879 * @param {string=} pointerType
880 * Type of pointing device. If the parameter is undefined, "mouse"
883 action.PointerParameters = class {
884 constructor(pointerType = "mouse") {
885 this.pointerType = action.PointerType.get(pointerType);
889 return `[pointerParameters ${this.pointerType}]`;
893 * @param {Object.<string, ?>} parametersData
894 * Object that represents pointer parameters.
896 * @return {action.PointerParameters}
897 * Validated pointer paramters.
899 static fromJSON(parametersData) {
900 if (typeof parametersData == "undefined") {
901 return new action.PointerParameters();
903 return new action.PointerParameters(parametersData.pointerType);
908 * Adds <var>pointerType</var> attribute to Action <var>act</var>.
910 * Helper function for {@link action.Action.fromJSON}.
914 * @param {action.PointerParams} pointerParams
915 * Input source pointer parameters.
916 * @param {action.Action} act
917 * Action to be updated.
919 * @throws {InvalidArgumentError}
920 * If <var>id</var> is already mapped to an
921 * {@link action.InputState} that is not compatible with
922 * <code>act.type</code> or <code>pointerParams.pointerType</code>.
924 action.processPointerAction = function(id, pointerParams, act) {
926 action.inputStateMap.has(id) &&
927 action.inputStateMap.get(id).type !== act.type
929 throw new InvalidArgumentError(
930 `Expected 'id' ${id} to be mapped to InputState whose type is ` +
931 action.inputStateMap.get(id).type +
932 pprint` , got ${act.type}`
935 let pointerType = pointerParams.pointerType;
937 action.inputStateMap.has(id) &&
938 action.inputStateMap.get(id).subtype !== pointerType
940 throw new InvalidArgumentError(
941 `Expected 'id' ${id} to be mapped to InputState whose subtype is ` +
942 action.inputStateMap.get(id).subtype +
943 pprint` , got ${pointerType}`
946 act.pointerType = pointerParams.pointerType;
949 /** Collect properties associated with KeyboardEvent */
951 constructor(rawKey) {
952 this.key = NORMALIZED_KEY_LOOKUP[rawKey] || rawKey;
953 this.code = KEY_CODE_LOOKUP[rawKey];
954 this.location = KEY_LOCATION_LOOKUP[rawKey] || 0;
956 this.shiftKey = false;
957 this.ctrlKey = false;
958 this.metaKey = false;
960 this.isComposing = false;
961 // keyCode will be computed by event.sendKeyDown
965 this.altKey = inputState.alt;
966 this.shiftKey = inputState.shift;
967 this.ctrlKey = inputState.ctrl;
968 this.metaKey = inputState.meta;
972 /** Collect properties associated with MouseEvent */
973 action.Mouse = class {
974 constructor(type, button = 0) {
976 assert.positiveInteger(button);
977 this.button = button;
980 this.shiftKey = false;
981 this.metaKey = false;
982 this.ctrlKey = false;
983 // set modifier properties based on whether any corresponding keys are
984 // pressed on any key input source
985 for (let inputState of action.inputStateMap.values()) {
986 if (inputState.type == "key") {
987 this.altKey = inputState.alt || this.altKey;
988 this.ctrlKey = inputState.ctrl || this.ctrlKey;
989 this.metaKey = inputState.meta || this.metaKey;
990 this.shiftKey = inputState.shift || this.shiftKey;
996 let allButtons = Array.from(inputState.pressed);
997 this.buttons = allButtons.reduce((a, i) => a + Math.pow(2, i), 0);
1002 * Dispatch a chain of actions over |chain.length| ticks.
1004 * This is done by creating a Promise for each tick that resolves once
1005 * all the Promises for individual tick-actions are resolved. The next
1006 * tick's actions are not dispatched until the Promise for the current
1009 * @param {action.Chain} chain
1010 * Actions grouped by tick; each element in |chain| is a sequence of
1011 * actions for one tick.
1012 * @param {WindowProxy} win
1013 * Current window global.
1014 * @param {boolean=} [specCompatPointerOrigin=true] specCompatPointerOrigin
1015 * Flag to turn off the WebDriver spec conforming pointer origin
1016 * calculation. It has to be kept until all Selenium bindings can
1017 * successfully handle the WebDriver spec conforming Pointer Origin
1018 * calculation. See https://bugzilla.mozilla.org/show_bug.cgi?id=1429338.
1021 * Promise for dispatching all actions in |chain|.
1023 action.dispatch = function(chain, win, specCompatPointerOrigin = true) {
1024 action.specCompatPointerOrigin = specCompatPointerOrigin;
1026 let chainEvents = (async () => {
1027 for (let tickActions of chain) {
1028 await action.dispatchTickActions(
1030 action.computeTickDuration(tickActions),
1039 * Dispatch sequence of actions for one tick.
1041 * This creates a Promise for one tick that resolves once the Promise
1042 * for each tick-action is resolved, which takes at least |tickDuration|
1043 * milliseconds. The resolved set of events for each tick is followed by
1044 * firing of pending DOM events.
1046 * Note that the tick-actions are dispatched in order, but they may have
1047 * different durations and therefore may not end in the same order.
1049 * @param {Array.<action.Action>} tickActions
1050 * List of actions for one tick.
1051 * @param {number} tickDuration
1052 * Duration in milliseconds of this tick.
1053 * @param {WindowProxy} win
1054 * Current window global.
1057 * Promise for dispatching all tick-actions and pending DOM events.
1059 action.dispatchTickActions = function(tickActions, tickDuration, win) {
1060 let pendingEvents = tickActions.map(toEvents(tickDuration, win));
1061 return Promise.all(pendingEvents);
1065 * Compute tick duration in milliseconds for a collection of actions.
1067 * @param {Array.<action.Action>} tickActions
1068 * List of actions for one tick.
1071 * Longest action duration in |tickActions| if any, or 0.
1073 action.computeTickDuration = function(tickActions) {
1075 for (let a of tickActions) {
1076 let affectsWallClockTime =
1077 a.subtype == action.Pause ||
1078 (a.type == "pointer" && a.subtype == action.PointerMove);
1079 if (affectsWallClockTime && a.duration) {
1080 max = Math.max(a.duration, max);
1087 * Compute viewport coordinates of pointer target based on given origin.
1089 * @param {action.Action} a
1090 * Action that specifies pointer origin and x and y coordinates of target.
1091 * @param {action.InputState} inputState
1092 * Input state that specifies current x and y coordinates of pointer.
1093 * @param {Map.<string, number>=} center
1094 * Object representing x and y coordinates of an element center-point.
1095 * This is only used if |a.origin| is a web element reference.
1097 * @return {Map.<string, number>}
1098 * x and y coordinates of pointer destination.
1100 action.computePointerDestination = function(a, inputState, center = undefined) {
1103 case action.PointerOrigin.Viewport:
1105 case action.PointerOrigin.Pointer:
1110 // origin represents web element
1111 assert.defined(center);
1112 assert.in("x", center);
1113 assert.in("y", center);
1121 * Create a closure to use as a map from action definitions to Promise events.
1123 * @param {number} tickDuration
1124 * Duration in milliseconds of this tick.
1125 * @param {WindowProxy} win
1126 * Current window global.
1128 * @return {function(action.Action): Promise}
1129 * Function that takes an action and returns a Promise for dispatching
1130 * the event that corresponds to that action.
1132 function toEvents(tickDuration, win) {
1134 let inputState = action.inputStateMap.get(a.id);
1136 switch (a.subtype) {
1138 return dispatchKeyUp(a, inputState, win);
1140 case action.KeyDown:
1141 return dispatchKeyDown(a, inputState, win);
1143 case action.PointerDown:
1144 return dispatchPointerDown(a, inputState, win);
1146 case action.PointerUp:
1147 return dispatchPointerUp(a, inputState, win);
1149 case action.PointerMove:
1150 return dispatchPointerMove(a, inputState, tickDuration, win);
1152 case action.PointerCancel:
1153 throw new UnsupportedOperationError();
1156 return dispatchPause(a, tickDuration);
1164 * Dispatch a keyDown action equivalent to pressing a key on a keyboard.
1166 * @param {action.Action} a
1167 * Action to dispatch.
1168 * @param {action.InputState} inputState
1169 * Input state for this action's input source.
1170 * @param {WindowProxy} win
1171 * Current window global.
1174 * Promise to dispatch at least a keydown event, and keypress if
1177 function dispatchKeyDown(a, inputState, win) {
1178 return new Promise(resolve => {
1179 let keyEvent = new action.Key(a.value);
1180 keyEvent.repeat = inputState.isPressed(keyEvent.key);
1181 inputState.press(keyEvent.key);
1182 if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
1183 inputState.setModState(keyEvent.key, true);
1186 // Append a copy of |a| with keyUp subtype
1187 action.inputsToCancel.push(Object.assign({}, a, { subtype: action.KeyUp }));
1188 keyEvent.update(inputState);
1189 event.sendKeyDown(a.value, keyEvent, win);
1196 * Dispatch a keyUp action equivalent to releasing a key on a keyboard.
1198 * @param {action.Action} a
1199 * Action to dispatch.
1200 * @param {action.InputState} inputState
1201 * Input state for this action's input source.
1202 * @param {WindowProxy} win
1203 * Current window global.
1206 * Promise to dispatch a keyup event.
1208 function dispatchKeyUp(a, inputState, win) {
1209 return new Promise(resolve => {
1210 let keyEvent = new action.Key(a.value);
1212 if (!inputState.isPressed(keyEvent.key)) {
1217 if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
1218 inputState.setModState(keyEvent.key, false);
1220 inputState.release(keyEvent.key);
1221 keyEvent.update(inputState);
1223 event.sendKeyUp(a.value, keyEvent, win);
1229 * Dispatch a pointerDown action equivalent to pressing a pointer-device
1232 * @param {action.Action} a
1233 * Action to dispatch.
1234 * @param {action.InputState} inputState
1235 * Input state for this action's input source.
1236 * @param {WindowProxy} win
1237 * Current window global.
1240 * Promise to dispatch at least a pointerdown event.
1242 function dispatchPointerDown(a, inputState, win) {
1243 return new Promise(resolve => {
1244 if (inputState.isPressed(a.button)) {
1249 inputState.press(a.button);
1250 // Append a copy of |a| with pointerUp subtype
1251 let copy = Object.assign({}, a, { subtype: action.PointerUp });
1252 action.inputsToCancel.push(copy);
1254 switch (inputState.subtype) {
1255 case action.PointerType.Mouse:
1256 let mouseEvent = new action.Mouse("mousedown", a.button);
1257 mouseEvent.update(inputState);
1258 if (mouseEvent.ctrlKey) {
1259 if (Services.appinfo.OS === "Darwin") {
1260 mouseEvent.button = 2;
1261 event.DoubleClickTracker.resetClick();
1263 } else if (event.DoubleClickTracker.isClicked()) {
1264 mouseEvent = Object.assign({}, mouseEvent, { clickCount: 2 });
1266 event.synthesizeMouseAtPoint(
1273 event.MouseButton.isSecondary(a.button) ||
1274 (mouseEvent.ctrlKey && Services.appinfo.OS === "Darwin")
1276 let contextMenuEvent = Object.assign({}, mouseEvent, {
1277 type: "contextmenu",
1279 event.synthesizeMouseAtPoint(
1288 case action.PointerType.Pen:
1289 case action.PointerType.Touch:
1290 throw new UnsupportedOperationError(
1291 "Only 'mouse' pointer type is supported"
1295 throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
1303 * Dispatch a pointerUp action equivalent to releasing a pointer-device
1306 * @param {action.Action} a
1307 * Action to dispatch.
1308 * @param {action.InputState} inputState
1309 * Input state for this action's input source.
1310 * @param {WindowProxy} win
1311 * Current window global.
1314 * Promise to dispatch at least a pointerup event.
1316 function dispatchPointerUp(a, inputState, win) {
1317 return new Promise(resolve => {
1318 if (!inputState.isPressed(a.button)) {
1323 inputState.release(a.button);
1325 switch (inputState.subtype) {
1326 case action.PointerType.Mouse:
1327 let mouseEvent = new action.Mouse("mouseup", a.button);
1328 mouseEvent.update(inputState);
1329 if (event.DoubleClickTracker.isClicked()) {
1330 mouseEvent = Object.assign({}, mouseEvent, { clickCount: 2 });
1332 event.synthesizeMouseAtPoint(
1340 case action.PointerType.Pen:
1341 case action.PointerType.Touch:
1342 throw new UnsupportedOperationError(
1343 "Only 'mouse' pointer type is supported"
1347 throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
1355 * Dispatch a pointerMove action equivalent to moving pointer device
1358 * If the action duration is 0, the pointer jumps immediately to the
1359 * target coordinates. Otherwise, events are synthesized to mimic a
1360 * pointer travelling in a discontinuous, approximately straight line,
1361 * with the pointer coordinates being updated around 60 times per second.
1363 * @param {action.Action} a
1364 * Action to dispatch.
1365 * @param {action.InputState} inputState
1366 * Input state for this action's input source.
1367 * @param {WindowProxy} win
1368 * Current window global.
1371 * Promise to dispatch at least one pointermove event, as well as
1372 * mousemove events as appropriate.
1374 function dispatchPointerMove(a, inputState, tickDuration, win) {
1375 const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
1376 // interval between pointermove increments in ms, based on common vsync
1379 return new Promise((resolve, reject) => {
1380 const start = Date.now();
1381 const [startX, startY] = [inputState.x, inputState.y];
1383 let coords = getElementCenter(a.origin, win);
1384 let target = action.computePointerDestination(a, inputState, coords);
1385 const [targetX, targetY] = [target.x, target.y];
1387 if (!inViewPort(targetX, targetY, win)) {
1388 throw new MoveTargetOutOfBoundsError(
1389 `(${targetX}, ${targetY}) is out of bounds of viewport ` +
1390 `width (${win.innerWidth}) ` +
1391 `and height (${win.innerHeight})`
1396 typeof a.duration == "undefined" ? tickDuration : a.duration;
1397 if (duration === 0) {
1398 // move pointer to destination in one step
1399 performOnePointerMove(inputState, targetX, targetY, win);
1404 const distanceX = targetX - startX;
1405 const distanceY = targetY - startY;
1406 const ONE_SHOT = Ci.nsITimer.TYPE_ONE_SHOT;
1407 let intermediatePointerEvents = (async () => {
1408 // wait |fps60| ms before performing first incremental pointer move
1409 await new Promise(resolveTimer =>
1410 timer.initWithCallback(resolveTimer, fps60, ONE_SHOT)
1413 let durationRatio = Math.floor(Date.now() - start) / duration;
1414 const epsilon = fps60 / duration / 10;
1415 while (1 - durationRatio > epsilon) {
1416 let x = Math.floor(durationRatio * distanceX + startX);
1417 let y = Math.floor(durationRatio * distanceY + startY);
1418 performOnePointerMove(inputState, x, y, win);
1419 // wait |fps60| ms before performing next pointer move
1420 await new Promise(resolveTimer =>
1421 timer.initWithCallback(resolveTimer, fps60, ONE_SHOT)
1424 durationRatio = Math.floor(Date.now() - start) / duration;
1428 // perform last pointer move after all incremental moves are resolved and
1429 // durationRatio is close enough to 1
1430 intermediatePointerEvents
1432 performOnePointerMove(inputState, targetX, targetY, win);
1441 function performOnePointerMove(inputState, targetX, targetY, win) {
1442 if (targetX == inputState.x && targetY == inputState.y) {
1446 switch (inputState.subtype) {
1447 case action.PointerType.Mouse:
1448 let mouseEvent = new action.Mouse("mousemove");
1449 mouseEvent.update(inputState);
1450 // TODO both pointermove (if available) and mousemove
1451 event.synthesizeMouseAtPoint(targetX, targetY, mouseEvent, win);
1454 case action.PointerType.Pen:
1455 case action.PointerType.Touch:
1456 throw new UnsupportedOperationError(
1457 "Only 'mouse' pointer type is supported"
1461 throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
1464 inputState.x = targetX;
1465 inputState.y = targetY;
1469 * Dispatch a pause action equivalent waiting for `a.duration`
1470 * milliseconds, or a default time interval of `tickDuration`.
1472 * @param {action.Action} a
1473 * Action to dispatch.
1474 * @param {number} tickDuration
1475 * Duration in milliseconds of this tick.
1478 * Promise that is resolved after the specified time interval.
1480 function dispatchPause(a, tickDuration) {
1481 let ms = typeof a.duration == "undefined" ? tickDuration : a.duration;
1487 function capitalize(str) {
1489 return str.charAt(0).toUpperCase() + str.slice(1);
1492 function inViewPort(x, y, win) {
1493 assert.number(x, `Expected x to be finite number`);
1494 assert.number(y, `Expected y to be finite number`);
1495 // Viewport includes scrollbars if rendered.
1496 return !(x < 0 || y < 0 || x > win.innerWidth || y > win.innerHeight);
1499 function getElementCenter(el, win) {
1500 if (element.isDOMElement(el)) {
1501 if (action.specCompatPointerOrigin) {
1502 return element.getInViewCentrePoint(el.getClientRects()[0], win);
1504 return element.coordinates(el);