Bug 1107706: Part 16: Fix rebase of action chains for chrome space
☠☠ backed out by 4c2c50594967 ☠ ☠
authorAndreas Tolfsen <ato@mozilla.com>
Tue, 24 Mar 2015 15:35:58 +0000
changeset 264226 d026794b4c0b4a95414deaab831da79c27232586
parent 264225 bb481b2d170ac58f77b9c6e43b3b47063fec4160
child 264227 47fa87252df0352a66c4698ceee71e351e8f2c8b
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1107706
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1107706: Part 16: Fix rebase of action chains for chrome space
testing/marionette/actions.js
testing/marionette/driver.js
testing/marionette/listener.js
--- a/testing/marionette/actions.js
+++ b/testing/marionette/actions.js
@@ -1,382 +1,492 @@
 /* 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/. */
 
+this.EXPORTED_SYMBOLS = ["ActionChain"];
+
 /**
  * Functionality for (single finger) action chains.
  */
-this.ActionChain = function (utils, checkForInterrupted) {
-  // For assigning unique ids to all touches
+this.ActionChain = function(utils, checkForInterrupted) {
+  // for assigning unique ids to all touches
   this.nextTouchId = 1000;
-  // Keep track of active Touches
+  // keep track of active Touches
   this.touchIds = {};
   // last touch for each fingerId
   this.lastCoordinates = null;
   this.isTap = false;
   this.scrolling = false;
   // whether to send mouse event
   this.mouseEventsOnly = false;
   this.checkTimer = Components.classes["@mozilla.org/timer;1"]
-                              .createInstance(Components.interfaces.nsITimer);
+      .createInstance(Components.interfaces.nsITimer);
 
-  // Callbacks for command completion.
+  // callbacks for command completion
   this.onSuccess = null;
   this.onError = null;
   if (typeof checkForInterrupted == "function") {
     this.checkForInterrupted = checkForInterrupted;
   } else {
     this.checkForInterrupted = () => {};
   }
 
-  // Determines if we create touch events.
+  // determines if we create touch events
   this.inputSource = null;
 
-  // Test utilities providing some event synthesis code.
+  // test utilities providing some event synthesis code
   this.utils = utils;
-}
+};
 
-ActionChain.prototype = {
+ActionChain.prototype.dispatchActions = function(
+    args,
+    touchId,
+    frame,
+    elementManager,
+    callbacks,
+    touchProvider) {
+  // Some touch events code in the listener needs to do ipc, so we can't
+  // share this code across chrome/content.
+  if (touchProvider) {
+    this.touchProvider = touchProvider;
+  }
 
-  dispatchActions: function (args, touchId, frame, elementManager, callbacks,
-                             touchProvider) {
-    // Some touch events code in the listener needs to do ipc, so we can't
-    // share this code across chrome/content.
-    if (touchProvider) {
-      this.touchProvider = touchProvider;
-    }
+  this.elementManager = elementManager;
+  let commandArray = elementManager.convertWrappedArguments(args, frame);
+  this.onSuccess = callbacks.onSuccess;
+  this.onError = callbacks.onError;
+  this.frame = frame;
+
+  if (touchId == null) {
+    touchId = this.nextTouchId++;
+  }
+
+  if (!frame.document.createTouch) {
+    this.mouseEventsOnly = true;
+  }
+
+  let keyModifiers = {
+    shiftKey: false,
+    ctrlKey: false,
+    altKey: false,
+    metaKey: false
+  };
 
-    this.elementManager = elementManager;
-    let commandArray = elementManager.convertWrappedArguments(args, frame);
-    let {onSuccess, onError} = callbacks;
-    this.onSuccess = onSuccess;
-    this.onError = onError;
-    this.frame = frame;
+  try {
+    this.actions(commandArray, touchId, 0, keyModifiers);
+  } catch (e) {
+    this.onError(e);
+    this.resetValues();
+  }
+};
 
-    if (touchId == null) {
-      touchId = this.nextTouchId++;
-    }
+/**
+ * This function emit mouse event.
+ *
+ * @param {Document} doc
+ *     Current document.
+ * @param {string} type
+ *     Type of event to dispatch.
+ * @param {number} clickCount
+ *     Number of clicks, button notes the mouse button.
+ * @param {number} elClientX
+ *     X coordinate of the mouse relative to the viewport.
+ * @param {number} elClientY
+ *     Y coordinate of the mouse relative to the viewport.
+ * @param {Object} modifiers
+ *     An object of modifier keys present.
+ */
+ActionChain.prototype.emitMouseEvent = function(
+    doc,
+    type,
+    elClientX,
+    elClientY,
+    button,
+    clickCount,
+    modifiers) {
+  if (!this.checkForInterrupted()) {
+    let loggingInfo = "emitting Mouse event of type " + type +
+      " at coordinates (" + elClientX + ", " + elClientY +
+      ") relative to the viewport\n" +
+      " button: " + button + "\n" +
+      " clickCount: " + clickCount + "\n";
+    dump(Date.now() + " Marionette: " + loggingInfo);
 
-    if (!frame.document.createTouch) {
-      this.mouseEventsOnly = true;
+    let win = doc.defaultView;
+    let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+        .getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+    let mods;
+    if (typeof modifiers != "undefined") {
+      mods = this.utils._parseModifiers(modifiers);
+    } else {
+      mods = 0;
     }
 
-    let keyModifiers = {
-      shiftKey: false,
-      ctrlKey: false,
-      altKey: false,
-      metaKey: false
-    };
+    domUtils.sendMouseEvent(
+        type,
+        elClientX,
+        elClientY,
+        button || 0, 
+        clickCount || 1,
+        mods,
+        false,
+        0,
+        this.inputSource);
+  }
+};
 
-    try {
-      this.actions(commandArray, touchId, 0, keyModifiers);
-    } catch (e) {
-      this.onError(e.message, e.code, e.stack);
-      this.resetValues();
-    }
-  },
+/**
+ * Reset any persisted values after a command completes.
+ */
+ActionChain.prototype.resetValues = function() {
+  this.onSuccess = null;
+  this.onError = null;
+  this.frame = null;
+  this.elementManager = null;
+  this.touchProvider = null;
+  this.mouseEventsOnly = false;
+};
 
-  /**
-   * This function emit mouse event
-   *   @param: doc is the current document
-   *           type is the type of event to dispatch
-   *           clickCount is the number of clicks, button notes the mouse button
-   *           elClientX and elClientY are the coordinates of the mouse relative to the viewport
-   *           modifiers is an object of modifier keys present
-   */
-  emitMouseEvent: function (doc, type, elClientX, elClientY, button, clickCount, modifiers) {
-    if (!this.checkForInterrupted()) {
-      let loggingInfo = "emitting Mouse event of type " + type +
-        " at coordinates (" + elClientX + ", " + elClientY +
-        ") relative to the viewport\n" +
-        " button: " + button + "\n" +
-        " clickCount: " + clickCount + "\n";
-      dump(Date.now() + " Marionette: " + loggingInfo);
-      let win = doc.defaultView;
-      let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-        .getInterface(Components.interfaces.nsIDOMWindowUtils);
-      let mods;
-      if (typeof modifiers != "undefined") {
-        mods = this.utils._parseModifiers(modifiers);
-      } else {
-        mods = 0;
-      }
-      domUtils.sendMouseEvent(type, elClientX, elClientY, button || 0, clickCount || 1,
-                              mods, false, 0, this.inputSource);
+/**
+ * Function to emit touch events for each finger. e.g.
+ * finger=[['press', id], ['wait', 5], ['release']] touchId represents
+ * the finger id, i keeps track of the current action of the chain
+ * keyModifiers is an object keeping track keyDown/keyUp pairs through
+ * an action chain.
+ */
+ActionChain.prototype.actions = function(chain, touchId, i, keyModifiers) {
+  if (i == chain.length) {
+    this.onSuccess({value: touchId});
+    this.resetValues();
+    return;
+  }
+
+  let pack = chain[i];
+  let command = pack[0];
+  let el;
+  let c;
+  i++;
+
+  if (["press", "wait", "keyDown", "keyUp", "click"].indexOf(command) == -1) {
+    // if mouseEventsOnly, then touchIds isn't used
+    if (!(touchId in this.touchIds) && !this.mouseEventsOnly) {
+      this.resetValues();
+      throw new WebDriverError("Element has not been pressed");
     }
-  },
-
-  /**
-   * Reset any persisted values after a command completes.
-   */
-  resetValues: function () {
-    this.onSuccess = null;
-    this.onError = null;
-    this.frame = null;
-    this.elementManager = null;
-    this.touchProvider = null;
-    this.mouseEventsOnly = false;
-  },
-
-  /**
-   * Function to emit touch events for each finger. e.g. finger=[['press', id], ['wait', 5], ['release']]
-   * touchId represents the finger id, i keeps track of the current action of the chain
-   * keyModifiers is an object keeping track keyDown/keyUp pairs through an action chain.
-   */
-  actions: function (chain, touchId, i, keyModifiers) {
+  }
 
-    if (i == chain.length) {
-      this.onSuccess({value: touchId});
-      this.resetValues();
-      return;
-    }
-
-    let pack = chain[i];
-    let command = pack[0];
-    let el;
-    let c;
-    i++;
-
-    if (['press', 'wait', 'keyDown', 'keyUp', 'click'].indexOf(command) == -1) {
-      // if mouseEventsOnly, then touchIds isn't used
-      if (!(touchId in this.touchIds) && !this.mouseEventsOnly) {
-        this.onError("Element has not been pressed", 500, null);
-        this.resetValues();
-        return;
-      }
-    }
-
-    switch(command) {
-    case 'keyDown':
+  switch(command) {
+    case "keyDown":
       this.utils.sendKeyDown(pack[1], keyModifiers, this.frame);
       this.actions(chain, touchId, i, keyModifiers);
       break;
-    case 'keyUp':
+
+    case "keyUp":
       this.utils.sendKeyUp(pack[1], keyModifiers, this.frame);
       this.actions(chain, touchId, i, keyModifiers);
       break;
-    case 'click':
+
+    case "click":
       el = this.elementManager.getKnownElement(pack[1], this.frame);
       let button = pack[2];
       let clickCount = pack[3];
       c = this.coordinates(el, null, null);
-      this.mouseTap(el.ownerDocument, c.x, c.y, button, clickCount,
-                    keyModifiers);
+      this.mouseTap(el.ownerDocument, c.x, c.y, button, clickCount, keyModifiers);
       if (button == 2) {
-        this.emitMouseEvent(el.ownerDocument, 'contextmenu', c.x, c.y,
-                            button, clickCount, keyModifiers);
+        this.emitMouseEvent(el.ownerDocument, "contextmenu", c.x, c.y,
+            button, clickCount, keyModifiers);
       }
       this.actions(chain, touchId, i, keyModifiers);
       break;
-    case 'press':
+
+    case "press":
       if (this.lastCoordinates) {
-        this.generateEvents('cancel', this.lastCoordinates[0], this.lastCoordinates[1],
-                            touchId, null, keyModifiers);
-        this.onError("Invalid Command: press cannot follow an active touch event", 500, null);
+        this.generateEvents(
+            "cancel",
+            this.lastCoordinates[0],
+            this.lastCoordinates[1],
+            touchId,
+            null,
+            keyModifiers);
         this.resetValues();
-        return;
+        throw new WebDriverError(
+            "Invalid Command: press cannot follow an active touch event");
       }
+
       // look ahead to check if we're scrolling. Needed for APZ touch dispatching.
       if ((i != chain.length) && (chain[i][0].indexOf('move') !== -1)) {
         this.scrolling = true;
       }
       el = this.elementManager.getKnownElement(pack[1], this.frame);
       c = this.coordinates(el, pack[2], pack[3]);
-      touchId = this.generateEvents('press', c.x, c.y, null, el, keyModifiers);
+      touchId = this.generateEvents("press", c.x, c.y, null, el, keyModifiers);
       this.actions(chain, touchId, i, keyModifiers);
       break;
-    case 'release':
-      this.generateEvents('release', this.lastCoordinates[0], this.lastCoordinates[1],
-                          touchId, null, keyModifiers);
+
+    case "release":
+      this.generateEvents(
+          "release",
+          this.lastCoordinates[0],
+          this.lastCoordinates[1],
+          touchId,
+          null,
+          keyModifiers);
       this.actions(chain, null, i, keyModifiers);
       this.scrolling =  false;
       break;
-    case 'move':
+
+    case "move":
       el = this.elementManager.getKnownElement(pack[1], this.frame);
       c = this.coordinates(el);
-      this.generateEvents('move', c.x, c.y, touchId, null, keyModifiers);
+      this.generateEvents("move", c.x, c.y, touchId, null, keyModifiers);
       this.actions(chain, touchId, i, keyModifiers);
       break;
-    case 'moveByOffset':
-      this.generateEvents('move', this.lastCoordinates[0] + pack[1],
-                          this.lastCoordinates[1] + pack[2],
-                          touchId, null, keyModifiers);
+
+    case "moveByOffset":
+      this.generateEvents(
+          "move",
+          this.lastCoordinates[0] + pack[1],
+          this.lastCoordinates[1] + pack[2],
+          touchId,
+          null,
+          keyModifiers);
       this.actions(chain, touchId, i, keyModifiers);
       break;
-    case 'wait':
-      if (pack[1] != null ) {
-        let time = pack[1]*1000;
+
+    case "wait":
+      if (pack[1] != null) {
+        let time = pack[1] * 1000;
+
         // standard waiting time to fire contextmenu
         let standard = 750;
         try {
           standard = Services.prefs.getIntPref("ui.click_hold_context_menus.delay");
-        }
-        catch (e){}
+        } catch (e) {}
+
         if (time >= standard && this.isTap) {
-          chain.splice(i, 0, ['longPress'], ['wait', (time-standard)/1000]);
+          chain.splice(i, 0, ["longPress"], ["wait", (time - standard) / 1000]);
           time = standard;
         }
-        this.checkTimer.initWithCallback(() => {
-          this.actions(chain, touchId, i, keyModifiers);
-        }, time, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
-      }
-      else {
+        this.checkTimer.initWithCallback(
+            () => this.actions(chain, touchId, i, keyModifiers),
+            time, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+      } else {
         this.actions(chain, touchId, i, keyModifiers);
       }
       break;
-    case 'cancel':
-      this.generateEvents('cancel', this.lastCoordinates[0], this.lastCoordinates[1],
-                          touchId, null, keyModifiers);
+  
+    case "cancel":
+      this.generateEvents(
+          "cancel",
+          this.lastCoordinates[0],
+          this.lastCoordinates[1],
+          touchId,
+          null,
+          keyModifiers);
       this.actions(chain, touchId, i, keyModifiers);
       this.scrolling = false;
       break;
-    case 'longPress':
-      this.generateEvents('contextmenu', this.lastCoordinates[0], this.lastCoordinates[1],
-                          touchId, null, keyModifiers);
+  
+    case "longPress":
+      this.generateEvents(
+          "contextmenu",
+          this.lastCoordinates[0],
+          this.lastCoordinates[1],
+          touchId,
+          null,
+          keyModifiers);
       this.actions(chain, touchId, i, keyModifiers);
       break;
-    }
-  },
+
+  }
+};
 
-  /**
-   * This function generates a pair of coordinates relative to the viewport given a
-   * target element and coordinates relative to that element's top-left corner.
-   * @param 'x', and 'y' are the relative to the target.
-   *        If they are not specified, then the center of the target is used.
-   */
-  coordinates: function (target, x, y) {
-    let box = target.getBoundingClientRect();
-    if (x == null) {
-      x = box.width / 2;
-    }
-    if (y == null) {
-      y = box.height / 2;
-    }
-    let coords = {};
-    coords.x = box.left + x;
-    coords.y = box.top + y;
-    return coords;
-  },
+/**
+ * This function generates a pair of coordinates relative to the viewport given a
+ * target element and coordinates relative to that element's top-left corner.
+ *
+ * @param {DOMElement} target
+ *     The target to calculate coordinates of.
+ * @param {number} x
+ *     X coordinate relative to target.  If unspecified, the centre of
+ *     the target is used.
+ * @param {number} y
+ *     Y coordinate relative to target.  If unspecified, the centre of
+ *     the target is used.
+ */
+ActionChain.prototype.coordinates = function(target, x, y) {
+  let box = target.getBoundingClientRect();
+  if (x == null) {
+    x = box.width / 2;
+  }
+  if (y == null) {
+    y = box.height / 2;
+  }
+  let coords = {};
+  coords.x = box.left + x;
+  coords.y = box.top + y;
+  return coords;
+};
 
-  /**
-   * Given an element and a pair of coordinates, returns an array of the form
-   * [ clientX, clientY, pageX, pageY, screenX, screenY ]
-   */
-  getCoordinateInfo: function (el, corx, cory) {
-    let win = el.ownerDocument.defaultView;
-    return [ corx, // clientX
-             cory, // clientY
-             corx + win.pageXOffset, // pageX
-             cory + win.pageYOffset, // pageY
-             corx + win.mozInnerScreenX, // screenX
-             cory + win.mozInnerScreenY // screenY
-           ];
-  },
+/**
+ * Given an element and a pair of coordinates, returns an array of the
+ * form [clientX, clientY, pageX, pageY, screenX, screenY].
+ */
+ActionChain.prototype.getCoordinateInfo = function(el, corx, cory) {
+  let win = el.ownerDocument.defaultView;
+  return [
+    corx, // clientX
+    cory, // clientY
+    corx + win.pageXOffset, // pageX
+    cory + win.pageYOffset, // pageY
+    corx + win.mozInnerScreenX, // screenX
+    cory + win.mozInnerScreenY // screenY
+  ];
+};
 
-  //x and y are coordinates relative to the viewport
-  generateEvents: function (type, x, y, touchId, target, keyModifiers) {
-    this.lastCoordinates = [x, y];
-    let doc = this.frame.document;
-    switch (type) {
-    case 'tap':
+/**
+ * @param {number} x
+ *     X coordinate of the location to generate the event that is relative
+ *     to the viewport.
+ * @param {number} y
+ *     Y coordinate of the location to generate the event that is relative
+ *     to the viewport.
+ */
+ActionChain.prototype.generateEvents = function(
+    type, x, y, touchId, target, keyModifiers) {
+  this.lastCoordinates = [x, y];
+  let doc = this.frame.document;
+
+  switch (type) {
+    case "tap":
       if (this.mouseEventsOnly) {
-        this.mouseTap(touch.target.ownerDocument, touch.clientX, touch.clientY,
-                      null, null, keyModifiers);
+        this.mouseTap(
+            touch.target.ownerDocument,
+            touch.clientX,
+            touch.clientY,
+            null,
+            null,
+            keyModifiers);
       } else {
         touchId = this.nextTouchId++;
         let touch = this.touchProvider.createATouch(target, x, y, touchId);
-        this.touchProvider.emitTouchEvent('touchstart', touch);
-        this.touchProvider.emitTouchEvent('touchend', touch);
-        this.mouseTap(touch.target.ownerDocument, touch.clientX, touch.clientY,
-                      null, null, keyModifiers);
+        this.touchProvider.emitTouchEvent("touchstart", touch);
+        this.touchProvider.emitTouchEvent("touchend", touch);
+        this.mouseTap(
+            touch.target.ownerDocument,
+            touch.clientX,
+            touch.clientY,
+            null,
+            null,
+            keyModifiers);
       }
       this.lastCoordinates = null;
       break;
-    case 'press':
+
+    case "press":
       this.isTap = true;
       if (this.mouseEventsOnly) {
-        this.emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
-        this.emitMouseEvent(doc, 'mousedown', x, y, null, null, keyModifiers);
-      }
-      else {
+        this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers);
+        this.emitMouseEvent(doc, "mousedown", x, y, null, null, keyModifiers);
+      } else {
         touchId = this.nextTouchId++;
         let touch = this.touchProvider.createATouch(target, x, y, touchId);
-        this.touchProvider.emitTouchEvent('touchstart', touch);
+        this.touchProvider.emitTouchEvent("touchstart", touch);
         this.touchIds[touchId] = touch;
         return touchId;
       }
       break;
-    case 'release':
+
+    case "release":
       if (this.mouseEventsOnly) {
         let [x, y] = this.lastCoordinates;
-        this.emitMouseEvent(doc, 'mouseup', x, y,
-                            null, null, keyModifiers);
-      }
-      else {
+        this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers);
+      } else {
         let touch = this.touchIds[touchId];
         let [x, y] = this.lastCoordinates;
+
         touch = this.touchProvider.createATouch(touch.target, x, y, touchId);
-        this.touchProvider.emitTouchEvent('touchend', touch);
+        this.touchProvider.emitTouchEvent("touchend", touch);
+
         if (this.isTap) {
-          this.mouseTap(touch.target.ownerDocument, touch.clientX, touch.clientY,
-                        null, null, keyModifiers);
+          this.mouseTap(
+              touch.target.ownerDocument,
+              touch.clientX,
+              touch.clientY,
+              null,
+              null,
+              keyModifiers);
         }
         delete this.touchIds[touchId];
       }
+
       this.isTap = false;
       this.lastCoordinates = null;
       break;
-    case 'cancel':
+
+    case "cancel":
       this.isTap = false;
       if (this.mouseEventsOnly) {
         let [x, y] = this.lastCoordinates;
-        this.emitMouseEvent(doc, 'mouseup', x, y,
-                            null, null, keyModifiers);
-      }
-      else {
-        this.touchProvider.emitTouchEvent('touchcancel', this.touchIds[touchId]);
+        this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers);
+      } else {
+        this.touchProvider.emitTouchEvent("touchcancel", this.touchIds[touchId]);
         delete this.touchIds[touchId];
       }
       this.lastCoordinates = null;
       break;
-    case 'move':
+
+    case "move":
       this.isTap = false;
       if (this.mouseEventsOnly) {
-        this.emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
-      }
-      else {
-        let touch = this.touchProvider.createATouch(this.touchIds[touchId].target,
-                                                    x, y, touchId);
+        this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers);
+      } else {
+        let touch = this.touchProvider.createATouch(
+            this.touchIds[touchId].target, x, y, touchId);
         this.touchIds[touchId] = touch;
-        this.touchProvider.emitTouchEvent('touchmove', touch);
+        this.touchProvider.emitTouchEvent("touchmove", touch);
       }
       break;
-    case 'contextmenu':
+
+    case "contextmenu":
       this.isTap = false;
-      let event = this.frame.document.createEvent('MouseEvents');
+      let event = this.frame.document.createEvent("MouseEvents");
       if (this.mouseEventsOnly) {
         target = doc.elementFromPoint(this.lastCoordinates[0], this.lastCoordinates[1]);
-      }
-      else {
+      } else {
         target = this.touchIds[touchId].target;
       }
-      let [ clientX, clientY,
-            pageX, pageY,
-            screenX, screenY ] = this.getCoordinateInfo(target, x, y);
-      event.initMouseEvent('contextmenu', true, true,
-                           target.ownerDocument.defaultView, 1,
-                           screenX, screenY, clientX, clientY,
-                           false, false, false, false, 0, null);
+
+      let [clientX, clientY, pageX, pageY, screenX, screenY] =
+          this.getCoordinateInfo(target, x, y);
+
+      event.initMouseEvent(
+          "contextmenu",
+          true,
+          true,
+          target.ownerDocument.defaultView,
+          1,
+          screenX,
+          screenY,
+          clientX,
+          clientY,
+          false,
+          false,
+          false,
+          false,
+          0,
+          null);
       target.dispatchEvent(event);
       break;
-    default:
-      throw {message:"Unknown event type: " + type, code: 500, stack:null};
-    }
-    this.checkForInterrupted();
-  },
 
-  mouseTap: function (doc, x, y, button, clickCount, keyModifiers) {
-    this.emitMouseEvent(doc, 'mousemove', x, y, button, clickCount, keyModifiers);
-    this.emitMouseEvent(doc, 'mousedown', x, y, button, clickCount, keyModifiers);
-    this.emitMouseEvent(doc, 'mouseup', x, y, button, clickCount, keyModifiers);
-  },
-}
+    default:
+      throw new WebDriverError("Unknown event type: " + type);
+  }
+  this.checkForInterrupted();
+};
+
+ActionChain.prototype.mouseTap = function(doc, x, y, button, count, mod) {
+  this.emitMouseEvent(doc, "mousemove", x, y, button, count, mod);
+  this.emitMouseEvent(doc, "mousedown", x, y, button, count, mod);
+  this.emitMouseEvent(doc, "mouseup", x, y, button, count, mod);
+};
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -17,16 +17,17 @@ Cu.import("resource://gre/modules/Task.j
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 this.DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
 
 XPCOMUtils.defineLazyServiceGetter(
     this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager");
 
+Cu.import("chrome://marionette/content/actions.js");
 Cu.import("chrome://marionette/content/elements.js");
 Cu.import("chrome://marionette/content/emulator.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/modal.js");
 Cu.import("chrome://marionette/content/simpletest.js");
 
 loader.loadSubScript("chrome://marionette/content/common.js");
 
@@ -234,16 +235,17 @@ this.GeckoDriver = function(appName, dev
   this.testName = null;
   this.mozBrowserClose = null;
   this.enabled_security_pref = false;
   this.sandbox = null;
   // frame ID of the current remote frame, used for mozbrowserclose events
   this.oopFrameId = null;
   this.observing = null;
   this._browserIds = new WeakMap();
+  this.actions = new ActionChain(utils);
 
   this.sessionCapabilities = {
     // Mandated capabilities
     "browserName": this.appName,
     "browserVersion": Services.appinfo.version,
     "platformName": Services.appinfo.OS.toUpperCase(),
     "platformVersion": Services.appinfo.platformVersion,
 
@@ -1874,24 +1876,39 @@ GeckoDriver.prototype.singleTap = functi
  * @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.
  */
 GeckoDriver.prototype.actionChain = function(cmd, resp) {
+  let {chain, nextId} = cmd.parameters;
+
   switch (this.context) {
     case Context.CHROME:
-      throw new WebDriverError("Command 'actionChain' is not available in chrome context");
+      if (this.appName != "Firefox") {
+        // be conservative until this has a use case and is established
+        // to work as expected on b2g/fennec
+        throw new WebDriverError(
+            "Command 'actionChain' is not available in chrome context");
+      }
+
+      let cbs = {};
+      cbs.onSuccess = val => resp.value = val;
+      cbs.onError = err => { throw err };
+
+      let win = this.getCurrentWindow();
+      let elm = this.curBrowser.elementManager;
+      this.actions.dispatchActions(chain, nextId, win, elm, cbs);
+      break;
 
     case Context.CONTENT:
       this.addFrameCloseListener("action chain");
-      resp.value = yield this.listener.actionChain(
-        {chain: cmd.parameters.chain, nextId: cmd.parameters.nextId});
+      resp.value = yield this.listener.actionChain({chain: chain, nextId: nextId});
       break;
   }
 };
 
 /**
  * A multi-action chain.
  *
  * @param {Object} value
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -964,18 +964,27 @@ function actionChain(msg) {
   callbacks.onError = (message, code, trace) => {
     sendError(message, code, trace, msg.json.command_id);
   };
 
   let touchProvider = {};
   touchProvider.createATouch = createATouch;
   touchProvider.emitTouchEvent = emitTouchEvent;
 
-  actions.dispatchActions(args, touchId, curFrame, elementManager, callbacks,
-                          touchProvider);
+  try {
+    actions.dispatchActions(
+        args,
+        touchId,
+        curFrame,
+        elementManager,
+        callbacks,
+        touchProvider);
+  } catch (e) {
+    sendError(e.message, e.code, e.stack, command_id);
+  }
 }
 
 /**
  * Function to emit touch events which allow multi touch on the screen
  * @param type represents the type of event, touch represents the current touch,touches are all pending touches
  */
 function emitMultiEvents(type, touch, touches) {
   let target = touch.target;