Bug 1135846 - Expose marionette's actions code to chrome scope where applicable. r=dburns
authorChris Manchester <cmanchester@mozilla.com>
Thu, 19 Mar 2015 18:41:19 -0700
changeset 265441 b6b20e7ca1189b93d0c68d25c9c159f19fee60f9
parent 265440 03eeae95700809d6bbbee9120ff1f31ef9b1751d
child 265442 c5f83cde557f6b7e03a95363030028116fd1f8f2
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdburns
bugs1135846
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 1135846 - Expose marionette's actions code to chrome scope where applicable. r=dburns
testing/marionette/client/marionette/tests/unit/test_click.py
testing/marionette/client/marionette/tests/unit/test_mouse_action.py
testing/marionette/client/marionette/tests/unit/unit-tests.ini
testing/marionette/jar.mn
testing/marionette/marionette-actions.js
testing/marionette/marionette-listener.js
testing/marionette/marionette-server.js
--- a/testing/marionette/client/marionette/tests/unit/test_click.py
+++ b/testing/marionette/client/marionette/tests/unit/test_click.py
@@ -27,77 +27,8 @@ class TestClick(MarionetteTestCase):
         self.assertEqual(self.marionette.title, "XHTML Test Page")
 
     def test_clicking_an_element_that_is_not_displayed_raises(self):
         test_html = self.marionette.absolute_url('hidden.html')
         self.marionette.navigate(test_html)
 
         with self.assertRaises(ElementNotVisibleException):
             self.marionette.find_element(By.ID, 'child').click()
-
-class TestClickAction(MarionetteTestCase):
-
-    def setUp(self):
-        MarionetteTestCase.setUp(self)
-        if self.marionette.session_capabilities['platformName'] == 'DARWIN':
-            self.mod_key = Keys.META
-        else:
-            self.mod_key = Keys.CONTROL
-        self.action = Actions(self.marionette)
-
-    def test_click_action(self):
-        test_html = self.marionette.absolute_url("test.html")
-        self.marionette.navigate(test_html)
-        link = self.marionette.find_element(By.ID, "mozLink")
-        self.action.click(link).perform()
-        self.assertEqual("Clicked", self.marionette.execute_script("return document.getElementById('mozLink').innerHTML;"))
-
-    def test_clicking_element_out_of_view_succeeds(self):
-        # The action based click doesn't check for visibility.
-        test_html = self.marionette.absolute_url('hidden.html')
-        self.marionette.navigate(test_html)
-        el = self.marionette.find_element(By.ID, 'child')
-        self.action.click(el).perform()
-
-    def test_double_click_action(self):
-        test_html = self.marionette.absolute_url("javascriptPage.html")
-        self.marionette.navigate(test_html)
-        el = self.marionette.find_element(By.ID, 'displayed')
-        # The first click just brings the element into view so text selection
-        # works as expected. (A different test page could be used to isolate
-        # this element and make sure it's always in view)
-        el.click()
-        self.action.double_click(el).perform()
-        el.send_keys(self.mod_key + 'c')
-        rel = self.marionette.find_element("id", "keyReporter")
-        rel.send_keys(self.mod_key + 'v')
-        self.assertEqual(rel.get_attribute('value'), 'Displayed')
-
-    def test_context_click_action(self):
-        test_html = self.marionette.absolute_url("javascriptPage.html")
-        self.marionette.navigate(test_html)
-        click_el = self.marionette.find_element(By.ID, 'resultContainer')
-
-        def context_menu_state():
-            with self.marionette.using_context('chrome'):
-                cm_el = self.marionette.find_element(By.ID, 'contentAreaContextMenu')
-                return cm_el.get_attribute('state')
-
-        self.assertEqual('closed', context_menu_state())
-        self.action.context_click(click_el).perform()
-        self.wait_for_condition(lambda _: context_menu_state() == 'open')
-
-        with self.marionette.using_context('chrome'):
-            (self.marionette.find_element(By.ID, 'main-window')
-                            .send_keys(Keys.ESCAPE))
-        self.wait_for_condition(lambda _: context_menu_state() == 'closed')
-
-    def test_middle_click_action(self):
-        test_html = self.marionette.absolute_url("clicks.html")
-        self.marionette.navigate(test_html)
-
-        self.marionette.find_element(By.ID, "addbuttonlistener").click()
-
-        el = self.marionette.find_element(By.ID, "showbutton")
-        self.action.middle_click(el).perform()
-
-        self.wait_for_condition(
-            lambda _: el.get_attribute('innerHTML') == '1')
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_mouse_action.py
@@ -0,0 +1,117 @@
+# 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/.
+
+from marionette import MarionetteTestCase
+from marionette_driver.marionette import Actions
+from marionette_driver.keys import Keys
+from marionette_driver.by import By
+
+class TestMouseAction(MarionetteTestCase):
+
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        if self.marionette.session_capabilities['platformName'] == 'DARWIN':
+            self.mod_key = Keys.META
+        else:
+            self.mod_key = Keys.CONTROL
+        self.action = Actions(self.marionette)
+
+    def test_click_action(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        link = self.marionette.find_element(By.ID, "mozLink")
+        self.action.click(link).perform()
+        self.assertEqual("Clicked", self.marionette.execute_script("return document.getElementById('mozLink').innerHTML;"))
+
+    def test_clicking_element_out_of_view_succeeds(self):
+        # The action based click doesn't check for visibility.
+        test_html = self.marionette.absolute_url('hidden.html')
+        self.marionette.navigate(test_html)
+        el = self.marionette.find_element(By.ID, 'child')
+        self.action.click(el).perform()
+
+    def test_double_click_action(self):
+        test_html = self.marionette.absolute_url("javascriptPage.html")
+        self.marionette.navigate(test_html)
+        el = self.marionette.find_element(By.ID, 'displayed')
+        # The first click just brings the element into view so text selection
+        # works as expected. (A different test page could be used to isolate
+        # this element and make sure it's always in view)
+        el.click()
+        self.action.double_click(el).perform()
+        el.send_keys(self.mod_key + 'c')
+        rel = self.marionette.find_element("id", "keyReporter")
+        rel.send_keys(self.mod_key + 'v')
+        self.assertEqual(rel.get_attribute('value'), 'Displayed')
+
+    def test_context_click_action(self):
+        test_html = self.marionette.absolute_url("javascriptPage.html")
+        self.marionette.navigate(test_html)
+        click_el = self.marionette.find_element(By.ID, 'resultContainer')
+
+        def context_menu_state():
+            with self.marionette.using_context('chrome'):
+                cm_el = self.marionette.find_element(By.ID, 'contentAreaContextMenu')
+                return cm_el.get_attribute('state')
+
+        self.assertEqual('closed', context_menu_state())
+        self.action.context_click(click_el).perform()
+        self.wait_for_condition(lambda _: context_menu_state() == 'open')
+
+        with self.marionette.using_context('chrome'):
+            (self.marionette.find_element(By.ID, 'main-window')
+                            .send_keys(Keys.ESCAPE))
+        self.wait_for_condition(lambda _: context_menu_state() == 'closed')
+
+    def test_middle_click_action(self):
+        test_html = self.marionette.absolute_url("clicks.html")
+        self.marionette.navigate(test_html)
+
+        self.marionette.find_element(By.ID, "addbuttonlistener").click()
+
+        el = self.marionette.find_element(By.ID, "showbutton")
+        self.action.middle_click(el).perform()
+
+        self.wait_for_condition(
+            lambda _: el.get_attribute('innerHTML') == '1')
+
+    def test_chrome_click(self):
+        self.marionette.navigate("about:blank")
+        data_uri = "data:text/html,<html></html>"
+        with self.marionette.using_context('chrome'):
+            urlbar = self.marionette.find_element(By.ID, "urlbar")
+            urlbar.send_keys(data_uri)
+            go_button = self.marionette.find_element(By.ID, "urlbar-go-button")
+            self.action.click(go_button).perform()
+        self.wait_for_condition(lambda mn: mn.get_url() == data_uri)
+
+    def test_chrome_double_click(self):
+        self.marionette.navigate("about:blank")
+        test_word = "quux"
+        with self.marionette.using_context('chrome'):
+            urlbar = self.marionette.find_element(By.ID, "urlbar")
+            self.assertEqual(urlbar.get_attribute('value'), '')
+
+            urlbar.send_keys(test_word)
+            self.assertEqual(urlbar.get_attribute('value'), test_word)
+            (self.action.double_click(urlbar).perform()
+                        .key_down(self.mod_key)
+                        .key_down('x').perform())
+            self.assertEqual(urlbar.get_attribute('value'), '')
+
+    def test_chrome_context_click_action(self):
+        self.marionette.set_context('chrome')
+        def context_menu_state():
+            cm_el = self.marionette.find_element(By.ID, 'tabContextMenu')
+            return cm_el.get_attribute('state')
+
+        currtab = self.marionette.execute_script("return gBrowser.selectedTab")
+        self.assertEqual('closed', context_menu_state())
+        self.action.context_click(currtab).perform()
+        self.wait_for_condition(lambda _: context_menu_state() == 'open')
+
+        (self.marionette.find_element(By.ID, 'main-window')
+                        .send_keys(Keys.ESCAPE))
+
+        self.wait_for_condition(lambda _: context_menu_state() == 'closed')
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -143,9 +143,11 @@ b2g = false
 [test_set_window_size.py]
 b2g = false
 skip-if = os == "linux" # Bug 1085717
 [test_with_using_context.py]
 
 [test_modal_dialogs.py]
 b2g = false
 [test_key_actions.py]
+[test_mouse_action.py]
+b2g = false
 [test_teardown_context_preserved.py]
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -4,16 +4,17 @@
 
 marionette.jar:
 % content marionette %content/
   content/marionette-server.js      (marionette-server.js)
   content/marionette-listener.js    (marionette-listener.js)
   content/marionette-elements.js    (marionette-elements.js)
   content/marionette-sendkeys.js    (marionette-sendkeys.js)
   content/marionette-common.js      (marionette-common.js)
+  content/marionette-actions.js     (marionette-actions.js)
   content/marionette-simpletest.js  (marionette-simpletest.js)
   content/marionette-frame-manager.js  (marionette-frame-manager.js)
   content/EventUtils.js  (EventUtils.js)
   content/ChromeUtils.js  (ChromeUtils.js)
 #ifdef ENABLE_TESTS
   content/test.xul  (client/marionette/chrome/test.xul)
   content/test2.xul  (client/marionette/chrome/test2.xul)
   content/test_nested_iframe.xul  (client/marionette/chrome/test_nested_iframe.xul)
new file mode 100644
--- /dev/null
+++ b/testing/marionette/marionette-actions.js
@@ -0,0 +1,382 @@
+/* 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/. */
+
+/**
+ * Functionality for (single finger) action chains.
+ */
+this.ActionChain = function (utils, checkForInterrupted) {
+  // For assigning unique ids to all touches
+  this.nextTouchId = 1000;
+  // 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);
+
+  // 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.
+  this.inputSource = null;
+
+  // Test utilities providing some event synthesis code.
+  this.utils = utils;
+}
+
+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;
+    }
+
+    this.elementManager = elementManager;
+    let commandArray = elementManager.convertWrappedArguments(args, frame);
+    let {onSuccess, onError} = callbacks;
+    this.onSuccess = onSuccess;
+    this.onError = 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
+    };
+
+    try {
+      this.actions(commandArray, touchId, 0, keyModifiers);
+    } catch (e) {
+      this.onError(e.message, e.code, e.stack);
+      this.resetValues();
+    }
+  },
+
+  /**
+   * 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);
+    }
+  },
+
+  /**
+   * 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':
+      this.utils.sendKeyDown(pack[1], keyModifiers, this.frame);
+      this.actions(chain, touchId, i, keyModifiers);
+      break;
+    case 'keyUp':
+      this.utils.sendKeyUp(pack[1], keyModifiers, this.frame);
+      this.actions(chain, touchId, i, keyModifiers);
+      break;
+    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);
+      if (button == 2) {
+        this.emitMouseEvent(el.ownerDocument, 'contextmenu', c.x, c.y,
+                            button, clickCount, keyModifiers);
+      }
+      this.actions(chain, touchId, i, keyModifiers);
+      break;
+    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.resetValues();
+        return;
+      }
+      // 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);
+      this.actions(chain, touchId, i, keyModifiers);
+      break;
+    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':
+      el = this.elementManager.getKnownElement(pack[1], this.frame);
+      c = this.coordinates(el);
+      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);
+      this.actions(chain, touchId, i, keyModifiers);
+      break;
+    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){}
+        if (time >= standard && this.isTap) {
+          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.actions(chain, touchId, i, keyModifiers);
+      }
+      break;
+    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);
+      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;
+  },
+
+  /**
+   * 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
+           ];
+  },
+
+  //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':
+      if (this.mouseEventsOnly) {
+        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.lastCoordinates = null;
+      break;
+    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 {
+        touchId = this.nextTouchId++;
+        let touch = this.touchProvider.createATouch(target, x, y, touchId);
+        this.touchProvider.emitTouchEvent('touchstart', touch);
+        this.touchIds[touchId] = touch;
+        return touchId;
+      }
+      break;
+    case 'release':
+      if (this.mouseEventsOnly) {
+        let [x, y] = this.lastCoordinates;
+        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);
+        if (this.isTap) {
+          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':
+      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]);
+        delete this.touchIds[touchId];
+      }
+      this.lastCoordinates = null;
+      break;
+    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.touchIds[touchId] = touch;
+        this.touchProvider.emitTouchEvent('touchmove', touch);
+      }
+      break;
+    case 'contextmenu':
+      this.isTap = false;
+      let event = this.frame.document.createEvent('MouseEvents');
+      if (this.mouseEventsOnly) {
+        target = doc.elementFromPoint(this.lastCoordinates[0], this.lastCoordinates[1]);
+      }
+      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);
+      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);
+  },
+}
--- a/testing/marionette/marionette-listener.js
+++ b/testing/marionette/marionette-listener.js
@@ -8,16 +8,17 @@ let {classes: Cc, interfaces: Ci, utils:
 let uuidGen = Cc["@mozilla.org/uuid-generator;1"]
                 .getService(Ci.nsIUUIDGenerator);
 
 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
                .getService(Ci.mozIJSSubScriptLoader);
 
 loader.loadSubScript("chrome://marionette/content/marionette-simpletest.js");
 loader.loadSubScript("chrome://marionette/content/marionette-common.js");
+loader.loadSubScript("chrome://marionette/content/marionette-actions.js");
 Cu.import("chrome://marionette/content/marionette-elements.js");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 let utils = {};
 utils.window = content;
 // Load Event/ChromeUtils for use with JS scripts:
 loader.loadSubScript("chrome://marionette/content/EventUtils.js", utils);
@@ -35,18 +36,18 @@ let isB2G = false;
 let marionetteTestName;
 let winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor)
                      .getInterface(Ci.nsIDOMWindowUtils);
 let listenerId = null; //unique ID of this listener
 let curFrame = content;
 let previousFrame = null;
 let elementManager = new ElementManager([]);
 let accessibility = new Accessibility();
+let actions = new ActionChain(utils, checkForInterrupted);
 let importedScripts = null;
-let inputSource = null;
 
 // The sandbox we execute test scripts in. Gets lazily created in
 // createExecuteContentSandbox().
 let sandbox;
 
 // the unload handler
 let onunload;
 
@@ -63,27 +64,18 @@ let originalOnError;
 let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 //timer for readystate
 let readyStateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 // timer for navigation commands.
 let navTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 let onDOMContentLoaded;
 // Send move events about this often
 let EVENT_INTERVAL = 30; // milliseconds
-// For assigning unique ids to all touches
-let nextTouchId = 1000;
-//Keep track of active Touches
-let touchIds = {};
 // last touch for each fingerId
 let multiLast = {};
-let lastCoordinates = null;
-let isTap = false;
-let scrolling = false;
-// whether to send mouse event
-let mouseEventsOnly = false;
 
 Cu.import("resource://gre/modules/Log.jsm");
 let logger = Log.repository.getLogger("Marionette");
 logger.info("loaded marionette-listener.js");
 let modalHandler = function() {
   // This gets called on the system app only since it receives the mozbrowserprompt event
   sendSyncMessage("Marionette:switchedToFrame", { frameValue: null, storePrevious: true });
   let isLocal = sendSyncMessage("MarionetteFrame:handleModal", {})[0].value;
@@ -119,17 +111,17 @@ function registerSelf() {
         sendAsyncMessage("Marionette:listenersAttached", {listenerId: id});
       }
     }
   }
 }
 
 function emitTouchEventForIFrame(message) {
   message = message.json;
-  let identifier = nextTouchId;
+  let identifier = actions.nextTouchId;
 
   let domWindowUtils = curFrame.
     QueryInterface(Components.interfaces.nsIInterfaceRequestor).
     getInterface(Components.interfaces.nsIDOMWindowUtils);
   var ratio = domWindowUtils.screenPixelsPerCSSPixel;
 
   var typeForUtils;
   switch (message.type) {
@@ -240,17 +232,17 @@ function newSession(msg) {
   resetValues();
   if (isB2G) {
     readyStateTimer.initWithCallback(waitForReady, 100, Ci.nsITimer.TYPE_ONE_SHOT);
     // We have to set correct mouse event source to MOZ_SOURCE_TOUCH
     // to offer a way for event listeners to differentiate
     // events being the result of a physical mouse action.
     // This is especially important for the touch event shim,
     // in order to prevent creating touch event for these fake mouse events.
-    inputSource = Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH;
+    actions.inputSource = Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH;
   }
 }
 
 /**
  * Puts the current session to sleep, so all listeners are removed except
  * for the 'restart' listener. This is used to keep the content listener
  * alive for reuse in B2G instead of reloading it each time.
  */
@@ -321,17 +313,17 @@ function deleteSession(msg) {
   removeMessageListenerId("Marionette:deleteCookie", deleteCookie);
   if (isB2G) {
     content.removeEventListener("mozbrowsershowmodalprompt", modalHandler, false);
   }
   elementManager.reset();
   // reset frame to the top-most frame
   curFrame = content;
   curFrame.focus();
-  touchIds = {};
+  actions.touchIds = {};
 }
 
 /*
  * Helper methods
  */
 
 /**
  * Generic method to send a message to the server
@@ -373,17 +365,17 @@ function sendError(message, status, trac
 }
 
 /**
  * Clear test values after completion of test
  */
 function resetValues() {
   sandbox = null;
   curFrame = content;
-  mouseEventsOnly = false;
+  actions.mouseEventsOnly = false;
 }
 
 /**
  * Dump a logline to stdout. Prepends logline with a timestamp.
  */
 function dumpLog(logline) {
   dump(Date.now() + " Marionette: " + logline);
 }
@@ -399,16 +391,32 @@ function wasInterrupted() {
     }
     else {
       return false;
     }
   }
   return sendSyncMessage("MarionetteFrame:getInterruptedState", {})[0].value;
 }
 
+function checkForInterrupted() {
+    if (wasInterrupted()) {
+      if (previousFrame) {
+        //if previousFrame is set, then we're in a single process environment
+        cuFrame = actions.frame = previousFrame;
+        previousFrame = null;
+        sandbox = null;
+      }
+      else {
+        //else we're in OOP environment, so we'll switch to the original OOP frame
+        sendSyncMessage("Marionette:switchToModalOrigin");
+      }
+      sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true });
+    }
+}
+
 /*
  * Marionette Methods
  */
 
 /**
  * Returns a content sandbox that can be used by the execute_foo functions.
  */
 function createExecuteContentSandbox(aWindow, timeout) {
@@ -726,17 +734,17 @@ function executeWithCallback(msg, useFin
 function emitTouchEvent(type, touch) {
   if (!wasInterrupted()) {
     let loggingInfo = "emitting Touch event of type " + type + " to element with id: " + touch.target.id + " and tag name: " + touch.target.tagName + " at coordinates (" + touch.clientX + ", " + touch.clientY + ") relative to the viewport";
     dumpLog(loggingInfo);
     var docShell = curFrame.document.defaultView.
                    QueryInterface(Components.interfaces.nsIInterfaceRequestor).
                    getInterface(Components.interfaces.nsIWebNavigation).
                    QueryInterface(Components.interfaces.nsIDocShell);
-    if (docShell.asyncPanZoomEnabled && scrolling) {
+    if (docShell.asyncPanZoomEnabled && actions.scrolling) {
       // if we're in APZ and we're scrolling, we must use injectTouchEvent to dispatch our touchmove events
       let index = sendSyncMessage("MarionetteFrame:getCurrentFrameId");
       // only call emitTouchEventForIFrame if we're inside an iframe.
       if (index != null) {
         sendSyncMessage("Marionette:emitTouchEvent",
           { index: index, type: type, id: touch.identifier,
             clientX: touch.clientX, clientY: touch.clientY,
             screenX: touch.screenX, screenY: touch.screenY,
@@ -754,63 +762,16 @@ function emitTouchEvent(type, touch) {
     marionetteLogObj.clearLogs();
     */
     let domWindowUtils = curFrame.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils);
     domWindowUtils.sendTouchEvent(type, [touch.identifier], [touch.clientX], [touch.clientY], [touch.radiusX], [touch.radiusY], [touch.rotationAngle], [touch.force], 1, 0);
   }
 }
 
 /**
- * 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
- */
-function emitMouseEvent(doc, type, elClientX, elClientY, button, clickCount, modifiers) {
-  if (!wasInterrupted()) {
-    let loggingInfo = "emitting Mouse event of type " + type +
-      " at coordinates (" + elClientX + ", " + elClientY +
-      ") relative to the viewport\n" +
-      " button: " + button + "\n" +
-      " clickCount: " + clickCount + "\n";
-    dumpLog(loggingInfo);
-    /*
-    Disabled per bug 888303
-    marionetteLogObj.log(loggingInfo, "TRACE");
-    sendSyncMessage("Marionette:shareData",
-                    {log: elementManager.wrapValue(marionetteLogObj.getLogs())});
-    marionetteLogObj.clearLogs();
-    */
-    let win = doc.defaultView;
-    let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                      .getInterface(Components.interfaces.nsIDOMWindowUtils);
-    let mods;
-    if (typeof modifiers != "undefined") {
-      mods = utils._parseModifiers(modifiers);
-    } else {
-      mods = 0;
-    }
-    domUtils.sendMouseEvent(type, elClientX, elClientY, button || 0, clickCount || 1,
-                            mods, false, 0, inputSource);
-  }
-}
-
-/**
- * Helper function that perform a mouse tap
- */
-function mousetap(doc, x, y, keyModifiers) {
-  emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
-  emitMouseEvent(doc, 'mousedown', x, y, null, null, keyModifiers);
-  emitMouseEvent(doc, 'mouseup', x, y, null, null, keyModifiers);
-}
-
-
-/**
  * 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.
  */
 function coordinates(target, x, y) {
   let box = target.getBoundingClientRect();
   if (x == null) {
@@ -820,16 +781,17 @@ function coordinates(target, x, y) {
     y = box.height / 2;
   }
   let coords = {};
   coords.x = box.left + x;
   coords.y = box.top + y;
   return coords;
 }
 
+
 /**
  * This function returns true if the given coordinates are in the viewport.
  * @param 'x', and 'y' are the coordinates relative to the target.
  *        If they are not specified, then the center of the target is used.
  */
 function elementInViewport(el, x, y) {
   let c = coordinates(el, x, y);
   let viewPort = {top: curFrame.pageYOffset,
@@ -871,123 +833,16 @@ function checkVisible(el, x, y) {
     }
     else {
       return false;
     }
   }
   return true;
 }
 
-//x and y are coordinates relative to the viewport
-function generateEvents(type, x, y, touchId, target, keyModifiers) {
-  lastCoordinates = [x, y];
-  let doc = curFrame.document;
-  switch (type) {
-    case 'tap':
-      if (mouseEventsOnly) {
-        mousetap(target.ownerDocument, x, y);
-      }
-      else {
-        let touchId = nextTouchId++;
-        let touch = createATouch(target, x, y, touchId);
-        emitTouchEvent('touchstart', touch);
-        emitTouchEvent('touchend', touch);
-        mousetap(target.ownerDocument, x, y);
-      }
-      lastCoordinates = null;
-      break;
-    case 'press':
-      isTap = true;
-      if (mouseEventsOnly) {
-        emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
-        emitMouseEvent(doc, 'mousedown', x, y, null, null, keyModifiers);
-      }
-      else {
-        let touchId = nextTouchId++;
-        let touch = createATouch(target, x, y, touchId);
-        emitTouchEvent('touchstart', touch);
-        touchIds[touchId] = touch;
-        return touchId;
-      }
-      break;
-    case 'release':
-      if (mouseEventsOnly) {
-        emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1],
-                       null, null, keyModifiers);
-      }
-      else {
-        let touch = touchIds[touchId];
-        touch = createATouch(touch.target, lastCoordinates[0], lastCoordinates[1], touchId);
-        emitTouchEvent('touchend', touch);
-        if (isTap) {
-          mousetap(touch.target.ownerDocument, touch.clientX, touch.clientY, keyModifiers);
-        }
-        delete touchIds[touchId];
-      }
-      isTap = false;
-      lastCoordinates = null;
-      break;
-    case 'cancel':
-      isTap = false;
-      if (mouseEventsOnly) {
-        emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1],
-                       null, null, keyModifiers);
-      }
-      else {
-        emitTouchEvent('touchcancel', touchIds[touchId]);
-        delete touchIds[touchId];
-      }
-      lastCoordinates = null;
-      break;
-    case 'move':
-      isTap = false;
-      if (mouseEventsOnly) {
-        emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
-      }
-      else {
-        touch = createATouch(touchIds[touchId].target, x, y, touchId);
-        touchIds[touchId] = touch;
-        emitTouchEvent('touchmove', touch);
-      }
-      break;
-    case 'contextmenu':
-      isTap = false;
-      let event = curFrame.document.createEvent('MouseEvents');
-      if (mouseEventsOnly) {
-        target = doc.elementFromPoint(lastCoordinates[0], lastCoordinates[1]);
-      }
-      else {
-        target = touchIds[touchId].target;
-      }
-      let [ clientX, clientY,
-            pageX, pageY,
-            screenX, screenY ] = 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};
-  }
-  if (wasInterrupted()) {
-    if (previousFrame) {
-      //if previousFrame is set, then we're in a single process environment
-      curFrame = previousFrame;
-      previousFrame = null;
-      sandbox = null;
-    }
-    else {
-      //else we're in OOP environment, so we'll switch to the original OOP frame
-      sendSyncMessage("Marionette:switchToModalOrigin");
-    }
-    sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true });
-  }
-}
 
 /**
  * Function that perform a single tap
  */
 function singleTap(msg) {
   let command_id = msg.json.command_id;
   try {
     let el = elementManager.getKnownElement(msg.json.id, curFrame);
@@ -996,20 +851,26 @@ function singleTap(msg) {
     let visible = checkVisible(el, msg.json.corx, msg.json.cory);
     checkVisibleAccessibility(acc, visible);
     if (!visible) {
       sendError("Element is not currently visible and may not be manipulated", 11, null, command_id);
       return;
     }
     checkActionableAccessibility(acc);
     if (!curFrame.document.createTouch) {
-      mouseEventsOnly = true;
+      actions.mouseEventsOnly = true;
     }
-    c = coordinates(el, msg.json.corx, msg.json.cory);
-    generateEvents('tap', c.x, c.y, null, el);
+    let c = coordinates(el, msg.json.corx, msg.json.cory);
+    if (!actions.mouseEventsOnly) {
+      let touchId = actions.nextTouchId++;
+      let touch = createATouch(el, c.x, c.y, touchId);
+      emitTouchEvent('touchstart', touch);
+      emitTouchEvent('touchend', touch);
+    }
+    actions.mouseTap(el.ownerDocument, c.x, c.y);
     sendOk(msg.json.command_id);
   }
   catch (e) {
     sendError(e.message, e.code, e.stack, msg.json.command_id);
   }
 }
 
 /**
@@ -1065,192 +926,52 @@ function checkActionableAccessibility(ac
     message = 'Element does not have a correct accessibility role ' +
       'and may not be manipulated via the accessibility API';
   } else if (!accessibility.hasValidName(accesible)) {
     message = 'Element is missing an accesible name';
   }
   accessibility.handleErrorMessage(message);
 }
 
-/**
- * Given an element and a pair of coordinates, returns an array of the form
- * [ clientX, clientY, pageX, pageY, screenX, screenY ]
- */
-function getCoordinateInfo(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
-         ];
-}
 
 /**
  * Function to create a touch based on the element
  * corx and cory are relative to the viewport, id is the touchId
  */
 function createATouch(el, corx, cory, touchId) {
   let doc = el.ownerDocument;
   let win = doc.defaultView;
   let [clientX, clientY, pageX, pageY, screenX, screenY] =
-    getCoordinateInfo(el, corx, cory);
+    actions.getCoordinateInfo(el, corx, cory);
   let atouch = doc.createTouch(win, el, touchId, pageX, pageY, screenX, screenY, clientX, clientY);
   return atouch;
 }
 
 /**
- * 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.
- */
-function actions(chain, touchId, command_id, i, keyModifiers) {
-  if (typeof i === "undefined") {
-    i = 0;
-  }
-  if (typeof keyModifiers === "undefined") {
-    keyModifiers = {
-      shiftKey: false,
-      ctrlKey: false,
-      altKey: false,
-      metaKey: false
-    };
-  }
-  if (i == chain.length) {
-    sendResponse({value: touchId}, command_id);
-    return;
-  }
-  let pack = chain[i];
-  let command = pack[0];
-  let el;
-  let c;
-  i++;
-  if (['press', 'wait', 'keyDown', 'keyUp'].indexOf(command) == -1) {
-    //if mouseEventsOnly, then touchIds isn't used
-    if (!(touchId in touchIds) && !mouseEventsOnly) {
-      sendError("Element has not been pressed", 500, null, command_id);
-      return;
-    }
-  }
-  switch(command) {
-    case 'keyDown':
-      utils.sendKeyDown(pack[1], keyModifiers, curFrame);
-      actions(chain, touchId, command_id, i, keyModifiers);
-      break;
-    case 'keyUp':
-      utils.sendKeyUp(pack[1], keyModifiers, curFrame);
-      actions(chain, touchId, command_id, i, keyModifiers);
-      break;
-    case 'click':
-      el = elementManager.getKnownElement(pack[1], curFrame);
-      let button = pack[2];
-      let clickCount = pack[3];
-      c = coordinates(el, null, null);
-      emitMouseEvent(el.ownerDocument, 'mousemove', c.x, c.y, button, clickCount,
-                     keyModifiers);
-      emitMouseEvent(el.ownerDocument, 'mousedown', c.x, c.y, button, clickCount,
-                     keyModifiers);
-      emitMouseEvent(el.ownerDocument, 'mouseup', c.x, c.y, button, clickCount,
-                     keyModifiers);
-      if (button == 2) {
-        emitMouseEvent(el.ownerDocument, 'contextmenu', c.x, c.y, button, clickCount,
-                       keyModifiers);
-      }
-      actions(chain, touchId, command_id, i, keyModifiers);
-      break;
-    case 'press':
-      if (lastCoordinates) {
-        generateEvents('cancel', lastCoordinates[0], lastCoordinates[1],
-                       touchId, null, keyModifiers);
-        sendError("Invalid Command: press cannot follow an active touch event", 500, null, command_id);
-        return;
-      }
-      // look ahead to check if we're scrolling. Needed for APZ touch dispatching.
-      if ((i != chain.length) && (chain[i][0].indexOf('move') !== -1)) {
-        scrolling = true;
-      }
-      el = elementManager.getKnownElement(pack[1], curFrame);
-      c = coordinates(el, pack[2], pack[3]);
-      touchId = generateEvents('press', c.x, c.y, null, el, keyModifiers);
-      actions(chain, touchId, command_id, i, keyModifiers);
-      break;
-    case 'release':
-      generateEvents('release', lastCoordinates[0], lastCoordinates[1],
-                     touchId, null, keyModifiers);
-      actions(chain, null, command_id, i, keyModifiers);
-      scrolling =  false;
-      break;
-    case 'move':
-      el = elementManager.getKnownElement(pack[1], curFrame);
-      c = coordinates(el);
-      generateEvents('move', c.x, c.y, touchId, null, keyModifiers);
-      actions(chain, touchId, command_id, i, keyModifiers);
-      break;
-    case 'moveByOffset':
-      generateEvents('move', lastCoordinates[0] + pack[1], lastCoordinates[1] + pack[2],
-                     touchId, null, keyModifiers);
-      actions(chain, touchId, command_id, i, keyModifiers);
-      break;
-    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){}
-        if (time >= standard && isTap) {
-            chain.splice(i, 0, ['longPress'], ['wait', (time-standard)/1000]);
-            time = standard;
-        }
-        checkTimer.initWithCallback(function() {
-          actions(chain, touchId, command_id, i, keyModifiers);
-        }, time, Ci.nsITimer.TYPE_ONE_SHOT);
-      }
-      else {
-        actions(chain, touchId, command_id, i, keyModifiers);
-      }
-      break;
-    case 'cancel':
-      generateEvents('cancel', lastCoordinates[0], lastCoordinates[1],
-                     touchId, null, keyModifiers);
-      actions(chain, touchId, command_id, i, keyModifiers);
-      scrolling = false;
-      break;
-    case 'longPress':
-      generateEvents('contextmenu', lastCoordinates[0], lastCoordinates[1],
-                     touchId, null, keyModifiers);
-      actions(chain, touchId, command_id, i, keyModifiers);
-      break;
-  }
-}
-
-/**
  * Function to start action chain on one finger
  */
 function actionChain(msg) {
   let command_id = msg.json.command_id;
   let args = msg.json.chain;
   let touchId = msg.json.nextId;
-  try {
-    let commandArray = elementManager.convertWrappedArguments(args, curFrame);
-    // loop the action array [ ['press', id], ['move', id], ['release', id] ]
-    if (touchId == null) {
-      touchId = nextTouchId++;
-    }
-    if (!curFrame.document.createTouch) {
-      mouseEventsOnly = true;
-    }
-    actions(commandArray, touchId, command_id);
-  }
-  catch (e) {
-    sendError(e.message, e.code, e.stack, msg.json.command_id);
-  }
+
+  let callbacks = {};
+  callbacks.onSuccess = (value) => {
+    sendResponse(value, command_id);
+  };
+  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);
 }
 
 /**
  * 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;
--- a/testing/marionette/marionette-server.js
+++ b/testing/marionette/marionette-server.js
@@ -12,16 +12,17 @@ const XUL_NS = "http://www.mozilla.org/k
 Cu.import("resource://gre/modules/Log.jsm");
 let logger = Log.repository.getLogger("Marionette");
 logger.info('marionette-server.js loaded');
 
 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
                .getService(Ci.mozIJSSubScriptLoader);
 loader.loadSubScript("chrome://marionette/content/marionette-simpletest.js");
 loader.loadSubScript("chrome://marionette/content/marionette-common.js");
+loader.loadSubScript("chrome://marionette/content/marionette-actions.js");
 Cu.import("resource://gre/modules/Services.jsm");
 loader.loadSubScript("chrome://marionette/content/marionette-frame-manager.js");
 Cu.import("chrome://marionette/content/marionette-elements.js");
 let utils = {};
 loader.loadSubScript("chrome://marionette/content/EventUtils.js", utils);
 loader.loadSubScript("chrome://marionette/content/ChromeUtils.js", utils);
 loader.loadSubScript("chrome://marionette/content/atoms.js", utils);
 loader.loadSubScript("chrome://marionette/content/marionette-sendkeys.js", utils);
@@ -185,16 +186,17 @@ function MarionetteServerConnection(aPre
     "appBuildId" : Services.appinfo.appBuildID,
     "device": qemu == "1" ? "qemu" : (!device ? "desktop" : device),
     "version": Services.appinfo.version
   };
 
   this.observing = null;
   this._browserIds = new WeakMap();
   this.quitFlags = null;
+  this.actions = new ActionChain(utils);
 }
 
 MarionetteServerConnection.prototype = {
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener,
                                          Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
@@ -1873,28 +1875,46 @@ MarionetteServerConnection.prototype = {
 
   /**
    * actionChain
    *
    * @param object aRequest
    *        'value' represents a nested array: inner array represents each event; outer array represents collection of events
    */
   actionChain: function MDA_actionChain(aRequest) {
-    this.command_id = this.getCommandId();
+    let command_id = this.command_id = this.getCommandId();
+    let chain = aRequest.parameters.chain;
+    let nextId = aRequest.parameters.nextId;
     if (this.context == "chrome") {
-      this.sendError("Command 'actionChain' is not available in chrome context", 500, null, this.command_id);
-    }
-    else {
+      if (appName != 'Firefox') {
+        // Be conservative until this has a use case and is established to work as
+        // expected on b2g/fennec.
+        this.sendError("Command 'actionChain' is not available in chrome context",
+                       500, null, this.command_id);
+      }
+
+      let callbacks = {};
+      callbacks.onSuccess = (value) => {
+        this.sendResponse(value, command_id);
+      };
+      callbacks.onError = (message, code, trace) => {
+        this.sendError(message, code, trace, command_id);
+      };
+
+      let currWin = this.getCurrentWindow();
+      let elementManager = this.curBrowser.elementManager;
+      this.actions.dispatchActions(chain, nextId, currWin, elementManager, callbacks);
+    } else {
       this.addFrameCloseListener("action chain");
       this.sendAsync("actionChain",
                      {
-                       chain: aRequest.parameters.chain,
-                       nextId: aRequest.parameters.nextId
+                       chain: chain,
+                       nextId: nextId
                      },
-                     this.command_id);
+                     command_id);
     }
   },
 
   /**
    * multiAction
    *
    * @param object aRequest
    *        'value' represents a nested array: inner array represents each event;