12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349 |
- /* 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);
- }
- }
|