Formalize PointerType and ACTIONS; rework InputState draft
authorMaja Frydrychowicz <mjzffr@gmail.com>
Thu, 06 Oct 2016 11:20:28 -0400
changeset 421892 cf87f4ccdfa1bcea5fc7d568518ededcfa22472b
parent 421891 66558fd8c1418454b3ddac63835fbad4e8324b71
child 421893 0d7af726133f5712476e86c5cca00f63671097c0
push id31629
push usermjzffr@gmail.com
push dateFri, 07 Oct 2016 02:36:08 +0000
milestone52.0a1
Formalize PointerType and ACTIONS; rework InputState MozReview-Commit-ID: 3bUmPdUHVrn
testing/marionette/action.js
testing/marionette/element.js
testing/marionette/test_action.js
--- a/testing/marionette/action.js
+++ b/testing/marionette/action.js
@@ -11,74 +11,121 @@ Cu.import("resource://gre/modules/Log.js
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/error.js");
 
 
 this.EXPORTED_SYMBOLS = ["action"];
 
 const logger = Log.repository.getLogger("Marionette");
 
-this.action = {};
+this.action = {
+  Pause: "pause",
+  KeyDown: "keyDown",
+  KeyUp: "keyUp",
+  PointerDown: "pointerDown",
+  PointerUp: "pointerUp",
+  PointerMove: "pointerMove",
+  PointerCancel: "pointerCancel",
+};
 
-action.Types = new Set([
-  "none",
-  "key",
-  "pointer",
-]);
+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,
+  ]),
+};
+
+
+action.PointerType = {
+  Mouse: "mouse",
+  Pen: "pen",
+  Touch: "touch",
+};
+
+action.PointerType.get = function(str){
+  let name = capitalize(str);
+  if (!(capitalize(str) in this)) {
+    throw new InvalidArgumentError(`Unknown pointerType: ${str}`);
+  }
+  else {
+    return this[name];
+  }
+};
 
 // map between input id (string) and device state for that input source
 // TODO associate with session
 // populated by "dispatch tick actions"?
 action.inputStateMap = new Map();
 
 // input source: virtual device that has an string id and a source type
 // This doesn't seem to be really used in spec. Each "actionSequence" has id
 // and type already, which is all we really need.
 // action.InputSource = function InputSource(id, type) {
 //   this.id = id;
 //   // null, key, pointer
 //   this.type = type;
 // }
 
-action.NullInputState = function NullInputState(){
-  // a named empty object
-};
+class InputState {
+  constructor() {
+    this.type = this.constructor.name.toLowerCase();
+  }
+
+  is(other) {
+    return this.type == other.type;
+  }
+
+  toString() {
+    return `[object ${this.constructor.name}InputState]`;
+  }
 
-action.NullInputState.prototype.toString = function() {
-  return '[object NullInputState]';
-};
+  static fromJson(json) {
+    let type = json.type;
+    if (!(type in ACTIONS)) {
+      throw new InvalidArgumentError(`Unknown action type: ${type}`);
+    }
+    let name = type == "none" ? "Null" : capitalize(type);
+    return new action.InputState[name]();
+  }
+}
 
-action.KeyInputState = function KeyInputState() {
-  this.pressed = new Set();
-  this.alt = false;
-  this.shift = false;
-  this.ctrl = false;
-  this.meta = false;
+action.InputState = {};
+
+action.InputState.Key = class extends InputState {
+  constructor() {
+    super();
+    this.pressed = new Set();
+    this.alt = false;
+    this.shift = false;
+    this.ctrl = false;
+    this.meta = false;
+  }
 };
 
-action.KeyInputState.prototype.toString = function() {
-  return '[object KeyInputState]';
+action.InputState.Null = class extends InputState {
+  constructor() {
+    super();
+    this.type = 'none';
+  }
 };
 
-action.PointerInputState = function PointerInputState(subtype, primary) {
-  this.pressed = new Set();
-  this.subtype = subtype;
-  this.primary = primary;
-  this.x = 0;
-  this.y = 0;
-};
-
-action.PointerInputState.prototype.toString = function() {
-  return '[object PointerInputState]';
-};
-
-const ACTION_INPUT_STATE = {
-  none: action.NullInputState,
-  key: action.KeyInputState,
-  pointer: action.PointerInputState,
+action.InputState.Pointer = class extends InputState {
+  constructor(subtype, primary) {
+    super();
+    this.pressed = new Set();
+    this.subtype = subtype;
+    this.primary = primary;
+    this.x = 0;
+    this.y = 0;
+  }
 };
 
 action.Action = function Action(id, type, subtype) {
   // represents action object for actionByTick
   this.id = id;
   this.type = type;
   this.subtype = subtype;
 };
@@ -102,19 +149,18 @@ action.extractActionChain = function ext
   }
   return actionsByTick;
 };
 
 // action_sequence has a list of actionItems for one input source
 action.processInputSourceActionSequence = function processInputSourceActionSequence(
     actionSequence) {
   let type = actionSequence.type;
-  if (!action.Types.has(type)) {
-    throw new InvalidArgumentError(`Invalid 'actionSequence.type', got: ${type}`);
-  }
+  // used here only to validate 'type' and InputState type
+  let inputSourceState = InputState.fromJson(actionSequence);
   let id = actionSequence.id;
   if (typeof id == 'undefined') {
     id = element.generateUUID();
   } else if (typeof id != 'string') {
     throw new InvalidArgumentError(`Expected 'id' to be a string, got: ${id}`);
   }
   let actionItems = actionSequence.actions;
   if (!Array.isArray(actionItems)) {
@@ -122,23 +168,21 @@ action.processInputSourceActionSequence 
         `Expected 'actionSequence.actions' to be an Array, got: ${actionSequence.actions}`);
   }
 
   let pointerParams;
   if (type === "pointer") {
     pointerParams = action.processPointerParameters(actionSequence.parameters);
   }
 
-  if (action.inputStateMap.has(id) &&
-      !(action.inputStateMap.get(id) instanceof ACTION_INPUT_STATE[type])) {
+  if (action.inputStateMap.has(id) && !action.inputStateMap.get(id).is(inputSourceState)) {
     throw new InvalidArgumentError(
-        `Expected ${id} to be mapped to ${ACTION_INPUT_STATE[type].name}, ` +
+        `Expected ${id} to be mapped to ${inputSourceState}, ` +
         `got: ${action.inputStateMap.get(id)}`);
   }
-
   let actions = [];
   for (let actionItem of actionItems) {
     let act;
     switch(type) {
       case "none":
         act = action.processNullAction(id, actionItem);
         break;
       case "key":
@@ -150,79 +194,63 @@ action.processInputSourceActionSequence 
     }
     actions.push(act);
   }
   return actions;
 };
 
 action.processPointerParameters = function processPointerParameters(parametersData) {
   let pointerParams = {
-    pointerType: "mouse",
+    pointerType: action.PointerType.Mouse,
     primary: true,
   };
   if (typeof parametersData == 'undefined') {
     return pointerParams;
   }
-  let pointerType = parametersData.pointerType;
-  if (typeof pointerType != 'undefined') {
-    let types = ["mouse", "pen", "touch"];
-    if(!types.includes(pointerType)){
-      throw new InvalidArgumentError(
-          `Expected 'pointerType' to be one of ${types}, got: ${pointerType}`);
-    }
-    pointerParams.pointerType = pointerType;
+  if (typeof parametersData.pointerType != 'undefined') {
+    pointerParams.pointerType = action.PointerType.get(parametersData.pointerType);
   }
   let primary = parametersData.primary;
   if (typeof primary != 'undefined') {
     assertBoolean(primary, 'primary');
     pointerParams.primary = primary;
   }
   return pointerParams;
 };
 
 action.processNullAction = function processNullAction(id, actionItem) {
   let subtype = actionItem.type;
-  if (subtype !== "pause") {
-    throw new InvalidArgumentError("Expected 'subtype' to be 'pause', got: " + subtype);
-  }
+  assertIsValidSubtype(subtype, "none");
   let act = new action.Action(id, "none", subtype);
   action.processPauseAction(actionItem, act);
   return act;
 };
 
 action.processKeyAction = function processKeyAction(id, actionItem) {
   let subtype = actionItem.type;
-  let types = ["keyUp", "keyDown", "pause"];
-  if (!types.includes(subtype)){
-    throw new InvalidArgumentError(`Expected 'subtype' to be one of ${Array.from(types)}, ` +
-                                   `got: ${subtype}`);
-  }
+  assertIsValidSubtype(subtype, "key");
   let act = new action.Action(id, "key", subtype);
   if (subtype === "pause"){
     action.processPauseAction(actionItem, act);
     return act;
   }
   let key = actionItem.value
+  // todo countGraphemes?
   // what about key codes like arrow, versus unicode chars?
   if (typeof key != 'string' || (typeof key == 'string' && key.length != 1)) {
     throw new InvalidArgumentError("Expected 'key' to be a single-character String, " +
                                    "got: " + key);
   }
   act.value = key;
   return act;
 };
 
 action.processPointerAction = function processPointerAction(id, pointerParams, actionItem) {
   let subtype = actionItem.type;
-  let types = ["pause", "pointerUp", "pointerDown", "pointerMove", "pointerCancel"];
-  if (!types.includes(subtype)) {
-    throw new InvalidArgumentError(`Expected 'subtype' to be one of ${Array.from(types)}, ` +
-                                   `got ${subtype}`);
-  }
-
+  assertIsValidSubtype(subtype, "pointer");
   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}`);
   }
 
   let act = new action.Action(id, "pointer", subtype);
   if (subtype === "pause") {
@@ -251,21 +279,18 @@ action.processPointerUpDownAction = func
   assertPositiveInteger(actionItem.button, 'button');
   act.button = actionItem.button;
 };
 
 action.processPointerMoveAction = function processPointerMoveAction(actionItem, act) {
   assertPositiveInteger(actionItem.duration, 'duration');
   act.duration = actionItem.duration;
   let webElement = actionItem.element;
-  let isElement = function(el){
-    let properties = Object.getOwnPropertyNames(el);
-    return properties.includes(element.Key) || properties.includes(element.LegacyKey);
-  }
-  if (typeof webElement != "undefined" && !isElement(webElement)) {
+
+  if (typeof webElement != "undefined" && !element.isWebElementReference(webElement)) {
     throw new InvalidArgumentError(
         "Expected 'actionItem.element' to be an Object that " +
         `represents a web element, got: ${webElement}`);
   }
   act.element = webElement;
 
   act.x = actionItem.x;
   if (typeof act.x != "undefined") {
@@ -284,8 +309,21 @@ function assertPositiveInteger(value, na
   }
 }
 
 function assertBoolean(value, name = undefined) {
   if (typeof(value) != "boolean") {
     throw new InvalidArgumentError(`Expected '${name}' to be a boolean, got: ${value}`);
   }
 }
+
+function assertIsValidSubtype(value, name = undefined) {
+  if (!ACTIONS[name].has(value)) {
+    throw new InvalidArgumentError(`Unknown subtype for ${name} action: ${value}`);
+  }
+}
+
+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/element.js
+++ b/testing/marionette/element.js
@@ -634,16 +634,21 @@ element.isCollection = function(seq) {
 
 element.makeWebElement = function(uuid) {
   return {
     [element.Key]: uuid,
     [element.LegacyKey]: uuid,
   };
 };
 
+element.isWebElementReference = function(el){
+  let properties = Object.getOwnPropertyNames(el);
+  return properties.includes(element.Key) || properties.includes(element.LegacyKey);
+};
+
 element.generateUUID = function() {
   let uuid = uuidGen.generateUUID().toString();
   return uuid.substring(1, uuid.length - 1);
 };
 
 /**
  * Convert any web elements in arbitrary objects to DOM elements by
  * looking them up in the seen element store.
--- a/testing/marionette/test_action.js
+++ b/testing/marionette/test_action.js
@@ -6,38 +6,38 @@
 
 const {utils: Cu} = Components;
 
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/action.js");
 
 add_test(function test_defaultPointerParameters() {
-  let defaultParameters = {pointerType: "mouse", primary: true};
+  let defaultParameters = {pointerType: action.PointerType.Mouse, primary: true};
   deepEqual(action.processPointerParameters(), defaultParameters);
   deepEqual(action.processPointerParameters({blah: 'nonsense'}), defaultParameters);
 
   run_next_test();
 });
 
 add_test(function test_processPointerParameters() {
   let check = function(regex, message, args) {
     checkErrors(regex, action.processPointerParameters, args, message);
   }
   let parametersData = {pointerType: "foo"};
 
   let message = `parametersData: ${parametersData}`;
-  check(/Expected 'pointerType' to be one of/, message, [parametersData]);
+  check(/Unknown pointerType/, message, [parametersData]);
   parametersData.pointerType = "pen";
   parametersData.primary = 'a';
   check(/Expected 'primary' to be a boolean/, message, [parametersData]);
 
   parametersData.primary = false;
   deepEqual(action.processPointerParameters(parametersData),
-            {pointerType: "pen", primary: false});
+            {pointerType: action.PointerType.Pen, primary: false});
 
 
   run_next_test();
 });
 
 add_test(function test_processPointerUpDownAction() {
   let actionItem = {};
   for (let d of [-1, 'a']) {
@@ -209,19 +209,19 @@ add_test(function test_processNullAction
 add_test(function test_processActionSubtypeValidation() {
   var actionItem = {};
   var id = "some_id";
   actionItem.type = "dancing";
   let check = function(regex, actionFunc, args=[id, actionItem]) {
     let message = `actionItem.type: ${actionItem.type}; actionFunc: ${actionFunc.name}`;
     checkErrors(regex, actionFunc, args, message);
   };
-  check(/Expected 'subtype' to be 'pause'/, action.processNullAction);
-  check(/Expected 'subtype' to be one of/, action.processKeyAction);
-  check(/Expected 'subtype' to be one of/,
+  check(/Unknown subtype for none action/, action.processNullAction);
+  check(/Unknown subtype for key action/, action.processKeyAction);
+  check(/Unknown subtype for pointer action/,
         action.processPointerAction,
         [id, [], actionItem]);
 
   run_next_test();
 });
 
 add_test(function test_processKeyActionPause() {
   let actionItem = {};
@@ -272,17 +272,17 @@ add_test(function test_processInputSourc
     type: "swim",
     id: "some id",
   };
   let check = function(message, regex) {
     checkErrors(regex, action.processInputSourceActionSequence,
                 [actionSequence], message);
   };
   check(`actionSequence.type: ${actionSequence.type}`,
-        /Invalid 'actionSequence\.type'/);
+        /Unknown action type/);
 
   actionSequence.type = "none";
   actionSequence.id = -1;
   check(`actionSequence.id: ${getTypeString(actionSequence.id)}`,
         /Expected 'id' to be a string/);
 
   actionSequence.id = "some_id";
   actionSequence.actions = -1;
@@ -320,18 +320,18 @@ add_test(function test_processInputSourc
     id: "9",
     actions: [actionItem],
     parameters: {
       pointerType: "pen",
       primary: false,
     },
   };
   let expectedAction = new action.Action(actionSequence.id,
-                                                  actionSequence.type,
-                                                  actionItem.type);
+                                          actionSequence.type,
+                                          actionItem.type);
   expectedAction.pointerType = actionSequence.parameters.pointerType;
   expectedAction.primary = actionSequence.parameters.primary;
   expectedAction.button = actionItem.button;
   let actions = action.processInputSourceActionSequence(actionSequence);
   equal(actions.length, 1);
   deepEqual(actions[0], expectedAction);
   run_next_test();
 });
@@ -342,18 +342,18 @@ add_test(function test_processInputSourc
       value: 'a',
   };
   let actionSequence = {
     type: "key",
     id: "9",
     actions: [actionItem],
   };
   let expectedAction = new action.Action(actionSequence.id,
-                                                  actionSequence.type,
-                                                  actionItem.type);
+                                          actionSequence.type,
+                                          actionItem.type);
   expectedAction.value = actionItem.value;
   let actions = action.processInputSourceActionSequence(actionSequence);
   equal(actions.length, 1);
   deepEqual(actions[0], expectedAction);
   run_next_test();
 });
 
 
@@ -380,17 +380,17 @@ add_test(function test_processInputSourc
     type: "pause",
     duration: 5,
   };
   let actionSequence = {
     type: "pointer",
     id: "1",
     actions: [actionItem],
   };
-  let wrongInputState = new action.NullInputState();
+  let wrongInputState = new action.InputState.Null();
   action.inputStateMap.set(actionSequence.id, wrongInputState);
   checkErrors(/to be mapped to/,
               action.processInputSourceActionSequence,
               [actionSequence],
               `${actionSequence.type} using ${wrongInputState}`);
   action.inputStateMap.clear();
   run_next_test();
 });
@@ -399,17 +399,17 @@ add_test(function test_processPointerAct
   let actionItem = {
     type: "pointerDown",
   };
   let id = 1;
   let parameters = {
     pointerType: "mouse",
     primary: true,
   };
-  let wrongInputState = new action.PointerInputState("pause", true);
+  let wrongInputState = new action.InputState.Pointer("pause", true);
   action.inputStateMap.set(id, wrongInputState)
   checkErrors(/to be mapped to InputState whose subtype is/,
               action.processPointerAction,
               [id, parameters, actionItem],
               `$subtype {actionItem.type} with ${wrongInputState.subtype} in inputState`);
   action.inputStateMap.clear();
   run_next_test();
 });
@@ -491,18 +491,18 @@ add_test(function test_extractActionChai
   };
   let actionsByTick = action.extractActionChain([keyActionSequence, mouseActionSequence]);
   // number of ticks is same as longest action sequence
   equal(keyActionItems.length, actionsByTick.length);
   equal(2, actionsByTick[0].length);
   equal(2, actionsByTick[1].length);
   equal(1, actionsByTick[2].length);
   let expectedAction = new action.Action(keyActionSequence.id,
-                                                  "key",
-                                                  keyActionItems[2].type);
+                                          "key",
+                                          keyActionItems[2].type);
   expectedAction.value = keyActionItems[2].value;
   deepEqual(actionsByTick[2][0], expectedAction);
 
   // one empty action sequence
   actionsByTick = action.extractActionChain([keyActionSequence,
                                                       {type:"none", actions: []}]);
   equal(keyActionItems.length, actionsByTick.length);
   equal(1, actionsByTick[0].length);