Skip to content

Commit 965ec7b

Browse files
committed
Add multi-touch actions, touch actions.
1 parent d8cbacd commit 965ec7b

File tree

4 files changed

+302
-33
lines changed

4 files changed

+302
-33
lines changed

lib/appium_lib/device/device.rb

+57-33
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def remove(id)
102102
end
103103
end
104104

105-
add_endpoint_method(:is_installed?, 'session/:session_id?/appium/device/app_installed') do
105+
add_endpoint_method(:is_installed?, 'session/:session_id/appium/device/app_installed') do
106106
def is_installed?(app_id)
107107
execute :is_installed?, {}, :bundleId => app_id
108108
end
@@ -160,6 +160,7 @@ def pull_file(path)
160160
end
161161
end
162162

163+
add_touch_actions
163164
extend_search_contexts
164165
end
165166

@@ -176,8 +177,8 @@ def add_endpoint_method(method, path, verb=:post)
176177
end
177178

178179
delegate_driver_method method
179-
delegate_appium_driver_method method
180-
end
180+
delegate_from_appium_driver method
181+
end
181182

182183
# @private
183184
def extend_webdriver_with_forwardable
@@ -194,8 +195,8 @@ def delegate_driver_method(method)
194195
end
195196

196197
# @private
197-
def delegate_appium_driver_method(method)
198-
def_delegator :@driver, method
198+
def delegate_from_appium_driver(method, delegation_target=:driver)
199+
def_delegator delegation_target, method
199200
end
200201

201202
# @private
@@ -246,37 +247,60 @@ def extend_search_contexts
246247
end
247248
end
248249

249-
# Perform a block within the given context, then switch back to the starting context.
250-
# @param context (String) The context to switch to for the duration of the block.
251-
#
252-
# ```ruby
253-
# within_context('NATIVE_APP') do
254-
# find_element [:tag, "button"]
255-
# ```
256-
def within_context(context)
257-
existing_context = current_context
258-
yield if block_given?
259-
current_context = existing_context
260-
end
250+
def add_touch_actions
251+
add_endpoint_method(:touch_actions, 'session/:session_id/touch/perform') do
252+
def touch_actions(actions)
253+
actions = [actions].flatten
254+
execute :touch_actions, {}, actions
255+
end
256+
end
257+
258+
add_endpoint_method(:multi_touch, 'session/:session_id/touch/multi/perform') do
259+
def multi_touch(actions)
260+
execute :multi_touch, {}, actions: actions
261+
end
262+
end
263+
264+
actions = Appium::TouchAction::ACTIONS + Appium::TouchAction::COMPLEX_ACTIONS
265+
actions.each do |method|
266+
delegate_from_appium_driver(method, Appium::TouchAction)
267+
end
261268

262-
# Change to the default context. This is equivalent to `current_context= nil`.
263-
def switch_to_default_context
264-
current_context = nil
269+
delegate_from_appium_driver(:pinch, Appium::MultiTouch)
270+
delegate_from_appium_driver(:zoom, Appium::MultiTouch)
265271
end
272+
end # class << self
273+
274+
# @!method current_context=
275+
# Change the context to the given context.
276+
# @param [String] The context to change to
277+
#
278+
# ```ruby
279+
# current_context= "NATIVE_APP"
280+
# ```
266281

267-
# @!method current_context=
268-
# Change the context to the given context.
269-
# @param [String] The context to change to
270-
#
271-
# ```ruby
272-
# current_context= "NATIVE_APP"
273-
# ```
282+
# @!method current_context
283+
# @return [String] The context currently being used.
274284

275-
# @!method current_context
276-
# @return [String] The context currently being used.
285+
# @!method available_contexts
286+
# @return [Array<String>] All usable contexts, as an array of strings.
277287

278-
# @!method available_contexts
279-
# @return [Array<String>] All usable contexts, as an array of strings
280-
end # class << self
288+
# Perform a block within the given context, then switch back to the starting context.
289+
# @param context (String) The context to switch to for the duration of the block.
290+
#
291+
# ```ruby
292+
# within_context('NATIVE_APP') do
293+
# find_element [:tag, "button"]
294+
# ```
295+
def within_context(context)
296+
existing_context = current_context
297+
yield if block_given?
298+
current_context = existing_context
299+
end
300+
301+
# Change to the default context. This is equivalent to `current_context= nil`.
302+
def switch_to_default_context
303+
current_context = nil
304+
end
281305
end # module Device
282-
end # module Appium
306+
end # module Appium

lib/appium_lib/device/multi_touch.rb

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
module Appium
2+
3+
# MultiTouch actions allow for multiple touches to happen at the same time,
4+
# for instance, to simulate multiple finger swipes.
5+
#
6+
# Create a series of touch actions by themselves (without a `prepare()`), then
7+
# add to a new MultiTouch action. When ready, call `prepare()` and all
8+
# actions will be executed simultaneously.
9+
#
10+
# ```ruby
11+
# action_1 = TouchAction.new.press(x: 45, y: 100).wait(5).release
12+
# action_2 = TouchAction.new.tap(element: el, x: 50, y:5, count: 3).release
13+
#
14+
# multi_touch_action = MultiTouch.new
15+
# multi_touch_action.add action_1
16+
# multi_touch_action.add action_2
17+
# multi_touch_action.perform
18+
class MultiTouch
19+
class << self
20+
21+
# Convenience method for pinching the screen.
22+
# Places two fingers at the edges of the screen and brings them together.
23+
# @param percentage (int) The percent size by which to shrink the screen when pinched.
24+
# @param auto_perform (boolean) Whether to perform the action immediately (default true)
25+
#
26+
# ```ruby
27+
# action = pinch 75 #=> Pinch the screen from the top right and bottom left corners
28+
# action.perform #=> to 25% of its size.
29+
# ```
30+
def pinch(percentage=25, auto_perform=true)
31+
raise ArgumentError("Can't pinch to greater than screen size.") if percentage > 100
32+
33+
p = Float(percentage) / 100
34+
i = 1 - p
35+
36+
top = TouchAction.new
37+
top.swipe start_x: 1.0, start_y: 0.0, end_x: i, end_y: i, duration: 1
38+
39+
bottom = TouchAction.new
40+
bottom.swipe(start_x: 0.0, start_y: 1.0, end_x: p, end_y: p, duration: 1)
41+
42+
pinch = MultiTouch.new
43+
pinch.add top
44+
pinch.add bottom
45+
return pinch unless auto_perform
46+
pinch.perform
47+
end
48+
49+
# Convenience method for zooming the screen.
50+
# Places two fingers at the edges of the screen and brings them together.
51+
# @param percentage (int) The percent size by which to shrink the screen when pinched.
52+
# @param auto_perform (boolean) Whether to perform the action immediately (default true)
53+
#
54+
# ```ruby
55+
# action = zoom 200 #=> Zoom in the screen from the center until it doubles in size.
56+
# action.perform
57+
# ```
58+
def zoom(percentage=200, auto_perform=true)
59+
raise ArgumentError("Can't zoom to smaller then screen size.") if percentage < 100
60+
61+
p = 100 / Float(percentage)
62+
i = 1 - p
63+
64+
top = TouchAction.new
65+
top.swipe start_x: i, start_y: i, end_x: 1, end_y: 1, duration: 1
66+
67+
bottom = TouchAction.new
68+
bottom.swipe start_x: p, start_y: p, end_x: 1, end_y: 1, duration: 1
69+
70+
zoom = MultiTouch.new
71+
zoom.add top
72+
zoom.add bottom
73+
return zoom unless auto_perform
74+
zoom.perform
75+
end
76+
end
77+
78+
# Create a new multi-action
79+
def initialize
80+
@actions = []
81+
end
82+
83+
# Add a touch_action to be performed
84+
# @param chain (TouchAction) The action to add to the chain
85+
def add(chain)
86+
@actions << chain.actions
87+
end
88+
89+
# Ask Appium to perform the actions
90+
def perform
91+
$driver.multi_touch @actions
92+
end
93+
end
94+
end
+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
module Appium
2+
3+
# Perform a series of gestures, one after another. Gestures are chained
4+
# together and only performed when `perform()` is called.
5+
#
6+
# Each method returns the object itself, so calls can be chained.
7+
#
8+
# ```ruby
9+
# action = TouchAction.new.press(x: 45, y: 100).wait(5).release
10+
class TouchAction
11+
ACTIONS = [:moveTo, :longPress, :press, :release, :tap, :wait, :perform]
12+
COMPLEX_ACTIONS = [:swipe]
13+
14+
class << self
15+
ACTIONS.each do |action|
16+
define_method(action) do |*args|
17+
ta = TouchAction.new
18+
ta.send(action, args)
19+
ta.perform
20+
end
21+
end
22+
23+
COMPLEX_ACTIONS.each do |action|
24+
define_method(action) do |opts|
25+
auto_perform = opts.delete(:auto_perform) {|k| true}
26+
ta = TouchAction.new
27+
ta.send(action, opts)
28+
return ta unless auto_perform
29+
ta.perform
30+
end
31+
end
32+
end
33+
34+
attr_reader :actions
35+
36+
def initialize
37+
@actions = []
38+
end
39+
40+
# Move to the given co-ordinates.
41+
# @option opts (integer) :x x co-ordinate to move to.
42+
# @option opts (integer) :y y co-ordinate to move to.
43+
# @option opts (WebDriver::Element) Element to scope this move within.
44+
def moveTo(opts)
45+
opts = args_with_ele_ref(opts)
46+
chain_method(:moveTo, opts)
47+
end
48+
49+
# Press down for a specific duration.
50+
# @param element (WebDriver::Element) the element to press.
51+
# @param x (integer) x co-ordinate to press on.
52+
# @param y (integer) y co-ordinate to press on.
53+
# @param (integer) Number of seconds to press.
54+
def press_for_duration(element, x, y, duration)
55+
@actions << {element: element.ref, x: x, y: y, duration: duration}
56+
chain_method(:longPress, args)
57+
end
58+
59+
# Press a finger onto the screen. Finger will stay down until you call
60+
# `release`.
61+
#
62+
# @option opts [WebDriver::Element] :element (Optional) Element to press within.
63+
# @option opts [integer] :x x co-ordinate to press on
64+
# @option opts [integer] :y y co-ordinate to press on
65+
def press(opts)
66+
args = opts.select {|k, v| [:element, :x, :y].include? k}
67+
args = args_with_ele_ref(args)
68+
chain_method(:press, args)
69+
end
70+
71+
# Remove a finger from the screen.
72+
#
73+
# @option opts [WebDriver::Element] :element (Optional) Element to release from.
74+
# @option opts [integer] :x x co-ordinate to release from
75+
# @option opts [integer] :y y co-ordinate to release from
76+
def release(opts=nil)
77+
args = args_with_ele_ref(opts) if opts
78+
chain_method(:release, args)
79+
end
80+
81+
# Touch a point on the screen
82+
#
83+
# @option opts [WebDriver::Element] :element (Optional) Element to restrict scope too.
84+
# @option opts [integer] :x x co-ordinate to tap
85+
# @option opts [integer] :y y co-ordinate to tap
86+
# @option opts [integer] :fingers how many fingers to tap with (Default 1)
87+
def tap(opts)
88+
opts[:count] = opts.delete(:fingers) if opts[:fingers]
89+
opts_with_defaults = {count: 1}.merge opts
90+
chain_method(:tap, opts_with_defaults)
91+
end
92+
93+
# Pause for a number of seconds before the next action
94+
# @params seconds (integer) Number of seconds to pause for
95+
def wait(seconds)
96+
args = {ms: seconds}
97+
chain_method(:wait, args)
98+
end
99+
100+
# Convenience method to peform a swipe.
101+
# @option opts [int] :start_x Where to start swiping, on the x axis. Default 0.
102+
# @option opts [int] :start_y Where to start swiping, on the y axis. Default 0.
103+
# @option opts [int] :end_x Where to end swiping, on the x axis. Default 0.
104+
# @option opts [int] :end_y Where to end swiping, on the y axis. Default 0.
105+
# @option opts [int] :duration How long the actual swipe takes to complete.
106+
def swipe(opts)
107+
start_x = opts.fetch :start_x, 0
108+
start_y = opts.fetch :start_y, 0
109+
end_x = opts.fetch :end_x, 0
110+
end_y = opts.fetch :end_y, 0
111+
duration = opts[:duration]
112+
113+
self.press x: start_x, y: start_y
114+
self.wait(duration) if duration
115+
self.moveTo x: end_x, y: end_y
116+
self.release
117+
self
118+
end
119+
120+
# Ask the driver to perform all actions in this action chain.
121+
def perform
122+
$driver.touch_actions @actions
123+
self
124+
end
125+
126+
# Does nothing, currently.
127+
def cancel
128+
@actions << {action: cancel}
129+
$driver.touch_actions @actions
130+
self
131+
end
132+
133+
private
134+
135+
def chain_method(method, args=nil)
136+
if args
137+
@actions << {action: method, options: args}
138+
else
139+
@actions << {action: method}
140+
end
141+
self
142+
end
143+
144+
def args_with_ele_ref(args)
145+
args[:element] = args[:element].ref if args.has_key? :element
146+
args
147+
end
148+
end
149+
end

lib/appium_lib/driver.rb

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
# device methods
3535
require_relative 'device/device'
36+
require_relative 'device/touch_actions'
37+
require_relative 'device/multi_touch'
3638

3739
# Fix uninitialized constant Minitest (NameError)
3840
module Minitest

0 commit comments

Comments
 (0)