--- a/testing/marionette/action.js
+++ b/testing/marionette/action.js
@@ -1,26 +1,29 @@
/* 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 interfac for providing
+ * 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",
@@ -35,48 +38,343 @@ const ACTIONS = {
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 subtypes for a pointer input source. */
action.PointerType = {
Mouse: "mouse",
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
+ * @throws {InvalidArgumentError}
* If |str| is not a valid pointer type.
*/
action.PointerType.get = function (str) {
let name = capitalize(str);
if (!(name in this)) {
throw new InvalidArgumentError(`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 = new Map();
+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();
}
@@ -96,27 +394,28 @@ class InputState {
return this.type === other.type;
}
toString() {
return `[object ${this.constructor.name}InputState]`;
}
/**
- * @param {?} actionSequence
- * Object representing an action sequence.
+ * @param {?} obj
+ * Object with property |type|, representing an action sequence or an
+ * action item.
*
* @return {action.InputState}
* An |action.InputState| object for the type of the |actionSequence|.
*
- * @throws InvalidArgumentError
+ * @throws {InvalidArgumentError}
* If |actionSequence.type| is not valid.
*/
- static fromJson(actionSequence) {
- let type = actionSequence.type;
+ static fromJson(obj) {
+ let type = obj.type;
if (!(type in ACTIONS)) {
throw new InvalidArgumentError(`Unknown action type: ${type}`);
}
let name = type == "none" ? "Null" : capitalize(type);
return new action.InputState[name]();
}
}
@@ -130,16 +429,75 @@ action.InputState.Key = class extends In
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| is removed successfully, false otherwise.
+ */
+ release(key) {
+ return this.pressed.delete(key);
+ }
};
/**
* Input state not associated with a specific physical device.
*/
action.InputState.Null = class extends InputState {
constructor() {
super();
@@ -171,17 +529,17 @@ action.InputState.Pointer = class extend
*
* @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
+ * @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]) {
@@ -197,24 +555,24 @@ action.Action = class {
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|
+ * Object representing a single action from |actionSequence|.
*
* @return {action.Action}
* An action that can be dispatched; corresponds to |actionItem|.
*
- * @throws InvalidArgumentError
+ * @throws {InvalidArgumentError}
* If any |actionSequence| or |actionItem| attributes are invalid.
- * @throws UnsupportedOperationError
+ * @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);
@@ -230,19 +588,22 @@ action.Action = class {
action.PointerParameters.fromJson(actionSequence.parameters), item);
}
switch (item.subtype) {
case action.KeyUp:
case action.KeyDown:
let key = actionItem.value;
// TODO countGraphemes
- if (typeof key != "string" || (typeof key == "string" && key.length != 1)) {
- throw new InvalidArgumentError("Expected 'key' to be a single-character string, " +
- "got: " + key);
+ // TODO key.value could be a single code point like "\uE012" (see rawKey)
+ // or "grapheme cluster"
+ if (typeof key != "string") {
+ throw new InvalidArgumentError(
+ "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`);
@@ -294,30 +655,29 @@ action.Action = class {
* 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
+ * @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
+ * @throws {InvalidArgumentError}
* If |actions| is not an Array.
*/
static fromJson(actions) {
- if (!Array.isArray(actions)) {
- throw new InvalidArgumentError(`Expected 'actions' to be an Array, got: ${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([]);
@@ -325,31 +685,31 @@ action.Chain = class extends Array {
actionsByTick[i].push(inputSourceActions[i]);
}
}
return actionsByTick;
}
};
/**
- * Represents one input source action sequence; this is essentially an |Array<action.Action>|.
+ * 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
+ * @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 only to validate 'type' and InputState type
let inputSourceState = InputState.fromJson(actionSequence);
let id = actionSequence.id;
@@ -379,24 +739,24 @@ action.Sequence = class extends Array {
/**
* 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.
* @param {boolean=} primary
* Whether the input source is the primary pointing device.
- * If the parameter is underfined, true is used.
+ * If the parameter is undefined, true is used.
*/
action.PointerParameters = class {
constructor(pointerType = "mouse", primary = true) {
this.pointerType = action.PointerType.get(pointerType);
assert.boolean(primary);
this.primary = primary;
- };
+ }
toString() {
return `[pointerParameters ${this.pointerType}, primary=${this.primary}]`;
}
/**
* @param {?} parametersData
* Object that represents pointer parameters.
@@ -419,30 +779,263 @@ action.PointerParameters = class {
*
* @param {string} id
* Input source ID.
* @param {action.PointerParams} pointerParams
* Input source pointer parameters.
* @param {action.Action} act
* Action to be updated.
*
- * @throws InvalidArgumentError
+ * @throws {InvalidArgumentError}
* If |id| is already mapped to an |action.InputState| that is
* not compatible with |act.subtype|.
*/
action.processPointerAction = function processPointerAction(id, pointerParams, act) {
let subtype = act.subtype;
if (action.inputStateMap.has(id) && action.inputStateMap.get(id).subtype !== subtype) {
throw new InvalidArgumentError(
`Expected 'id' ${id} to be mapped to InputState whose subtype is ` +
`${action.inputStateMap.get(id).subtype}, got: ${subtype}`);
}
act.pointerType = pointerParams.pointerType;
act.primary = pointerParams.primary;
};
+/** 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;
+ }
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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) {
+ if (!action.inputStateMap.has(a.id)) {
+ action.inputStateMap.set(a.id, InputState.fromJson(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:
+ case action.PointerUp:
+ case action.PointerMove:
+ 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 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) {
if (typeof str != "string") {
throw new InvalidArgumentError(`Expected string, got: ${str}`);
}
return str.charAt(0).toUpperCase() + str.slice(1);
}
--- a/testing/marionette/assert.js
+++ b/testing/marionette/assert.js
@@ -188,16 +188,35 @@ assert.string = function (obj, msg = "")
* If |obj| is not an object.
*/
assert.object = function (obj, msg = "") {
msg = msg || error.pprint`Expected ${obj} to be an object`;
return assert.that(o => typeof o == "object", msg)(obj);
};
/**
+ * Asserts that |obj| is an Array.
+ *
+ * @param {?} obj
+ * Value to test.
+ * @param {string=} msg
+ * Custom error message.
+ *
+ * @return {Object}
+ * |obj| is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ * If |obj| is not an Array.
+ */
+assert.array = function (obj, msg = "") {
+ msg = msg || error.pprint`Expected ${obj} to be an Array`;
+ return assert.that(o => Array.isArray(o), msg)(obj);
+};
+
+/**
* Returns a function that is used to assert the |predicate|.
*
* @param {function(?): boolean} predicate
* Evaluated on calling the return value of this function. If its
* return value of the inner function is false, |error| is thrown
* with |message|.
* @param {string=} message
* Custom error message.
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -1640,16 +1640,42 @@ GeckoDriver.prototype.singleTap = functi
case Context.CONTENT:
this.addFrameCloseListener("tap");
yield this.listener.singleTap(id, x, y);
break;
}
};
/**
+ * Perform a series of grouped actions at the specified points in time.
+ *
+ * @param {Array.<?>} actions
+ * Array of objects that each represent an action sequence.
+ *
+ * @throws {UnsupportedOperationError}
+ * If the command is made in chrome context.
+ */
+GeckoDriver.prototype.performActions = function(cmd, resp) {
+ switch (this.context) {
+ case Context.CHROME:
+ throw new UnsupportedOperationError(
+ "Command 'performActions' is not available in chrome context");
+ case Context.CONTENT:
+ return this.listener.performActions({"actions": cmd.parameters.actions});
+ }
+};
+
+/**
+ * Release all the keys and pointer buttons that are currently depressed.
+ */
+GeckoDriver.prototype.releaseActions = function(cmd, resp) {
+ return this.listener.releaseActions();
+};
+
+/**
* An action chain.
*
* @param {Object} value
* A nested array where the inner array represents each event,
* and the outer array represents a collection of events.
*
* @return {number}
* Last touch ID.
@@ -2853,16 +2879,18 @@ GeckoDriver.prototype.commands = {
"getLogs": GeckoDriver.prototype.getLogs,
"setContext": GeckoDriver.prototype.setContext,
"getContext": GeckoDriver.prototype.getContext,
"executeScript": GeckoDriver.prototype.executeScript,
"getTimeouts": GeckoDriver.prototype.getTimeouts,
"timeouts": GeckoDriver.prototype.setTimeouts, // deprecated until Firefox 55
"setTimeouts": GeckoDriver.prototype.setTimeouts,
"singleTap": GeckoDriver.prototype.singleTap,
+ "performActions": GeckoDriver.prototype.performActions,
+ "releaseActions": GeckoDriver.prototype.releaseActions,
"actionChain": GeckoDriver.prototype.actionChain, // deprecated
"multiAction": GeckoDriver.prototype.multiAction, // deprecated
"executeAsyncScript": GeckoDriver.prototype.executeAsyncScript,
"executeJSScript": GeckoDriver.prototype.executeJSScript,
"findElement": GeckoDriver.prototype.findElement,
"findElements": GeckoDriver.prototype.findElements,
"clickElement": GeckoDriver.prototype.clickElement,
"getElementAttribute": GeckoDriver.prototype.getElementAttribute,
--- a/testing/marionette/event.js
+++ b/testing/marionette/event.js
@@ -479,22 +479,22 @@ event.isKeypressFiredKey = function (key
/**
* Synthesise a key event.
*
* It is targeted at whatever would be targeted by an actual keypress
* by the user, typically the focused element.
*
* @param {string} key
* Key to synthesise. Should either be a character or a key code
- * starting with "VK_" such as VK_RETURN.
+ * starting with "VK_" such as VK_RETURN, or a normalized key value.
* @param {Object.<string, ?>} event
* Object which may contain the properties shiftKey, ctrlKey, altKey,
- * metaKey, accessKey, type. If the type is specified, a key event
- * of that type is fired. Otherwise, a keydown, a keypress, and then a
- * keyup event are fired in sequence.
+ * metaKey, accessKey, type. If the type is specified (keydown or keyup),
+ * a key event of that type is fired. Otherwise, a keydown, a keypress,
+ * and then a keyup event are fired in sequence.
* @param {Window=} window
* Window object. Defaults to the current window.
*
* @throws {TypeError}
* If unknown key.
*/
event.synthesizeKey = function (key, event, win = undefined)
{
@@ -589,17 +589,20 @@ function createKeyboardEventDictionary_(
} else if (key != "") {
keyName = key;
if (!keyCodeIsDefined) {
keyCode = computeKeyCodeFromChar_(key.charAt(0));
}
if (!keyCode) {
result.flags |= Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
}
- result.flags |= Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
+ // keyName was already determined in keyEvent so no fall-back needed
+ if (!("key" in keyEvent && keyName == keyEvent.key)) {
+ result.flags |= Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
+ }
}
var locationIsDefined = "location" in keyEvent;
if (locationIsDefined && keyEvent.location === 0) {
result.flags |= Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD;
}
result.dictionary = {
key: keyName,
code: "code" in keyEvent ? keyEvent.code : "",
@@ -1204,35 +1207,52 @@ function getKeyCode(c) {
return VIRTUAL_KEYCODE_LOOKUP[c];
}
return c;
}
event.sendKeyDown = function (keyToSend, modifiers, document) {
modifiers.type = "keydown";
event.sendSingleKey(keyToSend, modifiers, document);
+ // TODO This doesn't do anything since |synthesizeKeyEvent| ignores explicit
+ // keypress request, and instead figures out itself when to send keypress
if (["VK_SHIFT", "VK_CONTROL", "VK_ALT", "VK_META"].indexOf(getKeyCode(keyToSend)) < 0) {
modifiers.type = "keypress";
event.sendSingleKey(keyToSend, modifiers, document);
}
delete modifiers.type;
};
event.sendKeyUp = function (keyToSend, modifiers, window = undefined) {
modifiers.type = "keyup";
event.sendSingleKey(keyToSend, modifiers, window);
delete modifiers.type;
};
+/**
+ * Synthesize a key event for a single key.
+ *
+ * @param {string} keyToSend
+ * Code point or normalized key value
+ * @param {?} modifiers
+ * Object with properties used in KeyboardEvent (shiftkey, repeat, ...)
+ * as well as, the event |type| such as keydown. All properties are optional.
+ * @param {Window=} window
+ * Window object. If |window| is undefined, the event is synthesized in
+ * current window.
+ */
event.sendSingleKey = function (keyToSend, modifiers, window = undefined) {
let keyCode = getKeyCode(keyToSend);
if (keyCode in KEYCODES_LOOKUP) {
+ // We assume that if |keyToSend| is a raw code point (like "\uE009") then
+ // |modifiers| does not already have correct value for corresponding
+ // |modName| attribute (like ctrlKey), so that value needs to be flipped
let modName = KEYCODES_LOOKUP[keyCode];
modifiers[modName] = !modifiers[modName];
- } else if (modifiers.shiftKey) {
+ } else if (modifiers.shiftKey && keyCode != "Shift") {
keyCode = keyCode.toUpperCase();
}
event.synthesizeKey(keyCode, modifiers, window);
};
/**
* Focus element and, if a textual input field and no previous selection
* state exists, move the caret to the end of the input field.
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -8,25 +8,26 @@ var {classes: Cc, interfaces: Ci, utils:
var uuidGen = Cc["@mozilla.org/uuid-generator;1"]
.getService(Ci.nsIUUIDGenerator);
var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader);
Cu.import("chrome://marionette/content/accessibility.js");
-Cu.import("chrome://marionette/content/legacyaction.js");
+Cu.import("chrome://marionette/content/action.js");
Cu.import("chrome://marionette/content/atom.js");
Cu.import("chrome://marionette/content/capture.js");
Cu.import("chrome://marionette/content/cookies.js");
Cu.import("chrome://marionette/content/element.js");
Cu.import("chrome://marionette/content/error.js");
Cu.import("chrome://marionette/content/evaluate.js");
Cu.import("chrome://marionette/content/event.js");
Cu.import("chrome://marionette/content/interaction.js");
+Cu.import("chrome://marionette/content/legacyaction.js");
Cu.import("chrome://marionette/content/logging.js");
Cu.import("chrome://marionette/content/navigate.js");
Cu.import("chrome://marionette/content/proxy.js");
Cu.import("chrome://marionette/content/simpletest.js");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -235,16 +236,18 @@ var isElementSelectedFn = dispatch(isEle
var clearElementFn = dispatch(clearElement);
var isElementDisplayedFn = dispatch(isElementDisplayed);
var getElementValueOfCssPropertyFn = dispatch(getElementValueOfCssProperty);
var switchToShadowRootFn = dispatch(switchToShadowRoot);
var getCookiesFn = dispatch(getCookies);
var singleTapFn = dispatch(singleTap);
var takeScreenshotFn = dispatch(takeScreenshot);
var getScreenshotHashFn = dispatch(getScreenshotHash);
+var performActionsFn = dispatch(performActions);
+var releaseActionsFn = dispatch(releaseActions);
var actionChainFn = dispatch(actionChain);
var multiActionFn = dispatch(multiAction);
var addCookieFn = dispatch(addCookie);
var deleteCookieFn = dispatch(deleteCookie);
var deleteAllCookiesFn = dispatch(deleteAllCookies);
var executeFn = dispatch(execute);
var executeInSandboxFn = dispatch(executeInSandbox);
var executeSimpleTestFn = dispatch(executeSimpleTest);
@@ -254,16 +257,18 @@ var sendKeysToElementFn = dispatch(sendK
* Start all message listeners
*/
function startListeners() {
addMessageListenerId("Marionette:newSession", newSession);
addMessageListenerId("Marionette:execute", executeFn);
addMessageListenerId("Marionette:executeInSandbox", executeInSandboxFn);
addMessageListenerId("Marionette:executeSimpleTest", executeSimpleTestFn);
addMessageListenerId("Marionette:singleTap", singleTapFn);
+ addMessageListenerId("Marionette:performActions", performActionsFn);
+ addMessageListenerId("Marionette:releaseActions", releaseActionsFn);
addMessageListenerId("Marionette:actionChain", actionChainFn);
addMessageListenerId("Marionette:multiAction", multiActionFn);
addMessageListenerId("Marionette:get", get);
addMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
addMessageListenerId("Marionette:cancelRequest", cancelRequest);
addMessageListenerId("Marionette:getCurrentUrl", getCurrentUrlFn);
addMessageListenerId("Marionette:getTitle", getTitleFn);
addMessageListenerId("Marionette:getPageSource", getPageSourceFn);
@@ -358,16 +363,18 @@ function restart(msg) {
* Removes all listeners
*/
function deleteSession(msg) {
removeMessageListenerId("Marionette:newSession", newSession);
removeMessageListenerId("Marionette:execute", executeFn);
removeMessageListenerId("Marionette:executeInSandbox", executeInSandboxFn);
removeMessageListenerId("Marionette:executeSimpleTest", executeSimpleTestFn);
removeMessageListenerId("Marionette:singleTap", singleTapFn);
+ removeMessageListenerId("Marionette:performActions", performActionsFn);
+ removeMessageListenerId("Marionette:releaseActions", releaseActionsFn);
removeMessageListenerId("Marionette:actionChain", actionChainFn);
removeMessageListenerId("Marionette:multiAction", multiActionFn);
removeMessageListenerId("Marionette:get", get);
removeMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
removeMessageListenerId("Marionette:cancelRequest", cancelRequest);
removeMessageListenerId("Marionette:getTitle", getTitleFn);
removeMessageListenerId("Marionette:getPageSource", getPageSourceFn);
removeMessageListenerId("Marionette:getCurrentUrl", getCurrentUrlFn);
@@ -405,16 +412,22 @@ function deleteSession(msg) {
if (isB2G) {
content.removeEventListener("mozbrowsershowmodalprompt", modalHandler, false);
}
seenEls.clear();
// reset container frame to the top-most frame
curContainer = { frame: content, shadowRoot: null };
curContainer.frame.focus();
legacyactions.touchIds = {};
+ if (action.inputStateMap !== undefined) {
+ action.inputStateMap.clear();
+ }
+ if (action.inputsToCancel !== undefined) {
+ action.inputsToCancel.length = 0;
+ }
}
/**
* Send asynchronous reply to chrome.
*
* @param {UUID} uuid
* Unique identifier of the request.
* @param {AsyncContentSender.ResponseType} type
@@ -473,16 +486,18 @@ function sendLog(msg) {
/**
* Clear test values after completion of test
*/
function resetValues() {
sandboxes.clear();
curContainer = {frame: content, shadowRoot: null};
legacyactions.mouseEventsOnly = false;
+ action.inputStateMap = new Map();
+ action.inputsToCancel = [];
}
/**
* Dump a logline to stdout. Prepends logline with a timestamp.
*/
function dumpLog(logline) {
dump(Date.now() + " Marionette: " + logline);
}
@@ -662,16 +677,40 @@ function createATouch(el, corx, cory, to
let win = doc.defaultView;
let [clientX, clientY, pageX, pageY, screenX, screenY] =
legacyactions.getCoordinateInfo(el, corx, cory);
let atouch = doc.createTouch(win, el, touchId, pageX, pageY, screenX, screenY, clientX, clientY);
return atouch;
}
/**
+ * Perform a series of grouped actions at the specified points in time.
+ *
+ * @param {obj} msg
+ * Object with an |actions| attribute that is an Array of objects
+ * each of which represents an action sequence.
+ */
+function performActions(msg) {
+ let chain = action.Chain.fromJson(msg.actions);
+ action.dispatch(chain, seenEls, curContainer);
+}
+
+/**
+ * The Release Actions command is used to release all the keys and pointer
+ * buttons that are currently depressed. This causes events to be fired as if
+ * the state was released by an explicit series of actions. It also clears all
+ * the internal state of the virtual devices.
+ */
+function releaseActions() {
+ action.dispatchTickActions(action.inputsToCancel.reverse(), 0, seenEls, curContainer);
+ action.inputsToCancel.length = 0;
+ action.inputStateMap.clear();
+}
+
+/**
* Start action chain on one finger.
*/
function actionChain(chain, touchId) {
let touchProvider = {};
touchProvider.createATouch = createATouch;
touchProvider.emitTouchEvent = emitTouchEvent;
return legacyactions.dispatchActions(
--- a/testing/marionette/test_action.js
+++ b/testing/marionette/test_action.js
@@ -5,16 +5,18 @@
"use strict";
const {utils: Cu} = Components;
Cu.import("chrome://marionette/content/action.js");
Cu.import("chrome://marionette/content/element.js");
Cu.import("chrome://marionette/content/error.js");
+action.inputStateMap = new Map();
+
add_test(function test_createAction() {
Assert.throws(() => new action.Action(), InvalidArgumentError,
"Missing Action constructor args");
Assert.throws(() => new action.Action(1,2), InvalidArgumentError,
"Missing Action constructor args");
Assert.throws(
() => new action.Action(1, 2, "sometype"), /Expected string/, "Non-string arguments.");
ok(new action.Action("id", "sometype", "sometype"));
@@ -27,17 +29,17 @@ add_test(function test_defaultPointerPar
deepEqual(action.PointerParameters.fromJson(), defaultParameters);
run_next_test();
});
add_test(function test_processPointerParameters() {
let check = (regex, message, arg) => checkErrors(
regex, action.PointerParameters.fromJson, [arg], message);
- let parametersData = {pointerType: "foo"};
+ let parametersData = {pointerType: "foo", primary: undefined};
let message = `parametersData: [pointerType: ${parametersData.pointerType}, ` +
`primary: ${parametersData.primary}]`;
check(/Unknown pointerType/, message, parametersData);
parametersData.pointerType = "pen";
parametersData.primary = "a";
check(/Expected \[object String\] "a" to be boolean/, message, parametersData);
parametersData.primary = false;
deepEqual(action.PointerParameters.fromJson(parametersData),
@@ -72,29 +74,29 @@ add_test(function test_validateActionDur
checkErrors(/Expected '.*' \(.*\) to be >= 0/,
action.Action.fromJson, [actionSequence, actionItem], message);
};
for (let d of [-1, "a"]) {
actionItem.duration = d;
check("none", "pause");
check("pointer", "pointerMove");
}
- actionItem.duration = 5;
+ actionItem.duration = 5000;
for (let d of [-1, "a"]) {
for (let name of ["x", "y"]) {
actionItem[name] = d;
check("pointer", "pointerMove", `${name}: ${actionItem[name]}`);
}
}
run_next_test();
});
add_test(function test_processPointerMoveActionElementValidation() {
let actionSequence = {type: "pointer", id: "some_id"};
- let actionItem = {duration: 5, type: "pointerMove"};
+ let actionItem = {duration: 5000, type: "pointerMove"};
for (let d of [-1, "a", {a: "blah"}]) {
actionItem.element = d;
checkErrors(/Expected 'actionItem.element' to be a web element reference/,
action.Action.fromJson,
[actionSequence, actionItem],
`actionItem.element: (${getTypeString(d)})`);
}
actionItem.element = {[element.Key]: "something"};
@@ -103,38 +105,38 @@ add_test(function test_processPointerMov
run_next_test();
});
add_test(function test_processPointerMoveAction() {
let actionSequence = {id: "some_id", type: "pointer"};
let actionItems = [
{
- duration: 5,
+ duration: 5000,
type: "pointerMove",
element: undefined,
x: undefined,
y: undefined,
},
{
duration: undefined,
type: "pointerMove",
element: {[element.Key]: "id", [element.LegacyKey]: "id"},
x: undefined,
y: undefined,
},
{
- duration: 5,
+ duration: 5000,
type: "pointerMove",
x: 0,
y: undefined,
element: undefined,
},
{
- duration: 5,
+ duration: 5000,
type: "pointerMove",
x: 1,
y: 2,
element: undefined,
},
];
for (let expected of actionItems) {
let actual = action.Action.fromJson(actionSequence, expected);
@@ -153,22 +155,22 @@ add_test(function test_processPointerAct
id: "some_id",
parameters: {
pointerType: "touch",
primary: false,
},
}
let actionItems = [
{
- duration: 2,
+ duration: 2000,
type: "pause",
},
{
type: "pointerMove",
- duration: 2,
+ duration: 2000,
},
{
type: "pointerUp",
button: 1,
}
];
for (let expected of actionItems) {
let actual = action.Action.fromJson(actionSequence, expected);
@@ -185,17 +187,17 @@ add_test(function test_processPointerAct
equal(actual.pointerType, actionSequence.parameters.pointerType);
}
}
run_next_test();
});
add_test(function test_processPauseAction() {
- let actionItem = {type: "pause", duration: 5};
+ let actionItem = {type: "pause", duration: 5000};
let actionSequence = {id: "some_id"};
for (let type of ["none", "key", "pointer"]) {
actionSequence.type = type;
let act = action.Action.fromJson(actionSequence, actionItem);
ok(act instanceof action.Action);
equal(act.type, type);
equal(act.subtype, actionItem.type);
equal(act.id, actionSequence.id);
@@ -221,26 +223,26 @@ add_test(function test_processActionSubt
}
run_next_test();
});
add_test(function test_processKeyActionUpDown() {
let actionSequence = {type: "key", id: "some_id"};
let actionItem = {type: "keyDown"};
- for (let v of [-1, "bad", undefined, [], ["a"], {length: 1}, null]) {
+ for (let v of [-1, undefined, [], ["a"], {length: 1}, null]) {
actionItem.value = v;
let message = `actionItem.value: (${getTypeString(v)})`;
Assert.throws(() => action.Action.fromJson(actionSequence, actionItem),
InvalidArgumentError, message);
Assert.throws(() => action.Action.fromJson(actionSequence, actionItem),
- /Expected 'key' to be a single-character string/, message);
+ /Expected 'value' to be a string that represents single code point/, message);
}
- actionItem.value = "a";
+ actionItem.value = "\uE004";
let act = action.Action.fromJson(actionSequence, actionItem);
ok(act instanceof action.Action);
equal(act.type, actionSequence.type);
equal(act.subtype, actionItem.type);
equal(act.id, actionSequence.id);
equal(act.value, actionItem.value);
run_next_test();
@@ -317,32 +319,32 @@ add_test(function test_processInputSourc
deepEqual(actions[0], expectedAction);
run_next_test();
});
add_test(function test_processInputSourceActionSequenceGenerateID() {
let actionItems = [
{
type: "pause",
- duration: 5,
+ duration: 5000,
},
];
let actionSequence = {
type: "key",
actions: actionItems,
};
let actions = action.Sequence.fromJson(actionSequence);
equal(typeof actions[0].id, "string");
ok(actions[0].id.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i));
run_next_test();
});
add_test(function test_processInputSourceActionSequenceInputStateMap() {
let id = "1";
- let actionItem = {type: "pause", duration: 5};
+ let actionItem = {type: "pause", duration: 5000};
let actionSequence = {
type: "key",
id: id,
actions: [actionItem],
};
let wrongInputState = new action.InputState.Null();
action.inputStateMap.set(actionSequence.id, wrongInputState);
checkErrors(/to be mapped to/, action.Sequence.fromJson, [actionSequence],
@@ -388,17 +390,17 @@ add_test(function test_extractActionChai
});
add_test(function test_extractActionChainEmpty() {
deepEqual(action.Chain.fromJson([]), []);
run_next_test();
});
add_test(function test_extractActionChain_oneTickOneInput() {
- let actionItem = {type: "pause", duration: 5};
+ let actionItem = {type: "pause", duration: 5000};
let actionSequence = {
type: "none",
id: "some id",
actions: [actionItem],
};
let expectedAction = new action.Action(actionSequence.id, "none", actionItem.type);
expectedAction.duration = actionItem.duration;
let actionsByTick = action.Chain.fromJson([actionSequence]);
@@ -459,16 +461,52 @@ add_test(function test_extractActionChai
// one empty action sequence
actionsByTick = action.Chain.fromJson([keyActionSequence, {type: "none", actions: []}]);
equal(keyActionItems.length, actionsByTick.length);
equal(1, actionsByTick[0].length);
run_next_test();
});
+add_test(function test_computeTickDuration() {
+ let expected = 8000;
+ let tickActions = [
+ {type: "none", subtype: "pause", duration: 5000},
+ {type: "key", subtype: "pause", duration: 1000},
+ {type: "pointer", subtype: "pointerMove", duration: 6000},
+ // invalid because keyDown should not have duration, so duration should be ignored.
+ {type: "key", subtype: "keyDown", duration: 100000},
+ {type: "pointer", subtype: "pause", duration: expected},
+ {type: "pointer", subtype: "pointerUp"},
+ ];
+ equal(expected, action.computeTickDuration(tickActions));
+ run_next_test();
+});
+
+add_test(function test_computeTickDuration_empty() {
+ equal(0, action.computeTickDuration([]));
+ run_next_test();
+});
+
+add_test(function test_computeTickDuration_noDurations() {
+ let tickActions = [
+ // invalid because keyDown should not have duration, so duration should be ignored.
+ {type: "key", subtype: "keyDown", duration: 100000},
+ // undefined duration permitted
+ {type: "none", subtype: "pause"},
+ {type: "pointer", subtype: "pointerMove"},
+ {type: "pointer", subtype: "pointerDown"},
+ {type: "key", subtype: "keyUp"},
+ ];
+
+ equal(0, action.computeTickDuration(tickActions));
+ run_next_test();
+});
+
+
// helpers
function getTypeString(obj) {
return Object.prototype.toString.call(obj);
};
function checkErrors(regex, func, args, message) {
if (typeof message == "undefined") {
message = `actionFunc: ${func.name}; args: ${args}`;