Skip to content

Commit f65b1d7

Browse files
committed
Add touch and multi touch
1 parent 9b22dbc commit f65b1d7

11 files changed

Lines changed: 1035 additions & 0 deletions

File tree

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,97 @@ self.assertIsNotNone(el)
136136
els = self.driver.find_elements_by_accessibility_id('Animation')
137137
self.assertIsInstance(els, list)
138138
```
139+
140+
141+
### Touch actions
142+
143+
In order to accomodate mobile touch actions, and touch actions involving
144+
multiple pointers, the Selenium 3.0 draft specifies ["touch gestures"](https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html#touch-gestures) and ["multi actions"](https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html#multiactions-1), which build upon the touch actions.
145+
146+
move_to: note that use keyword arguments if no element
147+
148+
The API is built around `TouchAction` objects, which are chains of one or more actions to be performed in a sequence. The actions are:
149+
150+
#### `perform`
151+
152+
The `perform` method sends the chain to the server in order to be enacted. It also empties the action chain, so the object can be reused. It will be at the end of all single action chains, but is unused when writing multi-action chains.
153+
154+
#### `tap`
155+
156+
The `tap` method stands alone, being unable to be chained with other methods. If you need a `tap`-like action that starts a longer chain, use `press`.
157+
158+
It can take either an element with an optional x-y offset, or absolute x-y coordinates for the tap, and an optional count.
159+
160+
```python
161+
el = self.driver.find_element_by_accessibility_id('Animation')
162+
action = TouchAction(self.driver)
163+
action.tap(el).perform()
164+
el = self.driver.find_element_by_accessibility_id('Bouncing Balls')
165+
self.assertIsNotNone(el)
166+
```
167+
168+
#### `press`
169+
170+
#### `long_press`
171+
172+
#### `release`
173+
174+
#### `move_to`
175+
176+
#### `wait`
177+
178+
#### `cancel`
179+
180+
181+
### Multi-touch actions
182+
183+
In addition to chains of actions performed with in a single gesture, it is also possible to perform multiple chains at the same time, to simulate multi-finger actions. This is done through building a `MultiAction` object that comprises a number of individual `TouchAction` objects, one for each "finger".
184+
185+
Given two lists next to each other, we can scroll them independently but simultaneously:
186+
187+
```python
188+
els = self.driver.find_elements_by_tag_name('listView')
189+
a1 = TouchAction()
190+
a1.press(els[0]) \
191+
.move_to(x=10, y=0).move_to(x=10, y=-75).move_to(x=10, y=-600).release()
192+
193+
a2 = TouchAction()
194+
a2.press(els[1]) \
195+
.move_to(x=10, y=10).move_to(x=10, y=-300).move_to(x=10, y=-600).release()
196+
197+
ma = MultiAction(self.driver, els[0])
198+
ma.add(a1, a2)
199+
ma.perform();
200+
```
201+
202+
### Appium-Specific touch actions
203+
204+
There are a small number of operations that mobile testers need to do quite a bit that can be relatively complicated to build using the Touch and Multi-touch Action API. For these we provide some convenience methods in the Appium client.
205+
206+
#### `driver.tap`
207+
208+
This method, on the WedDriver object, allows for tapping with multiple fingers, simply by passing in an array of x-y coordinates to tap.
209+
210+
```python
211+
el = self.driver.find_element_by_name('Touch Paint')
212+
action.tap(el).perform()
213+
214+
# set up array of two coordinates
215+
positions = []
216+
positions.append((100, 200))
217+
positions.append((100, 400))
218+
219+
self.driver.tap(positions)
220+
```
221+
222+
#### `driver.swipe`
223+
224+
Swipe from one point to another point.
225+
226+
#### `driver.zoom`
227+
228+
Zoom in on an element, doing a pinch out operation.
229+
230+
#### `driver.pinch`
231+
232+
Zoom out on an element, doing a pinch in operation.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env python
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
17+
# The Selenium team implemented something like the Multi Action API in the form of
18+
# "action chains" (https://code.google.com/p/selenium/source/browse/py/selenium/webdriver/common/action_chains.py).
19+
# These do not quite work for this situation, and do not allow for ad hoc action
20+
# chaining as the spec requires.
21+
22+
from appium.webdriver.mobilecommand import MobileCommand as Command
23+
24+
import copy
25+
26+
class MultiAction(object):
27+
def __init__(self, driver, element=None):
28+
self._driver = driver
29+
self._element = element
30+
self._touch_actions = []
31+
32+
def add(self, *touch_actions):
33+
"""Add TouchAction objects to the MultiAction, to be performed later.
34+
35+
:Args:
36+
- touch_actions - one or more TouchAction objects describing a chain of actions to be performed by one finger
37+
38+
:Usage:
39+
a1 = TouchAction(driver)
40+
a1.press(el1).move_to(el2).release()
41+
a2 = TouchAction(driver)
42+
a2.press(el2).move_to(el1).release()
43+
44+
MultiAction(driver).add(a1, a2)
45+
"""
46+
for touch_action in touch_actions:
47+
if self._touch_actions == None:
48+
self._touch_actions = []
49+
50+
# deep copy, so that once they are in here, the user can't muck about
51+
self._touch_actions.append(copy.deepcopy(touch_action))
52+
53+
def perform(self):
54+
"""Perform the actions stored in the object.
55+
56+
:Usage:
57+
a1 = TouchAction(driver)
58+
a1.press(el1).move_to(el2).release()
59+
a2 = TouchAction(driver)
60+
a2.press(el2).move_to(el1).release()
61+
62+
MultiAction(driver).add(a1, a2).perform()
63+
"""
64+
self._driver.execute(Command.MULTI_ACTION, self.json_wire_gestures)
65+
66+
# clean up and be ready for the next batch
67+
self._touch_actions = []
68+
69+
return self
70+
71+
72+
@property
73+
def json_wire_gestures(self):
74+
actions = []
75+
for action in self._touch_actions:
76+
actions.append(action.json_wire_gestures)
77+
if self._element != None:
78+
return {'actions': actions, 'elementId': self._element.id}
79+
else:
80+
return {'actions': actions}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env python
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
17+
# The Selenium team implemented a version of the Touch Action API in their code
18+
# (https://code.google.com/p/selenium/source/browse/py/selenium/webdriver/common/touch_actions.py)
19+
# but it is deficient in many ways, and does not work in such a way as to be
20+
# amenable to Appium's use of iOS UIAutomation and Android UIAutomator
21+
# So it is reimplemented here.
22+
#
23+
# Theirs is `TouchActions`. Appium's is `TouchAction`.
24+
25+
from appium.webdriver.mobilecommand import MobileCommand as Command
26+
27+
import copy
28+
29+
class TouchAction(object):
30+
def __init__(self, driver=None):
31+
self._driver = driver
32+
self._actions = []
33+
34+
def tap(self, element=None, x=None, y=None, count=1):
35+
"""Perform a tap action on the element
36+
37+
:Args:
38+
- element - the element to tap
39+
- x - (optional) x coordinate to tap, relative to the top left corner of the element.
40+
- y - (optional) y coordinate. If y is used, x must also be set, and vice versa
41+
42+
:Usage:
43+
"""
44+
opts = self._get_opts(element, x, y)
45+
opts['count'] = count
46+
self._add_action('tap', opts)
47+
48+
return self
49+
50+
def press(self, el=None, x=None, y=None):
51+
"""Begin a chain with a press down action at a particular element or point
52+
"""
53+
self._add_action('press', self._get_opts(el, x, y))
54+
55+
return self
56+
57+
def long_press(self, el=None, x=None, y=None, duration=1000):
58+
"""Begin a chain with a press down that lasts `duration` milliseconds
59+
"""
60+
self._add_action('longPress', self._get_opts(el, x, y))
61+
62+
return self
63+
64+
def wait(self, ms=0):
65+
"""Pause for `ms` milliseconds.
66+
"""
67+
if ms == None:
68+
ms = 0
69+
70+
opts = {}
71+
opts['ms'] = ms
72+
73+
self._add_action('wait', opts)
74+
75+
return self
76+
77+
def move_to(self, el=None, x=None, y=None):
78+
"""Move the pointer from the previous point to the element or point specified
79+
"""
80+
self._add_action('moveTo', self._get_opts(el, x, y))
81+
82+
return self
83+
84+
def release(self):
85+
"""End the action by lifting the pointer off the screen
86+
"""
87+
self._add_action('release', {})
88+
89+
return self
90+
91+
def perform(self):
92+
"""Perform the action by sending the commands to the server to be operated upon
93+
"""
94+
params = {}
95+
params['actions'] = self._actions
96+
self._driver.execute(Command.TOUCH_ACTION, params)
97+
98+
# get rid of actions so the object can be reused
99+
self._actions = []
100+
101+
return self
102+
103+
104+
@property
105+
def json_wire_gestures(self):
106+
gestures = []
107+
for action in self._actions:
108+
gestures.append(copy.deepcopy(action))
109+
return gestures
110+
111+
def _add_action(self, action, options):
112+
gesture = {}
113+
gesture['action'] = action
114+
gesture['options'] = options
115+
self._actions.append(gesture)
116+
117+
def _get_opts(self, element, x, y):
118+
opts = {}
119+
if element != None:
120+
opts['element'] = element.id
121+
122+
# it makes no sense to have x but no y, or vice versa.
123+
if (x == None) | (y == None):
124+
x = None
125+
y = None
126+
opts['x'] = x
127+
opts['y'] = y
128+
129+
return opts
130+

appium/webdriver/mobilecommand.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ class MobileCommand(object):
1616
CONTEXTS = 'getContexts',
1717
GET_CURRENT_CONTEXT = 'getCurrentContext',
1818
SWITCH_TO_CONTEXT = 'switchToContext'
19+
TOUCH_ACTION = 'touchAction'
20+
MULTI_ACTION = 'multiAction'

0 commit comments

Comments
 (0)