summaryrefslogtreecommitdiffstats
path: root/testing/marionette/action.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette/action.js')
-rw-r--r--testing/marionette/action.js1348
1 files changed, 1348 insertions, 0 deletions
diff --git a/testing/marionette/action.js b/testing/marionette/action.js
new file mode 100644
index 000000000..2eb39810b
--- /dev/null
+++ b/testing/marionette/action.js
@@ -0,0 +1,1348 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+Cu.import("chrome://marionette/content/assert.js");
+Cu.import("chrome://marionette/content/element.js");
+Cu.import("chrome://marionette/content/error.js");
+Cu.import("chrome://marionette/content/event.js");
+
+this.EXPORTED_SYMBOLS = ["action"];
+
+// TODO? With ES 2016 and Symbol you can make a safer approximation
+// to an enum e.g. https://gist.github.com/xmlking/e86e4f15ec32b12c4689
+/**
+ * Implements WebDriver Actions API: a low-level interface for providing
+ * virtualised device input to the web browser.
+ */
+this.action = {
+ Pause: "pause",
+ KeyDown: "keyDown",
+ KeyUp: "keyUp",
+ PointerDown: "pointerDown",
+ PointerUp: "pointerUp",
+ PointerMove: "pointerMove",
+ PointerCancel: "pointerCancel",
+};
+
+const ACTIONS = {
+ none: new Set([action.Pause]),
+ key: new Set([action.Pause, action.KeyDown, action.KeyUp]),
+ pointer: new Set([
+ action.Pause,
+ action.PointerDown,
+ action.PointerUp,
+ action.PointerMove,
+ action.PointerCancel,
+ ]),
+};
+
+/** Map from normalized key value to UI Events modifier key name */
+const MODIFIER_NAME_LOOKUP = {
+ "Alt": "alt",
+ "Shift": "shift",
+ "Control": "ctrl",
+ "Meta": "meta",
+};
+
+/** Map from raw key (codepoint) to normalized key value */
+const NORMALIZED_KEY_LOOKUP = {
+ "\uE000": "Unidentified",
+ "\uE001": "Cancel",
+ "\uE002": "Help",
+ "\uE003": "Backspace",
+ "\uE004": "Tab",
+ "\uE005": "Clear",
+ "\uE006": "Return",
+ "\uE007": "Enter",
+ "\uE008": "Shift",
+ "\uE009": "Control",
+ "\uE00A": "Alt",
+ "\uE00B": "Pause",
+ "\uE00C": "Escape",
+ "\uE00D": " ",
+ "\uE00E": "PageUp",
+ "\uE00F": "PageDown",
+ "\uE010": "End",
+ "\uE011": "Home",
+ "\uE012": "ArrowLeft",
+ "\uE013": "ArrowUp",
+ "\uE014": "ArrowRight",
+ "\uE015": "ArrowDown",
+ "\uE016": "Insert",
+ "\uE017": "Delete",
+ "\uE018": ";",
+ "\uE019": "=",
+ "\uE01A": "0",
+ "\uE01B": "1",
+ "\uE01C": "2",
+ "\uE01D": "3",
+ "\uE01E": "4",
+ "\uE01F": "5",
+ "\uE020": "6",
+ "\uE021": "7",
+ "\uE022": "8",
+ "\uE023": "9",
+ "\uE024": "*",
+ "\uE025": "+",
+ "\uE026": ",",
+ "\uE027": "-",
+ "\uE028": ".",
+ "\uE029": "/",
+ "\uE031": "F1",
+ "\uE032": "F2",
+ "\uE033": "F3",
+ "\uE034": "F4",
+ "\uE035": "F5",
+ "\uE036": "F6",
+ "\uE037": "F7",
+ "\uE038": "F8",
+ "\uE039": "F9",
+ "\uE03A": "F10",
+ "\uE03B": "F11",
+ "\uE03C": "F12",
+ "\uE03D": "Meta",
+ "\uE040": "ZenkakuHankaku",
+ "\uE050": "Shift",
+ "\uE051": "Control",
+ "\uE052": "Alt",
+ "\uE053": "Meta",
+ "\uE054": "PageUp",
+ "\uE055": "PageDown",
+ "\uE056": "End",
+ "\uE057": "Home",
+ "\uE058": "ArrowLeft",
+ "\uE059": "ArrowUp",
+ "\uE05A": "ArrowRight",
+ "\uE05B": "ArrowDown",
+ "\uE05C": "Insert",
+ "\uE05D": "Delete",
+};
+
+/** Map from raw key (codepoint) to key location */
+const KEY_LOCATION_LOOKUP = {
+ "\uE007": 1,
+ "\uE008": 1,
+ "\uE009": 1,
+ "\uE00A": 1,
+ "\uE01A": 3,
+ "\uE01B": 3,
+ "\uE01C": 3,
+ "\uE01D": 3,
+ "\uE01E": 3,
+ "\uE01F": 3,
+ "\uE020": 3,
+ "\uE021": 3,
+ "\uE022": 3,
+ "\uE023": 3,
+ "\uE024": 3,
+ "\uE025": 3,
+ "\uE026": 3,
+ "\uE027": 3,
+ "\uE028": 3,
+ "\uE029": 3,
+ "\uE03D": 1,
+ "\uE050": 2,
+ "\uE051": 2,
+ "\uE052": 2,
+ "\uE053": 2,
+ "\uE054": 3,
+ "\uE055": 3,
+ "\uE056": 3,
+ "\uE057": 3,
+ "\uE058": 3,
+ "\uE059": 3,
+ "\uE05A": 3,
+ "\uE05B": 3,
+ "\uE05C": 3,
+ "\uE05D": 3,
+};
+
+const KEY_CODE_LOOKUP = {
+ "\uE00A": "AltLeft",
+ "\uE052": "AltRight",
+ "\uE015": "ArrowDown",
+ "\uE012": "ArrowLeft",
+ "\uE014": "ArrowRight",
+ "\uE013": "ArrowUp",
+ "`": "Backquote",
+ "~": "Backquote",
+ "\\": "Backslash",
+ "|": "Backslash",
+ "\uE003": "Backspace",
+ "[": "BracketLeft",
+ "{": "BracketLeft",
+ "]": "BracketRight",
+ "}": "BracketRight",
+ ",": "Comma",
+ "<": "Comma",
+ "\uE009": "ControlLeft",
+ "\uE051": "ControlRight",
+ "\uE017": "Delete",
+ ")": "Digit0",
+ "0": "Digit0",
+ "!": "Digit1",
+ "1": "Digit1",
+ "2": "Digit2",
+ "@": "Digit2",
+ "#": "Digit3",
+ "3": "Digit3",
+ "$": "Digit4",
+ "4": "Digit4",
+ "%": "Digit5",
+ "5": "Digit5",
+ "6": "Digit6",
+ "^": "Digit6",
+ "&": "Digit7",
+ "7": "Digit7",
+ "*": "Digit8",
+ "8": "Digit8",
+ "(": "Digit9",
+ "9": "Digit9",
+ "\uE010": "End",
+ "\uE006": "Enter",
+ "+": "Equal",
+ "=": "Equal",
+ "\uE00C": "Escape",
+ "\uE031": "F1",
+ "\uE03A": "F10",
+ "\uE03B": "F11",
+ "\uE03C": "F12",
+ "\uE032": "F2",
+ "\uE033": "F3",
+ "\uE034": "F4",
+ "\uE035": "F5",
+ "\uE036": "F6",
+ "\uE037": "F7",
+ "\uE038": "F8",
+ "\uE039": "F9",
+ "\uE002": "Help",
+ "\uE011": "Home",
+ "\uE016": "Insert",
+ "<": "IntlBackslash",
+ ">": "IntlBackslash",
+ "A": "KeyA",
+ "a": "KeyA",
+ "B": "KeyB",
+ "b": "KeyB",
+ "C": "KeyC",
+ "c": "KeyC",
+ "D": "KeyD",
+ "d": "KeyD",
+ "E": "KeyE",
+ "e": "KeyE",
+ "F": "KeyF",
+ "f": "KeyF",
+ "G": "KeyG",
+ "g": "KeyG",
+ "H": "KeyH",
+ "h": "KeyH",
+ "I": "KeyI",
+ "i": "KeyI",
+ "J": "KeyJ",
+ "j": "KeyJ",
+ "K": "KeyK",
+ "k": "KeyK",
+ "L": "KeyL",
+ "l": "KeyL",
+ "M": "KeyM",
+ "m": "KeyM",
+ "N": "KeyN",
+ "n": "KeyN",
+ "O": "KeyO",
+ "o": "KeyO",
+ "P": "KeyP",
+ "p": "KeyP",
+ "Q": "KeyQ",
+ "q": "KeyQ",
+ "R": "KeyR",
+ "r": "KeyR",
+ "S": "KeyS",
+ "s": "KeyS",
+ "T": "KeyT",
+ "t": "KeyT",
+ "U": "KeyU",
+ "u": "KeyU",
+ "V": "KeyV",
+ "v": "KeyV",
+ "W": "KeyW",
+ "w": "KeyW",
+ "X": "KeyX",
+ "x": "KeyX",
+ "Y": "KeyY",
+ "y": "KeyY",
+ "Z": "KeyZ",
+ "z": "KeyZ",
+ "-": "Minus",
+ "_": "Minus",
+ "\uE01A": "Numpad0",
+ "\uE05C": "Numpad0",
+ "\uE01B": "Numpad1",
+ "\uE056": "Numpad1",
+ "\uE01C": "Numpad2",
+ "\uE05B": "Numpad2",
+ "\uE01D": "Numpad3",
+ "\uE055": "Numpad3",
+ "\uE01E": "Numpad4",
+ "\uE058": "Numpad4",
+ "\uE01F": "Numpad5",
+ "\uE020": "Numpad6",
+ "\uE05A": "Numpad6",
+ "\uE021": "Numpad7",
+ "\uE057": "Numpad7",
+ "\uE022": "Numpad8",
+ "\uE059": "Numpad8",
+ "\uE023": "Numpad9",
+ "\uE054": "Numpad9",
+ "\uE024": "NumpadAdd",
+ "\uE026": "NumpadComma",
+ "\uE028": "NumpadDecimal",
+ "\uE05D": "NumpadDecimal",
+ "\uE029": "NumpadDivide",
+ "\uE007": "NumpadEnter",
+ "\uE024": "NumpadMultiply",
+ "\uE026": "NumpadSubtract",
+ "\uE03D": "OSLeft",
+ "\uE053": "OSRight",
+ "\uE01E": "PageDown",
+ "\uE01F": "PageUp",
+ ".": "Period",
+ ">": "Period",
+ "\"": "Quote",
+ "'": "Quote",
+ ":": "Semicolon",
+ ";": "Semicolon",
+ "\uE008": "ShiftLeft",
+ "\uE050": "ShiftRight",
+ "/": "Slash",
+ "?": "Slash",
+ "\uE00D": "Space",
+ " ": "Space",
+ "\uE004": "Tab",
+};
+
+/** Represents possible values for a pointer-move origin. */
+action.PointerOrigin = {
+ Viewport: "viewport",
+ Pointer: "pointer",
+};
+
+/**
+ * Look up a PointerOrigin.
+ *
+ * @param {?} obj
+ * Origin for a pointerMove action.
+ *
+ * @return {?}
+ * A pointer origin that is either "viewport" (default), "pointer", or a
+ * web-element reference.
+ *
+ * @throws {InvalidArgumentError}
+ * If |obj| is not a valid origin.
+ */
+action.PointerOrigin.get = function(obj) {
+ let origin = obj;
+ if (typeof obj == "undefined") {
+ origin = this.Viewport;
+ } else if (typeof obj == "string") {
+ let name = capitalize(obj);
+ assert.in(name, this, error.pprint`Unknown pointer-move origin: ${obj}`);
+ origin = this[name];
+ } else if (!element.isWebElementReference(obj)) {
+ throw new InvalidArgumentError("Expected 'origin' to be a string or a " +
+ `web element reference, got: ${obj}`);
+ }
+ return origin;
+};
+
+/** Represents possible subtypes for a pointer input source. */
+action.PointerType = {
+ Mouse: "mouse",
+ // TODO For now, only mouse is supported
+ //Pen: "pen",
+ //Touch: "touch",
+};
+
+/**
+ * Look up a PointerType.
+ *
+ * @param {string} str
+ * Name of pointer type.
+ *
+ * @return {string}
+ * A pointer type for processing pointer parameters.
+ *
+ * @throws {InvalidArgumentError}
+ * If |str| is not a valid pointer type.
+ */
+action.PointerType.get = function (str) {
+ let name = capitalize(str);
+ assert.in(name, this, error.pprint`Unknown pointerType: ${str}`);
+ return this[name];
+};
+
+/**
+ * Input state associated with current session. This is a map between input ID and
+ * the device state for that input source, with one entry for each active input source.
+ *
+ * Initialized in listener.js
+ */
+action.inputStateMap = undefined;
+
+/**
+ * List of |action.Action| associated with current session. Used to manage dispatching
+ * events when resetting the state of the input sources. Reset operations are assumed
+ * to be idempotent.
+ *
+ * Initialized in listener.js
+ */
+action.inputsToCancel = undefined;
+
+/**
+ * Represents device state for an input source.
+ */
+class InputState {
+ constructor() {
+ this.type = this.constructor.name.toLowerCase();
+ }
+
+ /**
+ * Check equality of this InputState object with another.
+ *
+ * @para{?} other
+ * Object representing an input state.
+ * @return {boolean}
+ * True if |this| has the same |type| as |other|.
+ */
+ is(other) {
+ if (typeof other == "undefined") {
+ return false;
+ }
+ return this.type === other.type;
+ }
+
+ toString() {
+ return `[object ${this.constructor.name}InputState]`;
+ }
+
+ /**
+ * @param {?} obj
+ * Object with property |type| and optionally |parameters| or |pointerType|,
+ * representing an action sequence or an action item.
+ *
+ * @return {action.InputState}
+ * An |action.InputState| object for the type of the |actionSequence|.
+ *
+ * @throws {InvalidArgumentError}
+ * If |actionSequence.type| is not valid.
+ */
+ static fromJson(obj) {
+ let type = obj.type;
+ assert.in(type, ACTIONS, error.pprint`Unknown action type: ${type}`);
+ let name = type == "none" ? "Null" : capitalize(type);
+ if (name == "Pointer") {
+ if (!obj.pointerType && (!obj.parameters || !obj.parameters.pointerType)) {
+ throw new InvalidArgumentError(
+ error.pprint`Expected obj to have pointerType, got: ${obj}`);
+ }
+ let pointerType = obj.pointerType || obj.parameters.pointerType;
+ return new action.InputState[name](pointerType);
+ } else {
+ return new action.InputState[name]();
+ }
+ }
+}
+
+/** Possible kinds of |InputState| for supported input sources. */
+action.InputState = {};
+
+/**
+ * Input state associated with a keyboard-type device.
+ */
+action.InputState.Key = class Key extends InputState {
+ constructor() {
+ super();
+ this.pressed = new Set();
+ this.alt = false;
+ this.shift = false;
+ this.ctrl = false;
+ this.meta = false;
+ }
+
+ /**
+ * Update modifier state according to |key|.
+ *
+ * @param {string} key
+ * Normalized key value of a modifier key.
+ * @param {boolean} value
+ * Value to set the modifier attribute to.
+ *
+ * @throws {InvalidArgumentError}
+ * If |key| is not a modifier.
+ */
+ setModState(key, value) {
+ if (key in MODIFIER_NAME_LOOKUP) {
+ this[MODIFIER_NAME_LOOKUP[key]] = value;
+ } else {
+ throw new InvalidArgumentError("Expected 'key' to be one of " +
+ `${Object.keys(MODIFIER_NAME_LOOKUP)}; got: ${key}`);
+ }
+ }
+
+ /**
+ * Check whether |key| is pressed.
+ *
+ * @param {string} key
+ * Normalized key value.
+ *
+ * @return {boolean}
+ * True if |key| is in set of pressed keys.
+ */
+ isPressed(key) {
+ return this.pressed.has(key);
+ }
+
+ /**
+ * Add |key| to the set of pressed keys.
+ *
+ * @param {string} key
+ * Normalized key value.
+ *
+ * @return {boolean}
+ * True if |key| is in list of pressed keys.
+ */
+ press(key) {
+ return this.pressed.add(key);
+ }
+
+ /**
+ * Remove |key| from the set of pressed keys.
+ *
+ * @param {string} key
+ * Normalized key value.
+ *
+ * @return {boolean}
+ * True if |key| was present before removal, false otherwise.
+ */
+ release(key) {
+ return this.pressed.delete(key);
+ }
+};
+
+/**
+ * Input state not associated with a specific physical device.
+ */
+action.InputState.Null = class Null extends InputState {
+ constructor() {
+ super();
+ this.type = "none";
+ }
+};
+
+/**
+ * Input state associated with a pointer-type input device.
+ *
+ * @param {string} subtype
+ * Kind of pointing device: mouse, pen, touch.
+ *
+ * @throws {InvalidArgumentError}
+ * If subtype is undefined or an invalid pointer type.
+ */
+action.InputState.Pointer = class Pointer extends InputState {
+ constructor(subtype) {
+ super();
+ this.pressed = new Set();
+ assert.defined(subtype, error.pprint`Expected subtype to be defined, got: ${subtype}`);
+ this.subtype = action.PointerType.get(subtype);
+ this.x = 0;
+ this.y = 0;
+ }
+
+ /**
+ * Check whether |button| is pressed.
+ *
+ * @param {number} button
+ * Positive integer that refers to a mouse button.
+ *
+ * @return {boolean}
+ * True if |button| is in set of pressed buttons.
+ */
+ isPressed(button) {
+ assert.positiveInteger(button);
+ return this.pressed.has(button);
+ }
+
+ /**
+ * Add |button| to the set of pressed keys.
+ *
+ * @param {number} button
+ * Positive integer that refers to a mouse button.
+ *
+ * @return {Set}
+ * Set of pressed buttons.
+ */
+ press(button) {
+ assert.positiveInteger(button);
+ return this.pressed.add(button);
+ }
+
+ /**
+ * Remove |button| from the set of pressed buttons.
+ *
+ * @param {number} button
+ * A positive integer that refers to a mouse button.
+ *
+ * @return {boolean}
+ * True if |button| was present before removals, false otherwise.
+ */
+ release(button) {
+ assert.positiveInteger(button);
+ return this.pressed.delete(button);
+ }
+};
+
+/**
+ * Repesents an action for dispatch. Used in |action.Chain| and |action.Sequence|.
+ *
+ * @param {string} id
+ * Input source ID.
+ * @param {string} type
+ * Action type: none, key, pointer.
+ * @param {string} subtype
+ * Action subtype: pause, keyUp, keyDown, pointerUp, pointerDown, pointerMove, pointerCancel.
+ *
+ * @throws {InvalidArgumentError}
+ * If any parameters are undefined.
+ */
+action.Action = class {
+ constructor(id, type, subtype) {
+ if ([id, type, subtype].includes(undefined)) {
+ throw new InvalidArgumentError("Missing id, type or subtype");
+ }
+ for (let attr of [id, type, subtype]) {
+ assert.string(attr, error.pprint`Expected string, got: ${attr}`);
+ }
+ this.id = id;
+ this.type = type;
+ this.subtype = subtype;
+ };
+
+ toString() {
+ return `[action ${this.type}]`;
+ }
+
+ /**
+ * @param {?} actionSequence
+ * Object representing sequence of actions from one input source.
+ * @param {?} actionItem
+ * Object representing a single action from |actionSequence|.
+ *
+ * @return {action.Action}
+ * An action that can be dispatched; corresponds to |actionItem|.
+ *
+ * @throws {InvalidArgumentError}
+ * If any |actionSequence| or |actionItem| attributes are invalid.
+ * @throws {UnsupportedOperationError}
+ * If |actionItem.type| is |pointerCancel|.
+ */
+ static fromJson(actionSequence, actionItem) {
+ let type = actionSequence.type;
+ let id = actionSequence.id;
+ let subtypes = ACTIONS[type];
+ if (!subtypes) {
+ throw new InvalidArgumentError("Unknown type: " + type);
+ }
+ let subtype = actionItem.type;
+ if (!subtypes.has(subtype)) {
+ throw new InvalidArgumentError(`Unknown subtype for ${type} action: ${subtype}`);
+ }
+
+ let item = new action.Action(id, type, subtype);
+ if (type === "pointer") {
+ action.processPointerAction(id,
+ action.PointerParameters.fromJson(actionSequence.parameters), item);
+ }
+
+ switch (item.subtype) {
+ case action.KeyUp:
+ case action.KeyDown:
+ let key = actionItem.value;
+ // TODO countGraphemes
+ // TODO key.value could be a single code point like "\uE012" (see rawKey)
+ // or "grapheme cluster"
+ assert.string(key,
+ error.pprint("Expected 'value' to be a string that represents single code point " +
+ `or grapheme cluster, got: ${key}`));
+ item.value = key;
+ break;
+
+ case action.PointerDown:
+ case action.PointerUp:
+ assert.positiveInteger(actionItem.button,
+ error.pprint`Expected 'button' (${actionItem.button}) to be >= 0`);
+ item.button = actionItem.button;
+ break;
+
+ case action.PointerMove:
+ item.duration = actionItem.duration;
+ if (typeof item.duration != "undefined"){
+ assert.positiveInteger(item.duration,
+ error.pprint`Expected 'duration' (${item.duration}) to be >= 0`);
+ }
+ item.origin = action.PointerOrigin.get(actionItem.origin);
+ item.x = actionItem.x;
+ if (typeof item.x != "undefined") {
+ assert.integer(item.x, error.pprint`Expected 'x' (${item.x}) to be an Integer`);
+ }
+ item.y = actionItem.y;
+ if (typeof item.y != "undefined") {
+ assert.integer(item.y, error.pprint`Expected 'y' (${item.y}) to be an Integer`);
+ }
+ break;
+
+ case action.PointerCancel:
+ throw new UnsupportedOperationError();
+ break;
+
+ case action.Pause:
+ item.duration = actionItem.duration;
+ if (typeof item.duration != "undefined") {
+ assert.positiveInteger(item.duration,
+ error.pprint`Expected 'duration' (${item.duration}) to be >= 0`);
+ }
+ break;
+ }
+
+ return item;
+ }
+};
+
+/**
+ * Represents a series of ticks, specifying which actions to perform at each tick.
+ */
+action.Chain = class extends Array {
+ toString() {
+ return `[chain ${super.toString()}]`;
+ }
+
+ /**
+ * @param {Array.<?>} actions
+ * Array of objects that each represent an action sequence.
+ *
+ * @return {action.Chain}
+ * Transpose of |actions| such that actions to be performed in a single tick
+ * are grouped together.
+ *
+ * @throws {InvalidArgumentError}
+ * If |actions| is not an Array.
+ */
+ static fromJson(actions) {
+ assert.array(actions,
+ error.pprint`Expected 'actions' to be an Array, got: ${actions}`);
+ let actionsByTick = new action.Chain();
+ // TODO check that each actionSequence in actions refers to a different input ID
+ for (let actionSequence of actions) {
+ let inputSourceActions = action.Sequence.fromJson(actionSequence);
+ for (let i = 0; i < inputSourceActions.length; i++) {
+ // new tick
+ if (actionsByTick.length < (i + 1)) {
+ actionsByTick.push([]);
+ }
+ actionsByTick[i].push(inputSourceActions[i]);
+ }
+ }
+ return actionsByTick;
+ }
+};
+
+/**
+ * Represents one input source action sequence; this is essentially an |Array.<action.Action>|.
+ */
+action.Sequence = class extends Array {
+ toString() {
+ return `[sequence ${super.toString()}]`;
+ }
+
+ /**
+ * @param {?} actionSequence
+ * Object that represents a sequence action items for one input source.
+ *
+ * @return {action.Sequence}
+ * Sequence of actions that can be dispatched.
+ *
+ * @throws {InvalidArgumentError}
+ * If |actionSequence.id| is not a string or it's aleady mapped
+ * to an |action.InputState} incompatible with |actionSequence.type|.
+ * If |actionSequence.actions| is not an Array.
+ */
+ static fromJson(actionSequence) {
+ // used here to validate 'type' in addition to InputState type below
+ let inputSourceState = InputState.fromJson(actionSequence);
+ let id = actionSequence.id;
+ assert.defined(id, "Expected 'id' to be defined");
+ assert.string(id, error.pprint`Expected 'id' to be a string, got: ${id}`);
+ let actionItems = actionSequence.actions;
+ assert.array(actionItems,
+ error.pprint("Expected 'actionSequence.actions' to be an Array, " +
+ `got: ${actionSequence.actions}`));
+ if (!action.inputStateMap.has(id)) {
+ action.inputStateMap.set(id, inputSourceState);
+ } else if (!action.inputStateMap.get(id).is(inputSourceState)) {
+ throw new InvalidArgumentError(
+ `Expected ${id} to be mapped to ${inputSourceState}, ` +
+ `got: ${action.inputStateMap.get(id)}`);
+ }
+ let actions = new action.Sequence();
+ for (let actionItem of actionItems) {
+ actions.push(action.Action.fromJson(actionSequence, actionItem));
+ }
+ return actions;
+ }
+};
+
+/**
+ * Represents parameters in an action for a pointer input source.
+ *
+ * @param {string=} pointerType
+ * Type of pointing device. If the parameter is undefined, "mouse" is used.
+ */
+action.PointerParameters = class {
+ constructor(pointerType = "mouse") {
+ this.pointerType = action.PointerType.get(pointerType);
+ }
+
+ toString() {
+ return `[pointerParameters ${this.pointerType}]`;
+ }
+
+ /**
+ * @param {?} parametersData
+ * Object that represents pointer parameters.
+ *
+ * @return {action.PointerParameters}
+ * Validated pointer paramters.
+ */
+ static fromJson(parametersData) {
+ if (typeof parametersData == "undefined") {
+ return new action.PointerParameters();
+ } else {
+ return new action.PointerParameters(parametersData.pointerType);
+ }
+ }
+};
+
+/**
+ * Adds |pointerType| attribute to Action |act|. Helper function
+ * for |action.Action.fromJson|.
+ *
+ * @param {string} id
+ * Input source ID.
+ * @param {action.PointerParams} pointerParams
+ * Input source pointer parameters.
+ * @param {action.Action} act
+ * Action to be updated.
+ *
+ * @throws {InvalidArgumentError}
+ * If |id| is already mapped to an |action.InputState| that is
+ * not compatible with |act.type| or |pointerParams.pointerType|.
+ */
+action.processPointerAction = function processPointerAction(id, pointerParams, act) {
+ if (action.inputStateMap.has(id) && action.inputStateMap.get(id).type !== act.type) {
+ throw new InvalidArgumentError(
+ `Expected 'id' ${id} to be mapped to InputState whose type is ` +
+ `${action.inputStateMap.get(id).type}, got: ${act.type}`);
+ }
+ let pointerType = pointerParams.pointerType;
+ if (action.inputStateMap.has(id) && action.inputStateMap.get(id).subtype !== pointerType) {
+ throw new InvalidArgumentError(
+ `Expected 'id' ${id} to be mapped to InputState whose subtype is ` +
+ `${action.inputStateMap.get(id).subtype}, got: ${pointerType}`);
+ }
+ act.pointerType = pointerParams.pointerType;
+};
+
+/** Collect properties associated with KeyboardEvent */
+action.Key = class {
+ constructor(rawKey) {
+ this.key = NORMALIZED_KEY_LOOKUP[rawKey] || rawKey;
+ this.code = KEY_CODE_LOOKUP[rawKey];
+ this.location = KEY_LOCATION_LOOKUP[rawKey] || 0;
+ this.altKey = false;
+ this.shiftKey = false;
+ this.ctrlKey = false;
+ this.metaKey = false;
+ this.repeat = false;
+ this.isComposing = false;
+ // Prevent keyCode from being guessed in event.js; we don't want to use it anyway.
+ this.keyCode = 0;
+ }
+
+ update(inputState) {
+ this.altKey = inputState.alt;
+ this.shiftKey = inputState.shift;
+ this.ctrlKey = inputState.ctrl;
+ this.metaKey = inputState.meta;
+ }
+};
+
+/** Collect properties associated with MouseEvent */
+action.Mouse = class {
+ constructor(type, button = 0) {
+ this.type = type;
+ assert.positiveInteger(button);
+ this.button = button;
+ this.buttons = 0;
+ }
+
+ update(inputState) {
+ let allButtons = Array.from(inputState.pressed);
+ this.buttons = allButtons.reduce((a, i) => a + Math.pow(2, i), 0);
+ }
+};
+
+/**
+ * Dispatch a chain of actions over |chain.length| ticks.
+ *
+ * This is done by creating a Promise for each tick that resolves once all the
+ * Promises for individual tick-actions are resolved. The next tick's actions are
+ * not dispatched until the Promise for the current tick is resolved.
+ *
+ * @param {action.Chain} chain
+ * Actions grouped by tick; each element in |chain| is a sequence of
+ * actions for one tick.
+ * @param {element.Store} seenEls
+ * Element store.
+ * @param {?} container
+ * Object with |frame| attribute of type |nsIDOMWindow|.
+ *
+ * @return {Promise}
+ * Promise for dispatching all actions in |chain|.
+ */
+action.dispatch = function(chain, seenEls, container) {
+ let chainEvents = Task.spawn(function*() {
+ for (let tickActions of chain) {
+ yield action.dispatchTickActions(
+ tickActions, action.computeTickDuration(tickActions), seenEls, container);
+ }
+ });
+ return chainEvents;
+};
+
+/**
+ * Dispatch sequence of actions for one tick.
+ *
+ * This creates a Promise for one tick that resolves once the Promise for each
+ * tick-action is resolved, which takes at least |tickDuration| milliseconds.
+ * The resolved set of events for each tick is followed by firing of pending DOM events.
+ *
+ * Note that the tick-actions are dispatched in order, but they may have different
+ * durations and therefore may not end in the same order.
+ *
+ * @param {Array.<action.Action>} tickActions
+ * List of actions for one tick.
+ * @param {number} tickDuration
+ * Duration in milliseconds of this tick.
+ * @param {element.Store} seenEls
+ * Element store.
+ * @param {?} container
+ * Object with |frame| attribute of type |nsIDOMWindow|.
+ *
+ * @return {Promise}
+ * Promise for dispatching all tick-actions and pending DOM events.
+ */
+action.dispatchTickActions = function(tickActions, tickDuration, seenEls, container) {
+ let pendingEvents = tickActions.map(toEvents(tickDuration, seenEls, container));
+ return Promise.all(pendingEvents).then(() => flushEvents(container));
+};
+
+/**
+ * Compute tick duration in milliseconds for a collection of actions.
+ *
+ * @param {Array.<action.Action>} tickActions
+ * List of actions for one tick.
+ *
+ * @return {number}
+ * Longest action duration in |tickActions| if any, or 0.
+ */
+action.computeTickDuration = function(tickActions) {
+ let max = 0;
+ for (let a of tickActions) {
+ let affectsWallClockTime = a.subtype == action.Pause ||
+ (a.type == "pointer" && a.subtype == action.PointerMove);
+ if (affectsWallClockTime && a.duration) {
+ max = Math.max(a.duration, max);
+ }
+ }
+ return max;
+};
+
+/**
+ * Compute viewport coordinates of pointer target based on given origin.
+ *
+ * @param {action.Action} a
+ * Action that specifies pointer origin and x and y coordinates of target.
+ * @param {action.InputState} inputState
+ * Input state that specifies current x and y coordinates of pointer.
+ * @param {Map.<string, number>=} center
+ * Object representing x and y coordinates of an element center-point.
+ * This is only used if |a.origin| is a web element reference.
+ *
+ * @return {Map.<string, number>}
+ * x and y coordinates of pointer destination.
+ */
+action.computePointerDestination = function(a, inputState, center = undefined) {
+ let {x, y} = a;
+ switch (a.origin) {
+ case action.PointerOrigin.Viewport:
+ break;
+ case action.PointerOrigin.Pointer:
+ x += inputState.x;
+ y += inputState.y;
+ break;
+ default:
+ // origin represents web element
+ assert.defined(center);
+ assert.in("x", center);
+ assert.in("y", center);
+ x += center.x;
+ y += center.y;
+ }
+ return {"x": x, "y": y};
+};
+
+/**
+ * Create a closure to use as a map from action definitions to Promise events.
+ *
+ * @param {number} tickDuration
+ * Duration in milliseconds of this tick.
+ * @param {element.Store} seenEls
+ * Element store.
+ * @param {?} container
+ * Object with |frame| attribute of type |nsIDOMWindow|.
+ *
+ * @return {function(action.Action): Promise}
+ * Function that takes an action and returns a Promise for dispatching
+ * the event that corresponds to that action.
+ */
+function toEvents(tickDuration, seenEls, container) {
+ return function (a) {
+ let inputState = action.inputStateMap.get(a.id);
+ switch (a.subtype) {
+ case action.KeyUp:
+ return dispatchKeyUp(a, inputState, container.frame);
+
+ case action.KeyDown:
+ return dispatchKeyDown(a, inputState, container.frame);
+
+ case action.PointerDown:
+ return dispatchPointerDown(a, inputState, container.frame);
+
+ case action.PointerUp:
+ return dispatchPointerUp(a, inputState, container.frame);
+
+ case action.PointerMove:
+ return dispatchPointerMove(a, inputState, tickDuration, seenEls, container);
+
+ case action.PointerCancel:
+ throw new UnsupportedOperationError();
+
+ case action.Pause:
+ return dispatchPause(a, tickDuration);
+ }
+ };
+}
+
+/**
+ * Dispatch a keyDown action equivalent to pressing a key on a keyboard.
+ *
+ * @param {action.Action} a
+ * Action to dispatch.
+ * @param {action.InputState} inputState
+ * Input state for this action's input source.
+ * @param {nsIDOMWindow} win
+ * Current window.
+ *
+ * @return {Promise}
+ * Promise to dispatch at least a keydown event, and keypress if appropriate.
+ */
+function dispatchKeyDown(a, inputState, win) {
+ return new Promise(resolve => {
+ let keyEvent = new action.Key(a.value);
+ keyEvent.repeat = inputState.isPressed(keyEvent.key);
+ inputState.press(keyEvent.key);
+ if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
+ inputState.setModState(keyEvent.key, true);
+ }
+ // Append a copy of |a| with keyUp subtype
+ action.inputsToCancel.push(Object.assign({}, a, {subtype: action.KeyUp}));
+ keyEvent.update(inputState);
+ event.sendKeyDown(keyEvent.key, keyEvent, win);
+
+ resolve();
+ });
+}
+
+/**
+ * Dispatch a keyUp action equivalent to releasing a key on a keyboard.
+ *
+ * @param {action.Action} a
+ * Action to dispatch.
+ * @param {action.InputState} inputState
+ * Input state for this action's input source.
+ * @param {nsIDOMWindow} win
+ * Current window.
+ *
+ * @return {Promise}
+ * Promise to dispatch a keyup event.
+ */
+function dispatchKeyUp(a, inputState, win) {
+ return new Promise(resolve => {
+ let keyEvent = new action.Key(a.value);
+ if (!inputState.isPressed(keyEvent.key)) {
+ resolve();
+ return;
+ }
+ if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
+ inputState.setModState(keyEvent.key, false);
+ }
+ inputState.release(keyEvent.key);
+ keyEvent.update(inputState);
+ event.sendKeyUp(keyEvent.key, keyEvent, win);
+
+ resolve();
+ });
+}
+
+/**
+ * Dispatch a pointerDown action equivalent to pressing a pointer-device
+ * button.
+ *
+ * @param {action.Action} a
+ * Action to dispatch.
+ * @param {action.InputState} inputState
+ * Input state for this action's input source.
+ * @param {nsIDOMWindow} win
+ * Current window.
+ *
+ * @return {Promise}
+ * Promise to dispatch at least a pointerdown event.
+ */
+function dispatchPointerDown(a, inputState, win) {
+ return new Promise(resolve => {
+ if (inputState.isPressed(a.button)) {
+ resolve();
+ return;
+ }
+ inputState.press(a.button);
+ // Append a copy of |a| with pointerUp subtype
+ action.inputsToCancel.push(Object.assign({}, a, {subtype: action.PointerUp}));
+ switch (inputState.subtype) {
+ case action.PointerType.Mouse:
+ let mouseEvent = new action.Mouse("mousedown", a.button);
+ mouseEvent.update(inputState);
+ event.synthesizeMouseAtPoint(inputState.x, inputState.y, mouseEvent, win);
+ break;
+ case action.PointerType.Pen:
+ case action.PointerType.Touch:
+ throw new UnsupportedOperationError("Only 'mouse' pointer type is supported");
+ break;
+ default:
+ throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
+ }
+ resolve();
+ });
+}
+
+/**
+ * Dispatch a pointerUp action equivalent to releasing a pointer-device
+ * button.
+ *
+ * @param {action.Action} a
+ * Action to dispatch.
+ * @param {action.InputState} inputState
+ * Input state for this action's input source.
+ * @param {nsIDOMWindow} win
+ * Current window.
+ *
+ * @return {Promise}
+ * Promise to dispatch at least a pointerup event.
+ */
+function dispatchPointerUp(a, inputState, win) {
+ return new Promise(resolve => {
+ if (!inputState.isPressed(a.button)) {
+ resolve();
+ return;
+ }
+ inputState.release(a.button);
+ switch (inputState.subtype) {
+ case action.PointerType.Mouse:
+ let mouseEvent = new action.Mouse("mouseup", a.button);
+ mouseEvent.update(inputState);
+ event.synthesizeMouseAtPoint(inputState.x, inputState.y,
+ mouseEvent, win);
+ break;
+ case action.PointerType.Pen:
+ case action.PointerType.Touch:
+ throw new UnsupportedOperationError("Only 'mouse' pointer type is supported");
+ default:
+ throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
+ }
+ resolve();
+ });
+}
+
+/**
+ * Dispatch a pointerMove action equivalent to moving pointer device in a line.
+ *
+ * If the action duration is 0, the pointer jumps immediately to the target coordinates.
+ * Otherwise, events are synthesized to mimic a pointer travelling in a discontinuous,
+ * approximately straight line, with the pointer coordinates being updated around 60
+ * times per second.
+ *
+ * @param {action.Action} a
+ * Action to dispatch.
+ * @param {action.InputState} inputState
+ * Input state for this action's input source.
+ * @param {element.Store} seenEls
+ * Element store.
+ * @param {?} container
+ * Object with |frame| attribute of type |nsIDOMWindow|.
+ *
+ * @return {Promise}
+ * Promise to dispatch at least one pointermove event, as well as mousemove events
+ * as appropriate.
+ */
+function dispatchPointerMove(a, inputState, tickDuration, seenEls, container) {
+ const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ // interval between pointermove increments in ms, based on common vsync
+ const fps60 = 17;
+ return new Promise(resolve => {
+ const start = Date.now();
+ const [startX, startY] = [inputState.x, inputState.y];
+ let target = action.computePointerDestination(a, inputState,
+ getElementCenter(a.origin, seenEls, container));
+ const [targetX, targetY] = [target.x, target.y];
+ if (!inViewPort(targetX, targetY, container.frame)) {
+ throw new MoveTargetOutOfBoundsError(
+ `(${targetX}, ${targetY}) is out of bounds of viewport ` +
+ `width (${container.frame.innerWidth}) and height (${container.frame.innerHeight})`);
+ }
+
+ const duration = typeof a.duration == "undefined" ? tickDuration : a.duration;
+ if (duration === 0) {
+ // move pointer to destination in one step
+ performOnePointerMove(inputState, targetX, targetY, container.frame);
+ resolve();
+ return;
+ }
+
+ const distanceX = targetX - startX;
+ const distanceY = targetY - startY;
+ const ONE_SHOT = Ci.nsITimer.TYPE_ONE_SHOT;
+ let intermediatePointerEvents = Task.spawn(function* () {
+ // wait |fps60| ms before performing first incremental pointer move
+ yield new Promise(resolveTimer =>
+ timer.initWithCallback(resolveTimer, fps60, ONE_SHOT)
+ );
+ let durationRatio = Math.floor(Date.now() - start) / duration;
+ const epsilon = fps60 / duration / 10;
+ while ((1 - durationRatio) > epsilon) {
+ let x = Math.floor(durationRatio * distanceX + startX);
+ let y = Math.floor(durationRatio * distanceY + startY);
+ performOnePointerMove(inputState, x, y, container.frame);
+ // wait |fps60| ms before performing next pointer move
+ yield new Promise(resolveTimer =>
+ timer.initWithCallback(resolveTimer, fps60, ONE_SHOT));
+ durationRatio = Math.floor(Date.now() - start) / duration;
+ }
+ });
+ // perform last pointer move after all incremental moves are resolved and
+ // durationRatio is close enough to 1
+ intermediatePointerEvents.then(() => {
+ performOnePointerMove(inputState, targetX, targetY, container.frame);
+ resolve();
+ });
+
+ });
+}
+
+function performOnePointerMove(inputState, targetX, targetY, win) {
+ if (targetX == inputState.x && targetY == inputState.y) {
+ return;
+ }
+ switch (inputState.subtype) {
+ case action.PointerType.Mouse:
+ let mouseEvent = new action.Mouse("mousemove");
+ mouseEvent.update(inputState);
+ //TODO both pointermove (if available) and mousemove
+ event.synthesizeMouseAtPoint(targetX, targetY, mouseEvent, win);
+ break;
+ case action.PointerType.Pen:
+ case action.PointerType.Touch:
+ throw new UnsupportedOperationError("Only 'mouse' pointer type is supported");
+ default:
+ throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
+ }
+ inputState.x = targetX;
+ inputState.y = targetY;
+}
+
+/**
+ * Dispatch a pause action equivalent waiting for |a.duration| milliseconds, or a
+ * default time interval of |tickDuration|.
+ *
+ * @param {action.Action} a
+ * Action to dispatch.
+ * @param {number} tickDuration
+ * Duration in milliseconds of this tick.
+ *
+ * @return {Promise}
+ * Promise that is resolved after the specified time interval.
+ */
+function dispatchPause(a, tickDuration) {
+ const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ let duration = typeof a.duration == "undefined" ? tickDuration : a.duration;
+ return new Promise(resolve =>
+ timer.initWithCallback(resolve, duration, Ci.nsITimer.TYPE_ONE_SHOT)
+ );
+}
+
+// helpers
+/**
+ * Force any pending DOM events to fire.
+ *
+ * @param {?} container
+ * Object with |frame| attribute of type |nsIDOMWindow|.
+ *
+ * @return {Promise}
+ * Promise to flush DOM events.
+ */
+function flushEvents(container) {
+ return new Promise(resolve => container.frame.requestAnimationFrame(resolve));
+}
+
+function capitalize(str) {
+ assert.string(str);
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
+
+function inViewPort(x, y, win) {
+ assert.number(x, `Expected x to be finite number`);
+ assert.number(y, `Expected y to be finite number`);
+ // Viewport includes scrollbars if rendered.
+ return !(x < 0 || y < 0 || x > win.innerWidth || y > win.innerHeight);
+}
+
+function getElementCenter(elementReference, seenEls, container) {
+ if (element.isWebElementReference(elementReference)) {
+ let uuid = elementReference[element.Key] || elementReference[element.LegacyKey];
+ let el = seenEls.get(uuid, container);
+ return element.coordinates(el);
+ }
+}