Bug 1533786 - [marionette] Add support for the WebDriver Actions API. r=ato
☠☠ backed out by daaee58201c2 ☠ ☠
authorHenrik Skupin <mail@hskupin.info>
Mon, 11 Mar 2019 15:26:20 +0000
changeset 521401 e510d3ed595a
parent 521400 6b9b50bc446d
child 521402 e04346181ace
push id10866
push usernerli@mozilla.com
push dateTue, 12 Mar 2019 18:59:09 +0000
treeherdermozilla-beta@445c24a51727 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersato
bugs1533786
milestone67.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 1533786 - [marionette] Add support for the WebDriver Actions API. r=ato Differential Revision: https://phabricator.services.mozilla.com/D22757
layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py
layout/base/tests/marionette/test_accessiblecaret_selection_mode.py
testing/marionette/client/docs/advanced/actions.rst
testing/marionette/client/docs/reference.rst
testing/marionette/client/marionette_driver/__init__.py
testing/marionette/client/marionette_driver/gestures.py
testing/marionette/client/marionette_driver/legacy_actions.py
testing/marionette/client/marionette_driver/marionette.py
testing/marionette/harness/marionette_harness/tests/unit/single_finger_functions.py
testing/marionette/harness/marionette_harness/tests/unit/test_key_actions.py
testing/marionette/harness/marionette_harness/tests/unit/test_legacy_mouse_action.py
testing/marionette/harness/marionette_harness/tests/unit/test_mouse_action.py
testing/marionette/harness/marionette_harness/tests/unit/test_single_finger_desktop.py
testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
--- a/layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py
+++ b/layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py
@@ -1,17 +1,17 @@
 # -*- coding: utf-8 -*-
 # 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/.
 
 import string
 
 from marionette_driver.by import By
-from marionette_driver.marionette import Actions
+from marionette_driver.legacy_actions import Actions
 from marionette_driver.selection import SelectionManager
 from marionette_harness.marionette_test import (
     MarionetteTestCase,
     parameterized,
 )
 
 
 class AccessibleCaretCursorModeTestCase(MarionetteTestCase):
--- a/layout/base/tests/marionette/test_accessiblecaret_selection_mode.py
+++ b/layout/base/tests/marionette/test_accessiblecaret_selection_mode.py
@@ -1,17 +1,17 @@
 # -*- coding: utf-8 -*-
 # 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/.
 
 import re
 
 from marionette_driver.by import By
-from marionette_driver.marionette import Actions
+from marionette_driver.legacy_actions import Actions
 from marionette_driver.selection import SelectionManager
 from marionette_harness.marionette_test import (
     MarionetteTestCase,
     SkipTest,
     parameterized
 )
 
 
--- a/testing/marionette/client/docs/advanced/actions.rst
+++ b/testing/marionette/client/docs/advanced/actions.rst
@@ -1,48 +1,21 @@
 Actions
 =======
 
 .. py:currentmodule:: marionette_driver.marionette
 
 Action Sequences
 ----------------
 
-:class:`Actions` are designed as a way to simulate user input as closely as possible
-on a touch device like a smart phone. A common operation is to tap the screen
-and drag your finger to another part of the screen and lift it off.
-
-This can be simulated using an Action::
-
-    from marionette_driver.marionette import Actions
+:class:`Actions` are designed as a way to simulate user input like a keyboard
+or a pointer device as closely as possible. For multiple interactions an
+action sequence can be used::
 
-    start_element = marionette.find_element('id', 'start')
-    end_element = marionette.find_element('id', 'end')
-
-    action = Actions(marionette)
-    action.press(start_element).wait(1).move(end_element).release()
-    action.perform()
-
-This will simulate pressing an element, waiting for one second, moving the
-finger over to another element and then lifting the finger off the screen. The
-wait is optional in this case, but can be useful for simulating delays typical
-to a users behaviour.
+    element = marionette.find_element("id", "input")
+    element.click()
 
-Multi-Action Sequences
-----------------------
-
-Sometimes it may be necessary to simulate multiple actions at the same time.
-For example a user may be dragging one finger while tapping another. This is
-where :class:`MultiActions` come in. MultiActions are simply a way of combining
-two or more actions together and performing them all at the same time::
-
-    from marionette_driver.marionette import Actions, MultiActions
+    key_chain = self.marionette.actions.sequence("key", "keyboard1")
+    key_chain.send_keys("fooba").pause(100).key_down("r").perform()
 
-    action1 = Actions(marionette)
-    action1.press(start_element).move(end_element).release()
-
-    action2 = Actions(marionette)
-    action2.press(another_element).wait(1).release()
-
-    multi = MultiActions(marionette)
-    multi.add(action1)
-    multi.add(action2)
-    multi.perform()
+This will simulate entering "fooba" into the input field, waiting for 100ms,
+and pressing the key "r". The pause is optional in this case, but can be useful
+for simulating delays typical to a users behaviour.
--- a/testing/marionette/client/docs/reference.rst
+++ b/testing/marionette/client/docs/reference.rst
@@ -21,22 +21,16 @@ DateTimeValue
    :members:
 
 Actions
 -------
 .. py:currentmodule:: marionette_driver.marionette.Actions
 .. autoclass:: marionette_driver.marionette.Actions
    :members:
 
-MultiActions
-------------
-.. py:currentmodule:: marionette_driver.marionette.MultiActions
-.. autoclass:: marionette_driver.marionette.MultiActions
-   :members:
-
 Alert
 -----
 .. py:currentmodule:: marionette_driver.marionette.Alert
 .. autoclass:: marionette_driver.marionette.Alert
    :members:
 
 Wait
 ----
--- a/testing/marionette/client/marionette_driver/__init__.py
+++ b/testing/marionette/client/marionette_driver/__init__.py
@@ -19,10 +19,9 @@ from marionette_driver import (
     localization,
     marionette,
     selection,
     wait,
 )
 from marionette_driver.by import By
 from marionette_driver.date_time_value import DateTimeValue
 from marionette_driver.gestures import smooth_scroll, pinch
-from marionette_driver.marionette import Actions
 from marionette_driver.wait import Wait
--- a/testing/marionette/client/marionette_driver/gestures.py
+++ b/testing/marionette/client/marionette_driver/gestures.py
@@ -1,15 +1,15 @@
 # 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 __future__ import absolute_import
 
-from .marionette import MultiActions, Actions
+from .legacy_actions import MultiActions, Actions
 
 
 def smooth_scroll(marionette_session, start_element, axis, direction,
                   length, increments=None, wait_period=None, scroll_back=None):
     """
         :param axis:  y or x
         :param direction: 0 for positive, and -1 for negative
         :param length: total length of scroll scroll
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/legacy_actions.py
@@ -0,0 +1,329 @@
+# 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 __future__ import absolute_import
+
+from . import errors
+from .marionette import MouseButton
+
+
+class Actions(object):
+    """Represent a set of actions that are executed in a particular order.
+
+    All action methods (press, etc.) return the Actions object itself, to make
+    it easy to create a chain of events.
+
+    Example usage::
+
+        # get html file
+        testAction = marionette.absolute_url("testFool.html")
+        # navigate to the file
+        marionette.navigate(testAction)
+        # find element1 and element2
+        element1 = marionette.find_element(By.ID, "element1")
+        element2 = marionette.find_element(By.ID, "element2")
+        # create action object
+        action = Actions(marionette)
+        # add actions (press, wait, move, release) into the object
+        action.press(element1).wait(5). move(element2).release()
+        # fire all the added events
+        action.perform()
+    """
+
+    def __init__(self, marionette):
+        self.action_chain = []
+        self.marionette = marionette
+        self.current_id = None
+
+    def press(self, element, x=None, y=None):
+        '''
+        Sends a 'touchstart' event to this element.
+
+        If no coordinates are given, it will be targeted at the center of the
+        element. If given, it will be targeted at the (x,y) coordinates
+        relative to the top-left corner of the element.
+
+        :param element: The element to press on.
+        :param x: Optional, x-coordinate to tap, relative to the top-left
+         corner of the element.
+        :param y: Optional, y-coordinate to tap, relative to the top-left
+         corner of the element.
+        '''
+        element = element.id
+        self.action_chain.append(['press', element, x, y])
+        return self
+
+    def release(self):
+        '''
+        Sends a 'touchend' event to this element.
+
+        May only be called if :func:`press` has already be called on this element.
+
+        If press and release are chained without a move action between them,
+        then it will be processed as a 'tap' event, and will dispatch the
+        expected mouse events ('mousemove' (if necessary), 'mousedown',
+        'mouseup', 'mouseclick') after the touch events. If there is a wait
+        period between press and release that will trigger a contextmenu,
+        then the 'contextmenu' menu event will be fired instead of the
+        touch/mouse events.
+        '''
+        self.action_chain.append(['release'])
+        return self
+
+    def move(self, element):
+        '''
+        Sends a 'touchmove' event at the center of the target element.
+
+        :param element: Element to move towards.
+
+        May only be called if :func:`press` has already be called.
+        '''
+        element = element.id
+        self.action_chain.append(['move', element])
+        return self
+
+    def move_by_offset(self, x, y):
+        '''
+        Sends 'touchmove' event to the given x, y coordinates relative to the
+        top-left of the currently touched element.
+
+        May only be called if :func:`press` has already be called.
+
+        :param x: Specifies x-coordinate of move event, relative to the
+         top-left corner of the element.
+        :param y: Specifies y-coordinate of move event, relative to the
+         top-left corner of the element.
+        '''
+        self.action_chain.append(['moveByOffset', x, y])
+        return self
+
+    def wait(self, time=None):
+        '''
+        Waits for specified time period.
+
+        :param time: Time in seconds to wait. If time is None then this has no effect
+                     for a single action chain. If used inside a multi-action chain,
+                     then time being None indicates that we should wait for all other
+                     currently executing actions that are part of the chain to complete.
+        '''
+        self.action_chain.append(['wait', time])
+        return self
+
+    def cancel(self):
+        '''
+        Sends 'touchcancel' event to the target of the original 'touchstart' event.
+
+        May only be called if :func:`press` has already be called.
+        '''
+        self.action_chain.append(['cancel'])
+        return self
+
+    def tap(self, element, x=None, y=None):
+        '''
+        Performs a quick tap on the target element.
+
+        :param element: The element to tap.
+        :param x: Optional, x-coordinate of tap, relative to the top-left
+         corner of the element. If not specified, default to center of
+         element.
+        :param y: Optional, y-coordinate of tap, relative to the top-left
+         corner of the element. If not specified, default to center of
+         element.
+
+        This is equivalent to calling:
+
+        ::
+
+          action.press(element, x, y).release()
+        '''
+        element = element.id
+        self.action_chain.append(['press', element, x, y])
+        self.action_chain.append(['release'])
+        return self
+
+    def double_tap(self, element, x=None, y=None):
+        '''
+        Performs a double tap on the target element.
+
+        :param element: The element to double tap.
+        :param x: Optional, x-coordinate of double tap, relative to the
+         top-left corner of the element.
+        :param y: Optional, y-coordinate of double tap, relative to the
+         top-left corner of the element.
+        '''
+        element = element.id
+        self.action_chain.append(['press', element, x, y])
+        self.action_chain.append(['release'])
+        self.action_chain.append(['press', element, x, y])
+        self.action_chain.append(['release'])
+        return self
+
+    def click(self, element, button=MouseButton.LEFT, count=1):
+        '''
+        Performs a click with additional parameters to allow for double clicking,
+        right click, middle click, etc.
+
+        :param element: The element to click.
+        :param button: The mouse button to click (indexed from 0, left to right).
+        :param count: Optional, the count of clicks to synthesize (for double
+                      click events).
+        '''
+        el = element.id
+        self.action_chain.append(['click', el, button, count])
+        return self
+
+    def context_click(self, element):
+        '''
+        Performs a context click on the specified element.
+
+        :param element: The element to context click.
+        '''
+        return self.click(element, button=MouseButton.RIGHT)
+
+    def middle_click(self, element):
+        '''
+        Performs a middle click on the specified element.
+
+        :param element: The element to middle click.
+        '''
+        return self.click(element, button=MouseButton.MIDDLE)
+
+    def double_click(self, element):
+        '''
+        Performs a double click on the specified element.
+
+        :param element: The element to double click.
+        '''
+        return self.click(element, count=2)
+
+    def flick(self, element, x1, y1, x2, y2, duration=200):
+        '''
+        Performs a flick gesture on the target element.
+
+        :param element: The element to perform the flick gesture on.
+        :param x1: Starting x-coordinate of flick, relative to the top left
+         corner of the element.
+        :param y1: Starting y-coordinate of flick, relative to the top left
+         corner of the element.
+        :param x2: Ending x-coordinate of flick, relative to the top left
+         corner of the element.
+        :param y2: Ending y-coordinate of flick, relative to the top left
+         corner of the element.
+        :param duration: Time needed for the flick gesture for complete (in
+         milliseconds).
+        '''
+        element = element.id
+        elapsed = 0
+        time_increment = 10
+        if time_increment >= duration:
+            time_increment = duration
+        move_x = time_increment*1.0/duration * (x2 - x1)
+        move_y = time_increment*1.0/duration * (y2 - y1)
+        self.action_chain.append(['press', element, x1, y1])
+        while elapsed < duration:
+            elapsed += time_increment
+            self.action_chain.append(['moveByOffset', move_x, move_y])
+            self.action_chain.append(['wait', time_increment/1000])
+        self.action_chain.append(['release'])
+        return self
+
+    def long_press(self, element, time_in_seconds, x=None, y=None):
+        '''
+        Performs a long press gesture on the target element.
+
+        :param element: The element to press.
+        :param time_in_seconds: Time in seconds to wait before releasing the press.
+        :param x: Optional, x-coordinate to tap, relative to the top-left
+         corner of the element.
+        :param y: Optional, y-coordinate to tap, relative to the top-left
+         corner of the element.
+
+        This is equivalent to calling:
+
+        ::
+
+          action.press(element, x, y).wait(time_in_seconds).release()
+
+        '''
+        element = element.id
+        self.action_chain.append(['press', element, x, y])
+        self.action_chain.append(['wait', time_in_seconds])
+        self.action_chain.append(['release'])
+        return self
+
+    def key_down(self, key_code):
+        """
+        Perform a "keyDown" action for the given key code. Modifier keys are
+        respected by the server for the course of an action chain.
+
+        :param key_code: The key to press as a result of this action.
+        """
+        self.action_chain.append(['keyDown', key_code])
+        return self
+
+    def key_up(self, key_code):
+        """
+        Perform a "keyUp" action for the given key code. Modifier keys are
+        respected by the server for the course of an action chain.
+
+        :param key_up: The key to release as a result of this action.
+        """
+        self.action_chain.append(['keyUp', key_code])
+        return self
+
+    def perform(self):
+        """Sends the action chain built so far to the server side for
+        execution and clears the current chain of actions."""
+        body = {"chain": self.action_chain, "nextId": self.current_id}
+        try:
+            self.current_id = self.marionette._send_message("Marionette:ActionChain",
+                                                            body, key="value")
+        except errors.UnknownCommandException:
+            self.current_id = self.marionette._send_message("actionChain",
+                                                            body, key="value")
+        self.action_chain = []
+        return self
+
+
+class MultiActions(object):
+    """Represent a sequence of actions that may be performed at the same time.
+
+    Its intent is to allow the simulation of multi-touch gestures.
+
+    Usage example::
+
+      # create multiaction object
+      multitouch = MultiActions(marionette)
+      # create several action objects
+      action_1 = Actions(marionette)
+      action_2 = Actions(marionette)
+      # add actions to each action object/finger
+      action_1.press(element1).move_to(element2).release()
+      action_2.press(element3).wait().release(element3)
+      # fire all the added events
+      multitouch.add(action_1).add(action_2).perform()
+    """
+
+    def __init__(self, marionette):
+        self.multi_actions = []
+        self.max_length = 0
+        self.marionette = marionette
+
+    def add(self, action):
+        """Add a set of actions to perform.
+
+        :param action: An Actions object.
+        """
+        self.multi_actions.append(action.action_chain)
+        if len(action.action_chain) > self.max_length:
+            self.max_length = len(action.action_chain)
+        return self
+
+    def perform(self):
+        """Perform all the actions added to this object."""
+        body = {"value": self.multi_actions, "max_length": self.max_length}
+        try:
+            self.marionette._send_message("Marionette:MultiAction", body)
+        except errors.UnknownCommandException:
+            self.marionette._send_message("multiAction", body)
--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -25,16 +25,178 @@ from .keys import Keys
 from .timeout import Timeouts
 
 CHROME_ELEMENT_KEY = "chromeelement-9fc5-4b51-a3c8-01716eedeb04"
 FRAME_KEY = "frame-075b-4da1-b6ba-e579c2d3230a"
 WEB_ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf"
 WINDOW_KEY = "window-fcc6-11e5-b4f8-330a88ab9d7f"
 
 
+class MouseButton(object):
+    """Enum-like class for mouse button constants."""
+    LEFT = 0
+    MIDDLE = 1
+    RIGHT = 2
+
+
+class ActionSequence(object):
+    r"""API for creating and performing action sequences.
+
+    Each action method adds one or more actions to a queue. When perform()
+    is called, the queued actions fire in order.
+
+    May be chained together as in::
+
+         ActionSequence(self.marionette, "key", id) \
+            .key_down("a") \
+            .key_up("a") \
+            .perform()
+    """
+
+    def __init__(self, marionette, action_type, input_id, pointer_params=None):
+        self.marionette = marionette
+        self._actions = []
+        self._id = input_id
+        self._pointer_params = pointer_params
+        self._type = action_type
+
+    @property
+    def dict(self):
+        d = {
+            "type": self._type,
+            "id": self._id,
+            "actions": self._actions,
+        }
+        if self._pointer_params is not None:
+            d["parameters"] = self._pointer_params
+        return d
+
+    def perform(self):
+        """Perform all queued actions."""
+        self.marionette.actions.perform([self.dict])
+
+    def _key_action(self, subtype, value):
+        self._actions.append({"type": subtype, "value": value})
+
+    def _pointer_action(self, subtype, button):
+        self._actions.append({"type": subtype, "button": button})
+
+    def pause(self, duration):
+        self._actions.append({"type": "pause", "duration": duration})
+        return self
+
+    def pointer_move(self, x, y, duration=None, origin=None):
+        """Queue a pointerMove action.
+
+        :param x: Destination x-axis coordinate of pointer in CSS pixels.
+        :param y: Destination y-axis coordinate of pointer in CSS pixels.
+        :param duration: Number of milliseconds over which to distribute the
+                         move. If None, remote end defaults to 0.
+        :param origin: Origin of coordinates, either "viewport", "pointer" or
+                       an Element. If None, remote end defaults to "viewport".
+        """
+        action = {
+            "type": "pointerMove",
+            "x": x,
+            "y": y
+        }
+        if duration is not None:
+            action["duration"] = duration
+        if origin is not None:
+            if isinstance(origin, HTMLElement):
+                action["origin"] = {WEB_ELEMENT_KEY: origin.id}
+            else:
+                action["origin"] = origin
+        self._actions.append(action)
+        return self
+
+    def pointer_up(self, button=MouseButton.LEFT):
+        """Queue a pointerUp action for `button`.
+
+        :param button: Pointer button to perform action with.
+                       Default: 0, which represents main device button.
+        """
+        self._pointer_action("pointerUp", button)
+        return self
+
+    def pointer_down(self, button=MouseButton.LEFT):
+        """Queue a pointerDown action for `button`.
+
+        :param button: Pointer button to perform action with.
+                       Default: 0, which represents main device button.
+        """
+        self._pointer_action("pointerDown", button)
+        return self
+
+    def click(self, element=None, button=MouseButton.LEFT):
+        """Queue a click with the specified button.
+
+        If an element is given, move the pointer to that element first,
+        otherwise click current pointer coordinates.
+
+        :param element: Optional element to click.
+        :param button: Integer representing pointer button to perform action
+                       with. Default: 0, which represents main device button.
+        """
+        if element:
+            self.pointer_move(0, 0, origin=element)
+        return self.pointer_down(button).pointer_up(button)
+
+    def key_down(self, value):
+        """Queue a keyDown action for `value`.
+
+        :param value: Single character to perform key action with.
+        """
+        self._key_action("keyDown", value)
+        return self
+
+    def key_up(self, value):
+        """Queue a keyUp action for `value`.
+
+        :param value: Single character to perform key action with.
+        """
+        self._key_action("keyUp", value)
+        return self
+
+    def send_keys(self, keys):
+        """Queue a keyDown and keyUp action for each character in `keys`.
+
+        :param keys: String of keys to perform key actions with.
+        """
+        for c in keys:
+            self.key_down(c)
+            self.key_up(c)
+        return self
+
+
+class Actions(object):
+    def __init__(self, marionette):
+        self.marionette = marionette
+
+    def perform(self, actions=None):
+        """Perform actions by tick from each action sequence in `actions`.
+
+        :param actions: List of input source action sequences. A single action
+                        sequence may be created with the help of
+                        ``ActionSequence.dict``.
+        """
+        body = {"actions": [] if actions is None else actions}
+        return self.marionette._send_message("WebDriver:PerformActions", body)
+
+    def release(self):
+        return self.marionette._send_message("WebDriver:ReleaseActions")
+
+    def sequence(self, *args, **kwargs):
+        """Return an empty ActionSequence of the designated type.
+
+        See ActionSequence for parameter list.
+        """
+        return ActionSequence(self.marionette, *args, **kwargs)
+
+
 class HTMLElement(object):
     """Represents a DOM Element."""
 
     identifiers = (CHROME_ELEMENT_KEY, FRAME_KEY, WINDOW_KEY, WEB_ELEMENT_KEY)
 
     def __init__(self, marionette, id):
         self.marionette = marionette
         assert(id is not None)
@@ -189,350 +351,16 @@ class HTMLElement(object):
                 return cls(marionette, json[CHROME_ELEMENT_KEY])
             elif FRAME_KEY in json:
                 return cls(marionette, json[FRAME_KEY])
             elif WINDOW_KEY in json:
                 return cls(marionette, json[WINDOW_KEY])
         raise ValueError("Unrecognised web element")
 
 
-class MouseButton(object):
-    """Enum-like class for mouse button constants."""
-    LEFT = 0
-    MIDDLE = 1
-    RIGHT = 2
-
-
-class Actions(object):
-    '''
-    An Action object represents a set of actions that are executed in a particular order.
-
-    All action methods (press, etc.) return the Actions object itself, to make
-    it easy to create a chain of events.
-
-    Example usage:
-
-    ::
-
-        # get html file
-        testAction = marionette.absolute_url("testFool.html")
-        # navigate to the file
-        marionette.navigate(testAction)
-        # find element1 and element2
-        element1 = marionette.find_element(By.ID, "element1")
-        element2 = marionette.find_element(By.ID, "element2")
-        # create action object
-        action = Actions(marionette)
-        # add actions (press, wait, move, release) into the object
-        action.press(element1).wait(5). move(element2).release()
-        # fire all the added events
-        action.perform()
-    '''
-
-    def __init__(self, marionette):
-        self.action_chain = []
-        self.marionette = marionette
-        self.current_id = None
-
-    def press(self, element, x=None, y=None):
-        '''
-        Sends a 'touchstart' event to this element.
-
-        If no coordinates are given, it will be targeted at the center of the
-        element. If given, it will be targeted at the (x,y) coordinates
-        relative to the top-left corner of the element.
-
-        :param element: The element to press on.
-        :param x: Optional, x-coordinate to tap, relative to the top-left
-         corner of the element.
-        :param y: Optional, y-coordinate to tap, relative to the top-left
-         corner of the element.
-        '''
-        element = element.id
-        self.action_chain.append(['press', element, x, y])
-        return self
-
-    def release(self):
-        '''
-        Sends a 'touchend' event to this element.
-
-        May only be called if :func:`press` has already be called on this element.
-
-        If press and release are chained without a move action between them,
-        then it will be processed as a 'tap' event, and will dispatch the
-        expected mouse events ('mousemove' (if necessary), 'mousedown',
-        'mouseup', 'mouseclick') after the touch events. If there is a wait
-        period between press and release that will trigger a contextmenu,
-        then the 'contextmenu' menu event will be fired instead of the
-        touch/mouse events.
-        '''
-        self.action_chain.append(['release'])
-        return self
-
-    def move(self, element):
-        '''
-        Sends a 'touchmove' event at the center of the target element.
-
-        :param element: Element to move towards.
-
-        May only be called if :func:`press` has already be called.
-        '''
-        element = element.id
-        self.action_chain.append(['move', element])
-        return self
-
-    def move_by_offset(self, x, y):
-        '''
-        Sends 'touchmove' event to the given x, y coordinates relative to the
-        top-left of the currently touched element.
-
-        May only be called if :func:`press` has already be called.
-
-        :param x: Specifies x-coordinate of move event, relative to the
-         top-left corner of the element.
-        :param y: Specifies y-coordinate of move event, relative to the
-         top-left corner of the element.
-        '''
-        self.action_chain.append(['moveByOffset', x, y])
-        return self
-
-    def wait(self, time=None):
-        '''
-        Waits for specified time period.
-
-        :param time: Time in seconds to wait. If time is None then this has no effect
-                     for a single action chain. If used inside a multi-action chain,
-                     then time being None indicates that we should wait for all other
-                     currently executing actions that are part of the chain to complete.
-        '''
-        self.action_chain.append(['wait', time])
-        return self
-
-    def cancel(self):
-        '''
-        Sends 'touchcancel' event to the target of the original 'touchstart' event.
-
-        May only be called if :func:`press` has already be called.
-        '''
-        self.action_chain.append(['cancel'])
-        return self
-
-    def tap(self, element, x=None, y=None):
-        '''
-        Performs a quick tap on the target element.
-
-        :param element: The element to tap.
-        :param x: Optional, x-coordinate of tap, relative to the top-left
-         corner of the element. If not specified, default to center of
-         element.
-        :param y: Optional, y-coordinate of tap, relative to the top-left
-         corner of the element. If not specified, default to center of
-         element.
-
-        This is equivalent to calling:
-
-        ::
-
-          action.press(element, x, y).release()
-        '''
-        element = element.id
-        self.action_chain.append(['press', element, x, y])
-        self.action_chain.append(['release'])
-        return self
-
-    def double_tap(self, element, x=None, y=None):
-        '''
-        Performs a double tap on the target element.
-
-        :param element: The element to double tap.
-        :param x: Optional, x-coordinate of double tap, relative to the
-         top-left corner of the element.
-        :param y: Optional, y-coordinate of double tap, relative to the
-         top-left corner of the element.
-        '''
-        element = element.id
-        self.action_chain.append(['press', element, x, y])
-        self.action_chain.append(['release'])
-        self.action_chain.append(['press', element, x, y])
-        self.action_chain.append(['release'])
-        return self
-
-    def click(self, element, button=MouseButton.LEFT, count=1):
-        '''
-        Performs a click with additional parameters to allow for double clicking,
-        right click, middle click, etc.
-
-        :param element: The element to click.
-        :param button: The mouse button to click (indexed from 0, left to right).
-        :param count: Optional, the count of clicks to synthesize (for double
-                      click events).
-        '''
-        el = element.id
-        self.action_chain.append(['click', el, button, count])
-        return self
-
-    def context_click(self, element):
-        '''
-        Performs a context click on the specified element.
-
-        :param element: The element to context click.
-        '''
-        return self.click(element, button=MouseButton.RIGHT)
-
-    def middle_click(self, element):
-        '''
-        Performs a middle click on the specified element.
-
-        :param element: The element to middle click.
-        '''
-        return self.click(element, button=MouseButton.MIDDLE)
-
-    def double_click(self, element):
-        '''
-        Performs a double click on the specified element.
-
-        :param element: The element to double click.
-        '''
-        return self.click(element, count=2)
-
-    def flick(self, element, x1, y1, x2, y2, duration=200):
-        '''
-        Performs a flick gesture on the target element.
-
-        :param element: The element to perform the flick gesture on.
-        :param x1: Starting x-coordinate of flick, relative to the top left
-         corner of the element.
-        :param y1: Starting y-coordinate of flick, relative to the top left
-         corner of the element.
-        :param x2: Ending x-coordinate of flick, relative to the top left
-         corner of the element.
-        :param y2: Ending y-coordinate of flick, relative to the top left
-         corner of the element.
-        :param duration: Time needed for the flick gesture for complete (in
-         milliseconds).
-        '''
-        element = element.id
-        elapsed = 0
-        time_increment = 10
-        if time_increment >= duration:
-            time_increment = duration
-        move_x = time_increment*1.0/duration * (x2 - x1)
-        move_y = time_increment*1.0/duration * (y2 - y1)
-        self.action_chain.append(['press', element, x1, y1])
-        while elapsed < duration:
-            elapsed += time_increment
-            self.action_chain.append(['moveByOffset', move_x, move_y])
-            self.action_chain.append(['wait', time_increment/1000])
-        self.action_chain.append(['release'])
-        return self
-
-    def long_press(self, element, time_in_seconds, x=None, y=None):
-        '''
-        Performs a long press gesture on the target element.
-
-        :param element: The element to press.
-        :param time_in_seconds: Time in seconds to wait before releasing the press.
-        :param x: Optional, x-coordinate to tap, relative to the top-left
-         corner of the element.
-        :param y: Optional, y-coordinate to tap, relative to the top-left
-         corner of the element.
-
-        This is equivalent to calling:
-
-        ::
-
-          action.press(element, x, y).wait(time_in_seconds).release()
-
-        '''
-        element = element.id
-        self.action_chain.append(['press', element, x, y])
-        self.action_chain.append(['wait', time_in_seconds])
-        self.action_chain.append(['release'])
-        return self
-
-    def key_down(self, key_code):
-        """
-        Perform a "keyDown" action for the given key code. Modifier keys are
-        respected by the server for the course of an action chain.
-
-        :param key_code: The key to press as a result of this action.
-        """
-        self.action_chain.append(['keyDown', key_code])
-        return self
-
-    def key_up(self, key_code):
-        """
-        Perform a "keyUp" action for the given key code. Modifier keys are
-        respected by the server for the course of an action chain.
-
-        :param key_up: The key to release as a result of this action.
-        """
-        self.action_chain.append(['keyUp', key_code])
-        return self
-
-    def perform(self):
-        """Sends the action chain built so far to the server side for
-        execution and clears the current chain of actions."""
-        body = {"chain": self.action_chain, "nextId": self.current_id}
-        try:
-            self.current_id = self.marionette._send_message("Marionette:ActionChain",
-                                                            body, key="value")
-        except errors.UnknownCommandException:
-            self.current_id = self.marionette._send_message("actionChain",
-                                                            body, key="value")
-        self.action_chain = []
-        return self
-
-
-class MultiActions(object):
-    '''
-    A MultiActions object represents a sequence of actions that may be
-    performed at the same time. Its intent is to allow the simulation
-    of multi-touch gestures.
-    Usage example:
-
-    ::
-
-      # create multiaction object
-      multitouch = MultiActions(marionette)
-      # create several action objects
-      action_1 = Actions(marionette)
-      action_2 = Actions(marionette)
-      # add actions to each action object/finger
-      action_1.press(element1).move_to(element2).release()
-      action_2.press(element3).wait().release(element3)
-      # fire all the added events
-      multitouch.add(action_1).add(action_2).perform()
-    '''
-
-    def __init__(self, marionette):
-        self.multi_actions = []
-        self.max_length = 0
-        self.marionette = marionette
-
-    def add(self, action):
-        '''
-        Adds a set of actions to perform.
-
-        :param action: An Actions object.
-        '''
-        self.multi_actions.append(action.action_chain)
-        if len(action.action_chain) > self.max_length:
-            self.max_length = len(action.action_chain)
-        return self
-
-    def perform(self):
-        """Perform all the actions added to this object."""
-        body = {"value": self.multi_actions, "max_length": self.max_length}
-        try:
-            self.marionette._send_message("Marionette:MultiAction", body)
-        except errors.UnknownCommandException:
-            self.marionette._send_message("multiAction", body)
-
-
 class Alert(object):
     """A class for interacting with alerts.
 
     ::
 
         Alert(marionette).accept()
         Alert(marionette).dismiss()
     """
@@ -628,16 +456,17 @@ class Marionette(object):
 
         self.shutdown_timeout = self.DEFAULT_SHUTDOWN_TIMEOUT
 
         if self.bin:
             self.instance = GeckoInstance.create(
                 app, host=self.host, port=self.port, bin=self.bin, **instance_args)
             self.start_binary(self.startup_timeout)
 
+        self.actions = Actions(self)
         self.timeout = Timeouts(self)
 
     @property
     def profile_path(self):
         if self.instance and self.instance.profile:
             return self.instance.profile.profile
 
     def start_binary(self, timeout):
deleted file mode 100644
--- a/testing/marionette/harness/marionette_harness/tests/unit/single_finger_functions.py
+++ /dev/null
@@ -1,133 +0,0 @@
-from __future__ import absolute_import
-
-from marionette_driver.marionette import Actions
-from marionette_driver.errors import TimeoutException
-from marionette_driver.by import By
-
-
-def wait_for_condition_else_raise(marionette, wait_for_condition, expected, script):
-    try:
-        wait_for_condition(lambda m: expected in m.execute_script(script))
-    except TimeoutException as e:
-        raise TimeoutException("{0} got {1} instead of {2}".format(
-            e.message, marionette.execute_script(script), expected))
-
-def press_release(marionette, times, wait_for_condition, expected):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    action = Actions(marionette)
-    button = marionette.find_element(By.ID, "button1")
-    action.press(button).release()
-    # Insert wait between each press and release chain.
-    for _ in range(times-1):
-        action.wait(0.1)
-        action.press(button).release()
-    action.perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected, "return document.getElementById('button1').innerHTML;")
-
-def move_element(marionette, wait_for_condition, expected1, expected2):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    ele = marionette.find_element(By.ID, "button1")
-    drop = marionette.find_element(By.ID, "button2")
-    action = Actions(marionette)
-    action.press(ele).move(drop).release()
-    action.perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected1, "return document.getElementById('button1').innerHTML;")
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected2, "return document.getElementById('button2').innerHTML;")
-
-def move_element_offset(marionette, wait_for_condition, expected1, expected2):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    ele = marionette.find_element(By.ID, "button1")
-    action = Actions(marionette)
-    action.press(ele).move_by_offset(0,150).move_by_offset(0, 150).release()
-    action.perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected1, "return document.getElementById('button1').innerHTML;")
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected2, "return document.getElementById('button2').innerHTML;")
-
-def chain(marionette, wait_for_condition, expected1, expected2):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    marionette.timeout.implicit = 15
-    action = Actions(marionette)
-    button1 = marionette.find_element(By.ID, "button1")
-    action.press(button1).perform()
-    button2 = marionette.find_element(By.ID, "delayed")
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected1, "return document.getElementById('button1').innerHTML;")
-    action.move(button2).release().perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected2, "return document.getElementById('delayed').innerHTML;")
-
-def chain_flick(marionette, wait_for_condition, expected1, expected2):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    button = marionette.find_element(By.ID, "button1")
-    action = Actions(marionette)
-    action.flick(button, 0, 0, 0, 200).perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected1,"return document.getElementById('button1').innerHTML;")
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected2,"return document.getElementById('buttonFlick').innerHTML;")
-
-
-def wait(marionette, wait_for_condition, expected):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    action = Actions(marionette)
-    button = marionette.find_element(By.ID, "button1")
-    action.press(button).wait().release().perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected, "return document.getElementById('button1').innerHTML;")
-
-def wait_with_value(marionette, wait_for_condition, expected):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    button = marionette.find_element(By.ID, "button1")
-    action = Actions(marionette)
-    action.press(button).wait(0.01).release()
-    action.perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected, "return document.getElementById('button1').innerHTML;")
-
-def context_menu(marionette, wait_for_condition, expected1, expected2):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    button = marionette.find_element(By.ID, "button1")
-    action = Actions(marionette)
-    action.press(button).wait(5).perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected1, "return document.getElementById('button1').innerHTML;")
-    action.release().perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected2, "return document.getElementById('button1').innerHTML;")
-
-def long_press_action(marionette, wait_for_condition, expected):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    button = marionette.find_element(By.ID, "button1")
-    action = Actions(marionette)
-    action.long_press(button, 5).perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected, "return document.getElementById('button1').innerHTML;")
-
-def long_press_on_xy_action(marionette, wait_for_condition, expected):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    html = marionette.find_element(By.TAG_NAME, "html")
-    button = marionette.find_element(By.ID, "button1")
-    action = Actions(marionette)
-
-    # Press the center of the button with respect to html.
-    x = button.rect['x'] + button.rect['width'] / 2.0
-    y = button.rect['y'] + button.rect['height'] / 2.0
-    action.long_press(html, 5, x, y).perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected, "return document.getElementById('button1').innerHTML;")
-
-def single_tap(marionette, wait_for_condition, expected):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    button = marionette.find_element(By.ID, "button1")
-    action = Actions(marionette)
-    action.tap(button).perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected, "return document.getElementById('button1').innerHTML;")
-
-def double_tap(marionette, wait_for_condition, expected):
-    testAction = marionette.absolute_url("testAction.html")
-    marionette.navigate(testAction)
-    button = marionette.find_element(By.ID, "button1")
-    action = Actions(marionette)
-    action.double_tap(button).perform()
-    wait_for_condition_else_raise(marionette, wait_for_condition, expected, "return document.getElementById('button1').innerHTML;")
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_key_actions.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_key_actions.py
@@ -3,77 +3,82 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 import urllib
 
 from marionette_driver.by import By
 from marionette_driver.keys import Keys
-from marionette_driver.marionette import Actions
-
 from marionette_harness import MarionetteTestCase, WindowManagerMixin
 
 
 def inline(doc):
     return "data:text/html;charset=utf-8,{}".format(urllib.quote(doc))
 
 
 class TestKeyActions(WindowManagerMixin, MarionetteTestCase):
 
     def setUp(self):
         super(TestKeyActions, self).setUp()
+        self.key_chain = self.marionette.actions.sequence("key", "keyboard_id")
+
         if self.marionette.session_capabilities["platformName"] == "mac":
             self.mod_key = Keys.META
         else:
             self.mod_key = Keys.CONTROL
+
         test_html = self.marionette.absolute_url("keyboard.html")
         self.marionette.navigate(test_html)
         self.reporter_element = self.marionette.find_element(By.ID, "keyReporter")
         self.reporter_element.click()
-        self.key_action = Actions(self.marionette)
+
+    def tearDown(self):
+        self.marionette.actions.release()
+
+        super(TestKeyActions, self).tearDown()
 
     @property
     def key_reporter_value(self):
         return self.reporter_element.get_property("value")
 
-    def test_key_action_basic_input(self):
-        self.key_action.key_down("a").key_down("b").key_down("c").perform()
+    def test_basic_input(self):
+        self.key_chain.key_down("a").key_down("b").key_down("c").perform()
         self.assertEqual(self.key_reporter_value, "abc")
 
     def test_upcase_input(self):
-        (self.key_action.key_down(Keys.SHIFT)
-                        .key_down("a")
-                        .key_up(Keys.SHIFT)
-                        .key_down("b")
-                        .key_down("c")
-                        .perform())
+        self.key_chain.key_down(Keys.SHIFT) \
+                      .key_down("a") \
+                      .key_up(Keys.SHIFT) \
+                      .key_down("b") \
+                      .key_down("c") \
+                      .perform()
         self.assertEqual(self.key_reporter_value, "Abc")
 
     def test_replace_input(self):
-        self.key_action.key_down("a").key_down("b").key_down("c").perform()
+        self.key_chain.key_down("a").key_down("b").key_down("c").perform()
         self.assertEqual(self.key_reporter_value, "abc")
-        (self.key_action.key_down(self.mod_key)
-                        .key_down("a")
-                        .key_up(self.mod_key)
-                        .key_down("x")
-                        .perform())
+
+        self.key_chain.key_down(self.mod_key) \
+                      .key_down("a") \
+                      .key_up(self.mod_key) \
+                      .key_down("x") \
+                      .perform()
         self.assertEqual(self.key_reporter_value, "x")
 
     def test_clear_input(self):
-        self.key_action.key_down("a").key_down("b").key_down("c").perform()
-        self.assertEqual(self.key_reporter_value, "abc")
-        (self.key_action.key_down(self.mod_key)
-                        .key_down("a")
-                        .key_down("x")
-                        .perform())
-        self.assertEqual(self.key_reporter_value, "")
-        self.key_action.key_down("a").key_down("b").key_down("c").perform()
+        self.key_chain.key_down("a").key_down("b").key_down("c").perform()
         self.assertEqual(self.key_reporter_value, "abc")
 
+        self.key_chain.key_down(self.mod_key) \
+                      .key_down("a") \
+                      .key_down("x") \
+                      .perform()
+        self.assertEqual(self.key_reporter_value, "")
+
     def test_input_with_wait(self):
-        self.key_action.key_down("a").key_down("b").key_down("c").perform()
-        (self.key_action.key_down(self.mod_key)
-                        .key_down("a")
-                        .wait(.5)
-                        .key_down("x")
-                        .perform())
+        self.key_chain.key_down("a").key_down("b").key_down("c").perform()
+        self.key_chain.key_down(self.mod_key) \
+                      .key_down("a") \
+                      .pause(250) \
+                      .key_down("x") \
+                      .perform()
         self.assertEqual(self.key_reporter_value, "")
deleted file mode 100644
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_legacy_mouse_action.py
+++ /dev/null
@@ -1,139 +0,0 @@
-# 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 __future__ import absolute_import
-
-import urllib
-
-from marionette_driver.by import By
-from marionette_driver.keys import Keys
-from marionette_driver.marionette import Actions
-
-from marionette_harness import MarionetteTestCase
-
-
-def inline(doc):
-    return "data:text/html;charset=utf-8,{}".format(urllib.quote(doc))
-
-
-class BaseLegacyMouseAction(MarionetteTestCase):
-
-    def setUp(self):
-        super(BaseLegacyMouseAction, self).setUp()
-
-        if self.marionette.session_capabilities["platformName"] == "mac":
-            self.mod_key = Keys.META
-        else:
-            self.mod_key = Keys.CONTROL
-
-        self.action = Actions(self.marionette)
-
-
-class TestLegacyMouseAction(BaseLegacyMouseAction):
-
-    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.
-        self.marionette.navigate(inline("""
-            <div style="position:relative;top:200vh;">foo</div>
-        """))
-        el = self.marionette.find_element(By.TAG_NAME, "div")
-        self.action.click(el).perform()
-
-    def test_double_click_action(self):
-        self.marionette.navigate(inline("""
-          <div contenteditable>zyxw</div><input type="text"/>
-        """))
-
-        el = self.marionette.find_element(By.CSS_SELECTOR, "div")
-        self.action.double_click(el).perform()
-        el.send_keys(self.mod_key + "c")
-        rel = self.marionette.find_element(By.CSS_SELECTOR, "input")
-        rel.send_keys(self.mod_key + "v")
-        self.assertEqual("zyxw", rel.get_property("value"))
-
-    def test_context_click_action(self):
-        test_html = self.marionette.absolute_url("clicks.html")
-        self.marionette.navigate(test_html)
-        click_el = self.marionette.find_element(By.ID, "normal")
-
-        def context_menu_state():
-            with self.marionette.using_context("chrome"):
-                cm_el = self.marionette.find_element(By.ID, "contentAreaContextMenu")
-                return cm_el.get_property("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_property("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.execute_script("return gURLBar.goButton")
-            self.action.click(go_button).perform()
-        self.wait_for_condition(lambda mn: mn.get_url() == data_uri)
-
-
-class TestChromeLegacyMouseAction(BaseLegacyMouseAction):
-
-    def setUp(self):
-        super(TestChromeLegacyMouseAction, self).setUp()
-
-        self.marionette.set_context("chrome")
-
-    def test_chrome_double_click(self):
-        test_word = "quux"
-
-        with self.marionette.using_context("content"):
-            self.marionette.navigate("about:blank")
-
-        urlbar = self.marionette.find_element(By.ID, "urlbar")
-        self.assertEqual("", urlbar.get_property("value"))
-
-        urlbar.send_keys(test_word)
-        self.assertEqual(urlbar.get_property("value"), test_word)
-        (self.action.double_click(urlbar).perform()
-                    .key_down(self.mod_key)
-                    .key_down("x").perform())
-        self.assertEqual(urlbar.get_property("value"), "")
-
-    def test_chrome_context_click_action(self):
-        def context_menu_state():
-            cm_el = self.marionette.find_element(By.ID, "tabContextMenu")
-            return cm_el.get_property("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/harness/marionette_harness/tests/unit/test_mouse_action.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_mouse_action.py
@@ -3,81 +3,39 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 import urllib
 
 from marionette_driver import By, errors, Wait
 from marionette_driver.keys import Keys
-from marionette_driver.marionette import WEB_ELEMENT_KEY
 
-from marionette_harness import MarionetteTestCase
+from marionette_harness import MarionetteTestCase, skip_if_mobile
 
 
 def inline(doc):
     return "data:text/html;charset=utf-8,{}".format(urllib.quote(doc))
 
 
-class Actions(object):
-    """Temporary class until Marionette client supports the WebDriver actions."""
-
-    def __init__(self, marionette):
-        self.marionette = marionette
-
-        self.action_chain = []
-
-    def perform(self):
-        params = {"actions": [{
-            "actions": self.action_chain,
-            "id": "mouse",
-            "parameters": {
-                "pointerType": "mouse"
-            },
-            "type": "pointer"
-        }]}
-
-        return self.marionette._send_message("WebDriver:PerformActions", params=params)
-
-    def move(self, element, x=0, y=0, duration=250):
-        self.action_chain.append({
-            "duration": duration,
-            "origin": {WEB_ELEMENT_KEY: element.id},
-            "type": "pointerMove",
-            "x": x,
-            "y": y,
-        })
-
-        return self
-
-    def click(self):
-        self.action_chain.extend([{
-            "button": 0,
-            "type": "pointerDown"
-        }, {
-            "button": 0,
-            "type": "pointerUp"
-        }])
-
-        return self
-
-
 class BaseMouseAction(MarionetteTestCase):
 
     def setUp(self):
         super(BaseMouseAction, self).setUp()
+        self.mouse_chain = self.marionette.actions.sequence(
+            "pointer", "pointer_id", {"pointerType": "mouse"})
 
         if self.marionette.session_capabilities["platformName"] == "mac":
             self.mod_key = Keys.META
         else:
             self.mod_key = Keys.CONTROL
 
-        self.action = Actions(self.marionette)
+    def tearDown(self):
+        self.marionette.actions.release()
 
-    def tearDown(self):
         super(BaseMouseAction, self).tearDown()
 
     @property
     def click_position(self):
         return self.marionette.execute_script("""
           if (window.click_x && window.click_y) {
             return {x: window.click_x, y: window.click_y};
           }
@@ -85,16 +43,77 @@ class BaseMouseAction(MarionetteTestCase
 
     def get_element_center_point(self, elem):
         return {
             "x": elem.rect["x"] + elem.rect["width"] / 2,
             "y": elem.rect["y"] + elem.rect["height"] / 2
         }
 
 
+class TestPointerActions(BaseMouseAction):
+
+    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.mouse_chain.click(element=link).perform()
+        self.assertEqual("Clicked", self.marionette.execute_script(
+            "return document.getElementById('mozLink').innerHTML"))
+
+    def test_clicking_element_out_of_view(self):
+        self.marionette.navigate(inline("""
+            <div style="position:relative;top:200vh;">foo</div>
+        """))
+        el = self.marionette.find_element(By.TAG_NAME, "div")
+        with self.assertRaises(errors.MoveTargetOutOfBoundsException):
+            self.mouse_chain.click(element=el).perform()
+
+    def test_double_click_action(self):
+        self.marionette.navigate(inline("""
+          <script>window.eventCount = 0;</script>
+          <button onclick="window.eventCount++">foobar</button>
+        """))
+
+        el = self.marionette.find_element(By.CSS_SELECTOR, "button")
+        self.mouse_chain.click(el).pause(100).click(el).perform()
+
+        event_count = self.marionette.execute_script("return window.eventCount", sandbox=None)
+        self.assertEqual(event_count, 2)
+
+    @skip_if_mobile("There is no context menu available on mobile")
+    def test_context_click_action(self):
+        test_html = self.marionette.absolute_url("clicks.html")
+        self.marionette.navigate(test_html)
+        click_el = self.marionette.find_element(By.ID, "normal")
+
+        def context_menu_state():
+            with self.marionette.using_context("chrome"):
+                cm_el = self.marionette.find_element(By.ID, "contentAreaContextMenu")
+                return cm_el.get_property("state")
+
+        self.assertEqual("closed", context_menu_state())
+        self.mouse_chain.click(element=click_el, button=2).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.mouse_chain.click(element=el, button=1).perform()
+
+        self.wait_for_condition(lambda _: el.get_property("innerHTML") == "1")
+
+
 class TestNonSpecCompliantPointerOrigin(BaseMouseAction):
 
     def setUp(self):
         super(TestNonSpecCompliantPointerOrigin, self).setUp()
 
         self.marionette.delete_session()
         self.marionette.start_session({"moz:useNonSpecCompliantPointerOrigin": True})
 
@@ -107,37 +126,38 @@ class TestNonSpecCompliantPointerOrigin(
     def test_click_element_smaller_than_viewport(self):
         self.marionette.navigate(inline("""
           <div id="div" style="width: 10vw; height: 10vh; background: green;"
                onclick="window.click_x = event.clientX; window.click_y = event.clientY" />
         """))
         elem = self.marionette.find_element(By.ID, "div")
         elem_center_point = self.get_element_center_point(elem)
 
-        self.action.move(elem).click().perform()
+        self.mouse_chain.click(element=elem).perform()
         click_position = Wait(self.marionette).until(lambda _: self.click_position,
                                                      message="No click event has been detected")
         self.assertAlmostEqual(click_position["x"], elem_center_point["x"], delta=1)
         self.assertAlmostEqual(click_position["y"], elem_center_point["y"], delta=1)
 
     def test_click_element_larger_than_viewport_with_center_point_inside(self):
         self.marionette.navigate(inline("""
           <div id="div" style="width: 150vw; height: 150vh; background: green;"
                onclick="window.click_x = event.clientX; window.click_y = event.clientY" />
         """))
         elem = self.marionette.find_element(By.ID, "div")
         elem_center_point = self.get_element_center_point(elem)
 
-        self.action.move(elem).click().perform()
+        self.mouse_chain.click(element=elem).perform()
         click_position = Wait(self.marionette).until(lambda _: self.click_position,
                                                      message="No click event has been detected")
         self.assertAlmostEqual(click_position["x"], elem_center_point["x"], delta=1)
         self.assertAlmostEqual(click_position["y"], elem_center_point["y"], delta=1)
 
+    @skip_if_mobile("Bug 1534291 - Missing MoveTargetOutOfBoundsException")
     def test_click_element_larger_than_viewport_with_center_point_outside(self):
         self.marionette.navigate(inline("""
           <div id="div" style="width: 300vw; height: 300vh; background: green;"
                onclick="window.click_x = event.clientX; window.click_y = event.clientY" />
         """))
         elem = self.marionette.find_element(By.ID, "div")
 
         with self.assertRaises(errors.MoveTargetOutOfBoundsException):
-            self.action.move(elem).click().perform()
+            self.mouse_chain.click(element=elem).perform()
deleted file mode 100644
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_single_finger_desktop.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# 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 __future__ import absolute_import
-
-import os
-import sys
-
-from marionette_driver.errors import MarionetteException
-from marionette_driver import Actions, By
-
-from marionette_harness import MarionetteTestCase, skip
-
-# add this directory to the path
-sys.path.append(os.path.dirname(__file__))
-
-from single_finger_functions import (
-    chain, chain_flick, context_menu, double_tap,
-    long_press_action, long_press_on_xy_action,
-    move_element, move_element_offset, press_release, single_tap, wait,
-    wait_with_value
-)
-
-
-class testSingleFingerMouse(MarionetteTestCase):
-    def setUp(self):
-        super(MarionetteTestCase, self).setUp()
-        # set context menu related preferences needed for some tests
-        self.marionette.set_context("chrome")
-        self.enabled = self.marionette.execute_script("""
-let prefs = Components.classes["@mozilla.org/preferences-service;1"]
-                              .getService(Components.interfaces.nsIPrefBranch);
-let value = false;
-try {
-  value = prefs.getBoolPref("ui.click_hold_context_menus");
-}
-catch (e) {}
-prefs.setBoolPref("ui.click_hold_context_menus", true);
-return value;
-""")
-        self.wait_time = self.marionette.execute_script("""
-let prefs = Components.classes["@mozilla.org/preferences-service;1"]
-                              .getService(Components.interfaces.nsIPrefBranch);
-let value = 750;
-try {
-  value = prefs.getIntPref("ui.click_hold_context_menus.delay");
-}
-catch (e) {}
-prefs.setIntPref("ui.click_hold_context_menus.delay", value);
-return value;
-""")
-        self.marionette.set_context("content")
-
-    def tearDown(self):
-        self.marionette.set_context("chrome")
-        self.marionette.execute_script(
-                          """
-let prefs = Components.classes["@mozilla.org/preferences-service;1"]
-                              .getService(Components.interfaces.nsIPrefBranch);
-prefs.setBoolPref("ui.click_hold_context_menus", arguments[0]);
-""", [self.enabled])
-        self.marionette.execute_script(
-                          """
-let prefs = Components.classes["@mozilla.org/preferences-service;1"]
-                              .getService(Components.interfaces.nsIPrefBranch);
-prefs.setIntPref("ui.click_hold_context_menus.delay", arguments[0]);
-""", [self.wait_time])
-        self.marionette.set_context("content")
-        super(MarionetteTestCase, self).tearDown()
-
-    def test_press_release(self):
-        press_release(self.marionette, 1, self.wait_for_condition, "button1-mousemove-mousedown-mouseup-click")
-
-    def test_press_release_twice(self):
-        press_release(self.marionette, 2, self.wait_for_condition, "button1-mousemove-mousedown-mouseup-click-mousemove-mousedown-mouseup-click")
-
-    def test_move_element(self):
-        move_element(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown", "button2-mousemove-mouseup")
-
-    def test_move_by_offset(self):
-        move_element_offset(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown", "button2-mousemove-mouseup")
-
-    def test_wait(self):
-        wait(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown-mouseup-click")
-
-    def test_wait_with_value(self):
-        wait_with_value(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown-mouseup-click")
-
-    @skip("Bug 1191066")
-    def test_context_menu(self):
-        context_menu(self.marionette, self.wait_for_condition,
-                     "button1-mousemove-mousedown-contextmenu",
-                     "button1-mousemove-mousedown-contextmenu-mouseup-click")
-
-    @skip("Bug 1191066")
-    def test_long_press_action(self):
-        long_press_action(self.marionette, self.wait_for_condition,
-                          "button1-mousemove-mousedown-contextmenu-mouseup-click")
-
-    @skip("Bug 1191066")
-    def test_long_press_on_xy_action(self):
-        long_press_on_xy_action(self.marionette, self.wait_for_condition,
-                                "button1-mousemove-mousedown-contextmenu-mouseup-click")
-
-    @skip("Bug 865334")
-    def test_long_press_fail(self):
-        testAction = self.marionette.absolute_url("testAction.html")
-        self.marionette.navigate(testAction)
-        button = self.marionette.find_element(By.ID, "button1Copy")
-        action = Actions(self.marionette)
-        action.press(button).long_press(button, 5)
-        self.assertRaises(MarionetteException, action.perform)
-
-    def test_chain(self):
-        chain(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown", "delayed-mousemove-mouseup")
-
-    def test_chain_flick(self):
-        chain_flick(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown-mousemove", "buttonFlick-mousemove-mouseup")
-
-    def test_single_tap(self):
-        single_tap(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown-mouseup-click")
-
-    def test_double_tap(self):
-        double_tap(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown-mouseup-click-mousemove-mousedown-mouseup-click")
--- a/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
@@ -35,19 +35,16 @@ skip-if = appname == 'fennec'
 [test_element_retrieval.py]
 [test_findelement_chrome.py]
 skip-if = appname == 'fennec'
 
 [test_get_current_url_chrome.py]
 [test_navigation.py]
 [test_timeouts.py]
 
-[test_single_finger_desktop.py]
-skip-if = appname == 'fennec' || os == "win" # Bug 1025040
-
 [test_anonymous_content.py]
 skip-if = appname == 'fennec'
 [test_switch_frame.py]
 [test_switch_frame_chrome.py]
 skip-if = appname == 'fennec'
 [test_switch_window_chrome.py]
 skip-if = appname == 'fennec'
 [test_switch_window_content.py]
@@ -95,20 +92,18 @@ skip-if = manage_instance == false || ap
 [test_context.py]
 
 [test_modal_dialogs.py]
 skip-if = appname == 'fennec' # Bug 1325738
 [test_unhandled_prompt_behavior.py]
 skip-if = appname == 'fennec' # Bug 1325738
 
 [test_key_actions.py]
-[test_legacy_mouse_action.py]
-skip-if = appname == 'fennec'
 [test_mouse_action.py]
-skip-if = appname == 'fennec'
+
 [test_teardown_context_preserved.py]
 [test_file_upload.py]
 skip-if = appname == 'fennec' || os == "win" # http://bugs.python.org/issue14574
 
 [test_execute_sandboxes.py]
 [test_prefs.py]
 [test_prefs_enforce.py]
 skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921