mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-19 16:25:38 +00:00
Bug 1533786 - [marionette] Add support for the WebDriver Actions API. r=ato
Differential Revision: https://phabricator.services.mozilla.com/D22757 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
6b4ae50927
commit
4b889bad47
@ -6,7 +6,7 @@
|
||||
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,
|
||||
|
@ -6,7 +6,7 @@
|
||||
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,
|
||||
|
@ -15,7 +15,7 @@ import shutil
|
||||
import tempfile
|
||||
|
||||
from marionette_harness import MarionetteTestCase
|
||||
from marionette_driver import Actions
|
||||
from marionette_driver.legacy_actions import Actions
|
||||
from marionette_driver.errors import JavascriptException, ScriptTimeoutException
|
||||
import mozlog.structured
|
||||
from marionette_driver.keys import Keys
|
||||
|
@ -6,43 +6,16 @@ Actions
|
||||
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.
|
||||
: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::
|
||||
|
||||
This can be simulated using an Action::
|
||||
element = marionette.find_element("id", "input")
|
||||
element.click()
|
||||
|
||||
from marionette_driver.marionette import Actions
|
||||
key_chain = self.marionette.actions.sequence("key", "keyboard1")
|
||||
key_chain.send_keys("fooba").pause(100).key_down("r").perform()
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
@ -26,12 +26,6 @@ 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
|
||||
|
@ -24,5 +24,4 @@ from marionette_driver import (
|
||||
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
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
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,
|
||||
|
329
testing/marionette/client/marionette_driver/legacy_actions.py
Normal file
329
testing/marionette/client/marionette_driver/legacy_actions.py
Normal file
@ -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)
|
@ -30,6 +30,168 @@ 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."""
|
||||
|
||||
@ -194,340 +356,6 @@ class HTMLElement(object):
|
||||
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.
|
||||
|
||||
@ -633,6 +461,7 @@ class Marionette(object):
|
||||
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
|
||||
|
@ -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;")
|
@ -8,8 +8,6 @@ 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
|
||||
|
||||
|
||||
@ -21,59 +19,66 @@ 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")
|
||||
|
||||
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(self.mod_key) \
|
||||
.key_down("a") \
|
||||
.key_down("x") \
|
||||
.perform()
|
||||
self.assertEqual(self.key_reporter_value, "")
|
||||
|
||||
def test_input_with_wait(self):
|
||||
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, "")
|
||||
|
@ -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")
|
@ -8,71 +8,29 @@ 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()
|
||||
|
||||
super(BaseMouseAction, self).tearDown()
|
||||
|
||||
@property
|
||||
@ -90,6 +48,67 @@ class BaseMouseAction(MarionetteTestCase):
|
||||
}
|
||||
|
||||
|
||||
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):
|
||||
@ -112,7 +131,7 @@ class TestNonSpecCompliantPointerOrigin(BaseMouseAction):
|
||||
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)
|
||||
@ -126,12 +145,13 @@ class TestNonSpecCompliantPointerOrigin(BaseMouseAction):
|
||||
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;"
|
||||
@ -140,4 +160,4 @@ class TestNonSpecCompliantPointerOrigin(BaseMouseAction):
|
||||
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()
|
||||
|
@ -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")
|
@ -40,9 +40,6 @@ skip-if = appname == 'fennec'
|
||||
[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]
|
||||
@ -100,10 +97,8 @@ skip-if = appname == 'fennec' # Bug 1325738
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user