calabash-cucumber 0.19.2 → 0.20.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/dylibs/libCalabashDyn.dylib +0 -0
- data/dylibs/libCalabashDynSim.dylib +0 -0
- data/lib/calabash-cucumber.rb +9 -2
- data/lib/calabash-cucumber/abstract.rb +23 -0
- data/lib/calabash-cucumber/automator/automator.rb +158 -0
- data/lib/calabash-cucumber/automator/coordinates.rb +401 -0
- data/lib/calabash-cucumber/automator/device_agent.rb +424 -0
- data/lib/calabash-cucumber/automator/instruments.rb +441 -0
- data/lib/calabash-cucumber/connection_helpers.rb +1 -0
- data/lib/calabash-cucumber/core.rb +632 -138
- data/lib/calabash-cucumber/device_agent.rb +346 -0
- data/lib/calabash-cucumber/dot_dir.rb +1 -0
- data/lib/calabash-cucumber/environment.rb +1 -0
- data/lib/calabash-cucumber/environment_helpers.rb +4 -3
- data/lib/calabash-cucumber/http/http.rb +6 -4
- data/lib/calabash-cucumber/keyboard_helpers.rb +97 -679
- data/lib/calabash-cucumber/launcher.rb +107 -31
- data/lib/calabash-cucumber/log_tailer.rb +46 -0
- data/lib/calabash-cucumber/map.rb +7 -1
- data/lib/calabash-cucumber/rotation_helpers.rb +47 -139
- data/lib/calabash-cucumber/status_bar_helpers.rb +51 -20
- data/lib/calabash-cucumber/store/preferences.rb +3 -0
- data/lib/calabash-cucumber/uia.rb +333 -2
- data/lib/calabash-cucumber/usage_tracker.rb +2 -0
- data/lib/calabash-cucumber/version.rb +2 -2
- data/lib/calabash-cucumber/wait_helpers.rb +2 -0
- data/lib/calabash/formatters/html.rb +6 -1
- data/lib/frank-calabash.rb +10 -4
- data/scripts/.irbrc +3 -0
- data/staticlib/calabash.framework.zip +0 -0
- data/staticlib/libFrankCalabash.a +0 -0
- metadata +11 -6
- data/lib/calabash-cucumber/actions/instruments_actions.rb +0 -155
@@ -0,0 +1,424 @@
|
|
1
|
+
# @!visibility private
|
2
|
+
module Calabash
|
3
|
+
module Cucumber
|
4
|
+
module Automator
|
5
|
+
|
6
|
+
require "calabash-cucumber/automator/automator"
|
7
|
+
|
8
|
+
# @!visibility private
|
9
|
+
class DeviceAgent < Calabash::Cucumber::Automator::Automator
|
10
|
+
|
11
|
+
require "run_loop"
|
12
|
+
require "calabash-cucumber/map"
|
13
|
+
|
14
|
+
require "calabash-cucumber/query_helpers"
|
15
|
+
include Calabash::Cucumber::QueryHelpers
|
16
|
+
|
17
|
+
require "calabash-cucumber/status_bar_helpers"
|
18
|
+
include Calabash::Cucumber::StatusBarHelpers
|
19
|
+
|
20
|
+
require "calabash-cucumber/rotation_helpers"
|
21
|
+
include Calabash::Cucumber::RotationHelpers
|
22
|
+
|
23
|
+
require "calabash-cucumber/environment_helpers"
|
24
|
+
include Calabash::Cucumber::EnvironmentHelpers
|
25
|
+
|
26
|
+
require "calabash-cucumber/automator/coordinates"
|
27
|
+
|
28
|
+
# @!visibility private
|
29
|
+
def self.expect_valid_args(args)
|
30
|
+
if args.nil?
|
31
|
+
raise ArgumentError, "Expected args to be a non-nil Array"
|
32
|
+
end
|
33
|
+
|
34
|
+
if !args.is_a?(Array)
|
35
|
+
raise ArgumentError, %Q[Expected args to be an Array, found:
|
36
|
+
|
37
|
+
args = #{args}
|
38
|
+
|
39
|
+
]
|
40
|
+
end
|
41
|
+
|
42
|
+
if args.count != 1
|
43
|
+
raise(ArgumentError,
|
44
|
+
%Q[Expected args to be an Array with one element, found:
|
45
|
+
|
46
|
+
args = #{args}
|
47
|
+
|
48
|
+
])
|
49
|
+
end
|
50
|
+
|
51
|
+
if !args[0].is_a?(RunLoop::DeviceAgent::Client)
|
52
|
+
raise(ArgumentError, %Q[
|
53
|
+
Expected first element of args to be a RunLoop::DeviceAgent::Client instance, found:
|
54
|
+
args[0] = #{args[0]}])
|
55
|
+
end
|
56
|
+
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_reader :client
|
61
|
+
|
62
|
+
# @!visibility private
|
63
|
+
def initialize(*args)
|
64
|
+
DeviceAgent.expect_valid_args(args)
|
65
|
+
@client = args[0]
|
66
|
+
end
|
67
|
+
|
68
|
+
# @!visibility private
|
69
|
+
def name
|
70
|
+
:device_agent
|
71
|
+
end
|
72
|
+
|
73
|
+
# @!visibility private
|
74
|
+
def stop
|
75
|
+
client.send(:shutdown)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @!visibility private
|
79
|
+
def running?
|
80
|
+
client.send(:running?)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @!visibility private
|
84
|
+
def session_delete
|
85
|
+
client.send(:session_delete)
|
86
|
+
end
|
87
|
+
|
88
|
+
# @!visibility private
|
89
|
+
def touch(options)
|
90
|
+
hash = query_for_coordinates(options)
|
91
|
+
|
92
|
+
client.perform_coordinate_gesture("touch",
|
93
|
+
hash[:coordinates][:x],
|
94
|
+
hash[:coordinates][:y])
|
95
|
+
[hash[:view]]
|
96
|
+
end
|
97
|
+
|
98
|
+
# @!visibility private
|
99
|
+
def double_tap(options)
|
100
|
+
hash = query_for_coordinates(options)
|
101
|
+
client.perform_coordinate_gesture("double_tap",
|
102
|
+
hash[:coordinates][:x],
|
103
|
+
hash[:coordinates][:y])
|
104
|
+
[hash[:view]]
|
105
|
+
end
|
106
|
+
|
107
|
+
# @!visibility private
|
108
|
+
def two_finger_tap(options)
|
109
|
+
hash = query_for_coordinates(options)
|
110
|
+
client.perform_coordinate_gesture("two_finger_tap",
|
111
|
+
hash[:coordinates][:x],
|
112
|
+
hash[:coordinates][:y])
|
113
|
+
[hash[:view]]
|
114
|
+
end
|
115
|
+
|
116
|
+
# @!visibility private
|
117
|
+
def touch_hold(options)
|
118
|
+
hash = query_for_coordinates(options)
|
119
|
+
|
120
|
+
duration = options[:duration] || 3
|
121
|
+
client.perform_coordinate_gesture("touch",
|
122
|
+
hash[:coordinates][:x],
|
123
|
+
hash[:coordinates][:y],
|
124
|
+
{:duration => duration})
|
125
|
+
[hash[:view]]
|
126
|
+
end
|
127
|
+
|
128
|
+
# @!visibility private
|
129
|
+
def swipe(options)
|
130
|
+
dupped_options = options.dup
|
131
|
+
|
132
|
+
if dupped_options[:query].nil?
|
133
|
+
dupped_options[:query] = "*"
|
134
|
+
end
|
135
|
+
|
136
|
+
hash = query_for_coordinates(dupped_options)
|
137
|
+
from_point = hash[:coordinates]
|
138
|
+
element = hash[:view]
|
139
|
+
|
140
|
+
# DeviceAgent does not understand the :force. Does anyone?
|
141
|
+
force = dupped_options[:force]
|
142
|
+
case force
|
143
|
+
when :strong
|
144
|
+
duration = 0.2
|
145
|
+
when :normal
|
146
|
+
duration = 0.4
|
147
|
+
when :light
|
148
|
+
duration = 0.7
|
149
|
+
else
|
150
|
+
# Caller is responsible for validating the :force option.
|
151
|
+
duration = 0.5
|
152
|
+
end
|
153
|
+
|
154
|
+
gesture_options = {
|
155
|
+
:duration => duration
|
156
|
+
}
|
157
|
+
|
158
|
+
direction = dupped_options[:direction]
|
159
|
+
to_point = Coordinates.end_point_for_swipe(direction, element, force)
|
160
|
+
client.pan_between_coordinates(from_point, to_point, gesture_options)
|
161
|
+
[hash[:view]]
|
162
|
+
end
|
163
|
+
|
164
|
+
# @!visibility private
|
165
|
+
def pan(from_query, to_query, options)
|
166
|
+
dupped_options = options.dup
|
167
|
+
|
168
|
+
dupped_options[:query] = from_query
|
169
|
+
from_hash = query_for_coordinates(dupped_options)
|
170
|
+
from_point = from_hash[:coordinates]
|
171
|
+
|
172
|
+
dupped_options[:query] = to_query
|
173
|
+
to_hash = query_for_coordinates(dupped_options)
|
174
|
+
to_point = to_hash[:coordinates]
|
175
|
+
|
176
|
+
gesture_options = {
|
177
|
+
:duration => dupped_options[:duration]
|
178
|
+
}
|
179
|
+
|
180
|
+
client.pan_between_coordinates(from_point, to_point,
|
181
|
+
gesture_options)
|
182
|
+
|
183
|
+
[from_hash[:view], to_hash[:view]]
|
184
|
+
end
|
185
|
+
|
186
|
+
# @!visibility private
|
187
|
+
def pan_coordinates(from_point, to_point, options)
|
188
|
+
|
189
|
+
gesture_options = {
|
190
|
+
:duration => options[:duration]
|
191
|
+
}
|
192
|
+
|
193
|
+
client.pan_between_coordinates(from_point, to_point,
|
194
|
+
gesture_options)
|
195
|
+
[first_element_for_query("*")]
|
196
|
+
end
|
197
|
+
|
198
|
+
# @!visibility private
|
199
|
+
def flick(options)
|
200
|
+
gesture_options = {
|
201
|
+
duration: 0.2
|
202
|
+
}
|
203
|
+
|
204
|
+
delta = options[:delta]
|
205
|
+
|
206
|
+
# The UIA deltas are too small.
|
207
|
+
scaled_delta = {
|
208
|
+
:x => delta[:x] * 2.0,
|
209
|
+
:y => delta[:y] * 2.0
|
210
|
+
}
|
211
|
+
|
212
|
+
hash = query_for_coordinates(options)
|
213
|
+
view = hash[:view]
|
214
|
+
|
215
|
+
start_point = point_from(view)
|
216
|
+
end_point = point_from(view, {:offset => scaled_delta})
|
217
|
+
|
218
|
+
client.pan_between_coordinates(start_point,
|
219
|
+
end_point,
|
220
|
+
gesture_options)
|
221
|
+
[view]
|
222
|
+
end
|
223
|
+
|
224
|
+
# @!visibility private
|
225
|
+
def enter_text_with_keyboard(string, options={})
|
226
|
+
client.enter_text(string)
|
227
|
+
end
|
228
|
+
|
229
|
+
# @!visibility private
|
230
|
+
def enter_char_with_keyboard(char)
|
231
|
+
client.enter_text(char)
|
232
|
+
end
|
233
|
+
|
234
|
+
# @!visibility private
|
235
|
+
def char_for_keyboard_action(action_key)
|
236
|
+
SPECIAL_ACTION_CHARS[action_key]
|
237
|
+
end
|
238
|
+
|
239
|
+
# @!visibility private
|
240
|
+
def tap_keyboard_action_key
|
241
|
+
mark = mark_for_return_key_of_first_responder
|
242
|
+
if mark
|
243
|
+
begin
|
244
|
+
# The underlying query for coordinates always expects results.
|
245
|
+
value = client.touch({marked: mark})
|
246
|
+
return value
|
247
|
+
rescue RuntimeError => _
|
248
|
+
RunLoop.log_debug("Cannot find mark '#{mark}' with query; will send a newline")
|
249
|
+
end
|
250
|
+
else
|
251
|
+
RunLoop.log_debug("Cannot find keyboard return key type; sending a newline")
|
252
|
+
end
|
253
|
+
|
254
|
+
code = char_for_keyboard_action("Return")
|
255
|
+
client.enter_text(code)
|
256
|
+
end
|
257
|
+
|
258
|
+
# @!visibility private
|
259
|
+
def tap_keyboard_delete_key
|
260
|
+
client.touch({marked: "delete"})
|
261
|
+
end
|
262
|
+
|
263
|
+
# @!visibility private
|
264
|
+
def fast_enter_text(text)
|
265
|
+
client.enter_text(text)
|
266
|
+
end
|
267
|
+
|
268
|
+
# @!visibility private
|
269
|
+
#
|
270
|
+
# Stable across different keyboard languages.
|
271
|
+
def dismiss_ipad_keyboard
|
272
|
+
client.touch({marked: "Hide keyboard"})
|
273
|
+
end
|
274
|
+
|
275
|
+
# @!visibility private
|
276
|
+
def rotate(direction)
|
277
|
+
# Caller is responsible for normalizing and verifying direction.
|
278
|
+
current_orientation = status_bar_orientation.to_sym
|
279
|
+
key = orientation_key(direction, current_orientation)
|
280
|
+
position = orientation_for_key(key)
|
281
|
+
rotate_home_button_to(position)
|
282
|
+
end
|
283
|
+
|
284
|
+
# @!visibility private
|
285
|
+
def rotate_home_button_to(position)
|
286
|
+
# Caller is responsible for normalizing and verifying position.
|
287
|
+
client.rotate_home_button_to(position)
|
288
|
+
status_bar_orientation.to_sym
|
289
|
+
end
|
290
|
+
|
291
|
+
private
|
292
|
+
|
293
|
+
# @!visibility private
|
294
|
+
#
|
295
|
+
# Calls #point_from which applies any :offset supplied in the options.
|
296
|
+
def query_for_coordinates(options)
|
297
|
+
uiquery = options[:query]
|
298
|
+
|
299
|
+
if uiquery.nil?
|
300
|
+
offset = options[:offset]
|
301
|
+
|
302
|
+
if offset && offset[:x] && offset[:y]
|
303
|
+
{
|
304
|
+
:coordinates => offset,
|
305
|
+
:view => offset
|
306
|
+
}
|
307
|
+
else
|
308
|
+
raise ArgumentError, %Q[
|
309
|
+
If query is nil, there must be a valid offset in the options.
|
310
|
+
|
311
|
+
Expected: options[:offset] = {:x => NUMERIC, :y => NUMERIC}
|
312
|
+
Actual: options[:offset] = #{offset ? offset : "nil"}
|
313
|
+
|
314
|
+
]
|
315
|
+
end
|
316
|
+
else
|
317
|
+
|
318
|
+
first_element = first_element_for_query(uiquery)
|
319
|
+
|
320
|
+
if first_element.nil?
|
321
|
+
msg = %Q[
|
322
|
+
Could not find any views with query:
|
323
|
+
|
324
|
+
#{uiquery}
|
325
|
+
|
326
|
+
Make sure your query returns at least one view.
|
327
|
+
|
328
|
+
]
|
329
|
+
Calabash::Cucumber::Map.new.screenshot_and_raise(msg)
|
330
|
+
else
|
331
|
+
{
|
332
|
+
:coordinates => point_from(first_element, options),
|
333
|
+
:view => first_element
|
334
|
+
}
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
# @!visibility private
|
340
|
+
def first_element_for_query(uiquery)
|
341
|
+
|
342
|
+
if uiquery.nil?
|
343
|
+
raise ArgumentError, "Query cannot be nil"
|
344
|
+
end
|
345
|
+
|
346
|
+
# Will raise if response "outcome" is not SUCCESS
|
347
|
+
results = Calabash::Cucumber::Map.raw_map(uiquery, :query)["results"]
|
348
|
+
|
349
|
+
if results.empty?
|
350
|
+
nil
|
351
|
+
else
|
352
|
+
results[0]
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# @!visibility private
|
357
|
+
#
|
358
|
+
# Don't change the double quotes.
|
359
|
+
SPECIAL_ACTION_CHARS = {
|
360
|
+
"Delete" => "\b",
|
361
|
+
"Return" => "\n"
|
362
|
+
}.freeze
|
363
|
+
|
364
|
+
# @!visibility private
|
365
|
+
#
|
366
|
+
# Keys are from the UIReturnKeyType enum.
|
367
|
+
#
|
368
|
+
# The values are localization independent identifiers - these are
|
369
|
+
# stable across localizations and keyboard languages. The exception is
|
370
|
+
# Continue which is not stable.
|
371
|
+
RETURN_KEY_TYPE = {
|
372
|
+
0 => "Return",
|
373
|
+
1 => "Go",
|
374
|
+
2 => "Google",
|
375
|
+
# Needs special physical device vs simulator handling.
|
376
|
+
3 => "Join",
|
377
|
+
4 => "Next",
|
378
|
+
5 => "Route",
|
379
|
+
6 => "Search",
|
380
|
+
7 => "Send",
|
381
|
+
8 => "Yahoo",
|
382
|
+
9 => "Done",
|
383
|
+
10 => "Emergency call",
|
384
|
+
# https://xamarin.atlassian.net/browse/TCFW-344
|
385
|
+
# Localized!!! Apple bug.
|
386
|
+
11 => "Continue"
|
387
|
+
}.freeze
|
388
|
+
|
389
|
+
# @!visibility private
|
390
|
+
def mark_for_return_key_type(number)
|
391
|
+
# https://xamarin.atlassian.net/browse/TCFW-361
|
392
|
+
value = RETURN_KEY_TYPE[number]
|
393
|
+
if value == "Join" && !simulator?
|
394
|
+
"Join:"
|
395
|
+
else
|
396
|
+
value
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# @!visibility private
|
401
|
+
def return_key_type_of_first_responder
|
402
|
+
|
403
|
+
['textField', 'textView'].each do |ui_class|
|
404
|
+
query = "#{ui_class} isFirstResponder:1"
|
405
|
+
raw = Calabash::Cucumber::Map.raw_map(query, :query, :returnKeyType)
|
406
|
+
results = raw["results"]
|
407
|
+
if !results.empty?
|
408
|
+
return results.first
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
RunLoop.log_debug("Cannot find keyboard first responder to ask for its returnKeyType")
|
413
|
+
nil
|
414
|
+
end
|
415
|
+
|
416
|
+
# @!visibility private
|
417
|
+
def mark_for_return_key_of_first_responder
|
418
|
+
number = return_key_type_of_first_responder
|
419
|
+
mark_for_return_key_type(number)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
@@ -0,0 +1,441 @@
|
|
1
|
+
module Calabash
|
2
|
+
module Cucumber
|
3
|
+
# @!visibility private
|
4
|
+
module Automator
|
5
|
+
|
6
|
+
require "calabash-cucumber/automator/automator"
|
7
|
+
|
8
|
+
# @!visibility private
|
9
|
+
class Instruments < Calabash::Cucumber::Automator::Automator
|
10
|
+
|
11
|
+
require "calabash-cucumber/uia"
|
12
|
+
include Calabash::Cucumber::UIA
|
13
|
+
|
14
|
+
require "calabash-cucumber/connection_helpers"
|
15
|
+
include Calabash::Cucumber::ConnectionHelpers
|
16
|
+
|
17
|
+
require "calabash-cucumber/query_helpers"
|
18
|
+
include Calabash::Cucumber::QueryHelpers
|
19
|
+
|
20
|
+
require "calabash-cucumber/status_bar_helpers"
|
21
|
+
include Calabash::Cucumber::StatusBarHelpers
|
22
|
+
|
23
|
+
require "calabash-cucumber/rotation_helpers"
|
24
|
+
include Calabash::Cucumber::RotationHelpers
|
25
|
+
|
26
|
+
require "calabash-cucumber/map"
|
27
|
+
|
28
|
+
# @!visibility private
|
29
|
+
UIA_STRATEGIES = [:preferences, :host, :shared_element]
|
30
|
+
|
31
|
+
attr_reader :run_loop
|
32
|
+
|
33
|
+
# @!visibility private
|
34
|
+
def self.expect_valid_init_args(args)
|
35
|
+
if args.nil?
|
36
|
+
raise(ArgumentError, "Expected non-nil argument for initializer")
|
37
|
+
end
|
38
|
+
|
39
|
+
if !args.is_a?(Array)
|
40
|
+
raise(ArgumentError, "Expected an array argument for initializer")
|
41
|
+
end
|
42
|
+
|
43
|
+
run_loop = args[0]
|
44
|
+
|
45
|
+
if run_loop.nil?
|
46
|
+
raise(ArgumentError,
|
47
|
+
%Q[Expected first element of args to be non-nil:
|
48
|
+
|
49
|
+
args = #{args}
|
50
|
+
|
51
|
+
])
|
52
|
+
end
|
53
|
+
|
54
|
+
if args.count != 1
|
55
|
+
raise(ArgumentError,
|
56
|
+
%Q[Expected args to have exactly one element but found:
|
57
|
+
|
58
|
+
args = #{args}
|
59
|
+
])
|
60
|
+
end
|
61
|
+
|
62
|
+
self.expect_valid_run_loop(run_loop)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @!visibility private
|
66
|
+
def self.expect_valid_run_loop(run_loop)
|
67
|
+
if run_loop.nil?
|
68
|
+
raise(ArgumentError, "Expected run_loop arg to be non-nil")
|
69
|
+
end
|
70
|
+
|
71
|
+
if !run_loop.is_a?(Hash)
|
72
|
+
raise(ArgumentError, %Q[
|
73
|
+
Expected run_loop arg to be a hash, but found:
|
74
|
+
|
75
|
+
run_loop = #{run_loop} is_a => #{run_loop.class}
|
76
|
+
|
77
|
+
])
|
78
|
+
end
|
79
|
+
|
80
|
+
automator = run_loop[:automator]
|
81
|
+
if automator && automator != :instruments
|
82
|
+
raise(ArgumentError, %Q[
|
83
|
+
Invalid :@automator. Expected :instruments but found:
|
84
|
+
|
85
|
+
#{automator}
|
86
|
+
|
87
|
+
in
|
88
|
+
|
89
|
+
#{run_loop}
|
90
|
+
|
91
|
+
])
|
92
|
+
end
|
93
|
+
|
94
|
+
[:pid, :udid, :index, :log_file, :uia_strategy].each do |key|
|
95
|
+
if !run_loop[key]
|
96
|
+
raise(ArgumentError, %Q[
|
97
|
+
Expected run_loop to have a truthy value for :#{key} but found:
|
98
|
+
|
99
|
+
#{run_loop}
|
100
|
+
|
101
|
+
])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
strategy = run_loop[:uia_strategy]
|
106
|
+
if !self.valid_uia_strategy?(strategy)
|
107
|
+
raise(ArgumentError, %Q[
|
108
|
+
Expected '#{strategy}' to be one of these supported strategies:
|
109
|
+
|
110
|
+
#{UIA_STRATEGIES}
|
111
|
+
|
112
|
+
])
|
113
|
+
end
|
114
|
+
true
|
115
|
+
end
|
116
|
+
|
117
|
+
# @!visibility private
|
118
|
+
def self.valid_uia_strategy?(strategy)
|
119
|
+
UIA_STRATEGIES.include?(strategy)
|
120
|
+
end
|
121
|
+
|
122
|
+
# @!visibility private
|
123
|
+
def initialize(*args)
|
124
|
+
Instruments.expect_valid_init_args(args)
|
125
|
+
@run_loop = args[0]
|
126
|
+
end
|
127
|
+
|
128
|
+
# @!visibility private
|
129
|
+
def name
|
130
|
+
:instruments
|
131
|
+
end
|
132
|
+
|
133
|
+
# @!visibility private
|
134
|
+
def stop
|
135
|
+
RunLoop.stop(run_loop)
|
136
|
+
end
|
137
|
+
|
138
|
+
# @!visibility private
|
139
|
+
def touch(options)
|
140
|
+
query_action(options, :uia_tap_offset)
|
141
|
+
end
|
142
|
+
|
143
|
+
# @!visibility private
|
144
|
+
def double_tap(options)
|
145
|
+
query_action(options, :uia_double_tap_offset)
|
146
|
+
end
|
147
|
+
|
148
|
+
# @!visibility private
|
149
|
+
def two_finger_tap(options)
|
150
|
+
query_action(options, :uia_two_finger_tap_offset)
|
151
|
+
end
|
152
|
+
|
153
|
+
# @!visibility private
|
154
|
+
def flick(options)
|
155
|
+
query_action(options) do |offset|
|
156
|
+
delta = {:offset => options[:delta] || {}}
|
157
|
+
uia_flick_offset(offset, point_from(offset, delta))
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# @!visibility private
|
162
|
+
def touch_hold(options)
|
163
|
+
query_action(options) do |offset|
|
164
|
+
duration = options[:duration] || 3
|
165
|
+
uia_touch_hold_offset(duration, offset)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# @!visibility private
|
170
|
+
def swipe(options)
|
171
|
+
query_action(options, :uia_swipe_offset, options)
|
172
|
+
end
|
173
|
+
|
174
|
+
# @!visibility private
|
175
|
+
def pan(from, to, options={})
|
176
|
+
query_action(:query => from) do |from_offset|
|
177
|
+
query_action(:query => to) do |to_offset|
|
178
|
+
uia_pan_offset(from_offset, to_offset, options)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# @!visibility private
|
184
|
+
def pan_coordinates(from, to, options={})
|
185
|
+
uia_pan_offset(from, to, options)
|
186
|
+
[find_and_normalize("*")]
|
187
|
+
end
|
188
|
+
|
189
|
+
# @!visibility private
|
190
|
+
def pinch(in_out, options)
|
191
|
+
query_action(options) do |offset|
|
192
|
+
options[:duration] = options[:duration] || 0.5
|
193
|
+
uia_pinch_offset(in_out, offset, options)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# @!visibility private
|
198
|
+
def send_app_to_background(secs)
|
199
|
+
uia_send_app_to_background(secs)
|
200
|
+
end
|
201
|
+
|
202
|
+
# @!visibility private
|
203
|
+
def enter_text_with_keyboard(string, existing_text="")
|
204
|
+
uia_type_string(string, existing_text)
|
205
|
+
end
|
206
|
+
|
207
|
+
# @!visibility private
|
208
|
+
def fast_enter_text(text)
|
209
|
+
uia_set_responder_value(text)
|
210
|
+
end
|
211
|
+
|
212
|
+
# @!visibility private
|
213
|
+
# It is the caller's responsibility to ensure the keyboard is visible.
|
214
|
+
def enter_char_with_keyboard(char)
|
215
|
+
uia("uia.keyboard().typeString('#{char}')")
|
216
|
+
end
|
217
|
+
|
218
|
+
# @!visibility private
|
219
|
+
def char_for_keyboard_action(action_key)
|
220
|
+
SPECIAL_ACTION_CHARS[action_key]
|
221
|
+
end
|
222
|
+
|
223
|
+
# @!visibility private
|
224
|
+
# TODO Implement this in JavaScript?
|
225
|
+
# See the device_agent implementation of tap_keyboard_action_key and
|
226
|
+
# the tap_keyboard_delete_key of this class.
|
227
|
+
def tap_keyboard_action_key
|
228
|
+
code = char_for_keyboard_action("Return")
|
229
|
+
enter_char_with_keyboard(code)
|
230
|
+
end
|
231
|
+
|
232
|
+
# @!visibility private
|
233
|
+
def tap_keyboard_delete_key
|
234
|
+
js_tap_delete = %Q[(function() {
|
235
|
+
var deleteElement = uia.keyboard().elements().firstWithName('Delete');
|
236
|
+
if (deleteElement.isValid()) {
|
237
|
+
deleteElement.tap();
|
238
|
+
} else {
|
239
|
+
uia.keyboard().elements().firstWithName('delete').tap();
|
240
|
+
}
|
241
|
+
})();].gsub!($-0, "")
|
242
|
+
|
243
|
+
uia(js_tap_delete)
|
244
|
+
end
|
245
|
+
|
246
|
+
# @!visibility private
|
247
|
+
def dismiss_ipad_keyboard
|
248
|
+
js = %Q[#{query_uia_hide_keyboard_button}.tap()]
|
249
|
+
uia(js)
|
250
|
+
end
|
251
|
+
|
252
|
+
# @!visibility private
|
253
|
+
def rotate(direction)
|
254
|
+
current_orientation = status_bar_orientation.to_sym
|
255
|
+
result = rotate_with_uia(direction, current_orientation)
|
256
|
+
recalibrate_after_rotation
|
257
|
+
ap result if RunLoop::Environment.debug?
|
258
|
+
status_bar_orientation.to_sym
|
259
|
+
end
|
260
|
+
|
261
|
+
# @!visibility private
|
262
|
+
def rotate_home_button_to(position)
|
263
|
+
rotate_to_uia_orientation(position)
|
264
|
+
recalibrate_after_rotation
|
265
|
+
status_bar_orientation.to_sym
|
266
|
+
end
|
267
|
+
|
268
|
+
private
|
269
|
+
|
270
|
+
# @!visibility private
|
271
|
+
#
|
272
|
+
# Calls #point_from which applies any :offset supplied in the options.
|
273
|
+
def query_for_coordinates(options)
|
274
|
+
ui_query = options[:query]
|
275
|
+
|
276
|
+
first_element, orientation = first_element_for_query(ui_query)
|
277
|
+
|
278
|
+
if first_element.nil?
|
279
|
+
msg = %Q[
|
280
|
+
Could not find any views with query:
|
281
|
+
|
282
|
+
#{ui_query}
|
283
|
+
|
284
|
+
Try adjusting your query to return at least one view.
|
285
|
+
|
286
|
+
]
|
287
|
+
Calabash::Cucumber::Map.new.screenshot_and_raise(msg)
|
288
|
+
else
|
289
|
+
|
290
|
+
normalize_rect_for_orientation!(orientation, first_element)
|
291
|
+
|
292
|
+
{
|
293
|
+
:coordinates => point_from(first_element, options),
|
294
|
+
:view => first_element
|
295
|
+
}
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# @!visibility private
|
300
|
+
def first_element_for_query(ui_query)
|
301
|
+
# Will raise if response "outcome" is not SUCCESS
|
302
|
+
raw = Calabash::Cucumber::Map.raw_map(ui_query, :query)
|
303
|
+
results = raw["results"]
|
304
|
+
orientation = raw["status_bar_orientation"]
|
305
|
+
|
306
|
+
if results.empty?
|
307
|
+
return nil, nil
|
308
|
+
else
|
309
|
+
return results[0], orientation
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# @!visibility private
|
314
|
+
# Data interface
|
315
|
+
# options[:query] or options[:offset]
|
316
|
+
def query_action(options, action=nil, *args, &block)
|
317
|
+
ui_query = options[:query]
|
318
|
+
offset = options[:offset]
|
319
|
+
if ui_query
|
320
|
+
res = find_and_normalize(ui_query)
|
321
|
+
return res if res.empty?
|
322
|
+
el = res.first
|
323
|
+
final_offset = point_from(el, options)
|
324
|
+
if block_given?
|
325
|
+
yield final_offset
|
326
|
+
else
|
327
|
+
self.send(action, final_offset, *args)
|
328
|
+
end
|
329
|
+
[el]
|
330
|
+
else
|
331
|
+
##implies offset
|
332
|
+
if block_given?
|
333
|
+
yield offset
|
334
|
+
else
|
335
|
+
self.send(action, offset, *args)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# @!visibility private
|
341
|
+
def find_and_normalize(ui_query)
|
342
|
+
raw_result = Calabash::Cucumber::Map.raw_map(ui_query, :query)
|
343
|
+
orientation = raw_result["status_bar_orientation"]
|
344
|
+
res = raw_result["results"]
|
345
|
+
|
346
|
+
return res if res.empty?
|
347
|
+
|
348
|
+
first_res = res.first
|
349
|
+
normalize_rect_for_orientation!(orientation, first_res["rect"]) if first_res["rect"]
|
350
|
+
|
351
|
+
res
|
352
|
+
end
|
353
|
+
|
354
|
+
# @!visibility private
|
355
|
+
def normalize_rect_for_orientation!(orientation, rect)
|
356
|
+
orientation = orientation.to_sym
|
357
|
+
launcher = Calabash::Cucumber::Launcher.launcher
|
358
|
+
device = launcher.device
|
359
|
+
|
360
|
+
# Coordinate translations for orientation is handled in the server for iOS 8+
|
361
|
+
if device.ios_major_version.to_i >= 8
|
362
|
+
return
|
363
|
+
end
|
364
|
+
|
365
|
+
# We cannot use Device#screen_dimensions here because on iPads the height
|
366
|
+
# and width are the opposite of what we expect.
|
367
|
+
# @todo Move all coordinate/orientation translation into the server.
|
368
|
+
if device.ipad?
|
369
|
+
screen_size = { :width => 768, :height => 1024 }
|
370
|
+
elsif device.iphone_4in?
|
371
|
+
screen_size = { :width => 320, :height => 568 }
|
372
|
+
else
|
373
|
+
screen_size = { :width => 320, :height => 480 }
|
374
|
+
end
|
375
|
+
|
376
|
+
case orientation
|
377
|
+
when :right
|
378
|
+
cx = rect["center_x"]
|
379
|
+
rect["center_x"] = rect["center_y"]
|
380
|
+
rect["center_y"] = screen_size[:width] - cx
|
381
|
+
when :left
|
382
|
+
cx = rect["center_x"]
|
383
|
+
rect["center_x"] = screen_size[:height] - rect["center_y"]
|
384
|
+
rect["center_y"] = cx
|
385
|
+
when :up
|
386
|
+
cy = rect["center_y"]
|
387
|
+
cx = rect["center_x"]
|
388
|
+
rect["center_y"] = screen_size[:height] - cy
|
389
|
+
rect["center_x"] = screen_size[:width] - cx
|
390
|
+
else
|
391
|
+
# no-op by design.
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# @!visibility private
|
396
|
+
def recalibrate_after_rotation
|
397
|
+
uia_query :window
|
398
|
+
end
|
399
|
+
|
400
|
+
# @!visibility private
|
401
|
+
def rotate_to_uia_orientation(orientation)
|
402
|
+
case orientation
|
403
|
+
when :down then key = :portrait
|
404
|
+
when :up then key = :upside_down
|
405
|
+
when :left then key = :landscape_right
|
406
|
+
when :right then key = :landscape_left
|
407
|
+
else
|
408
|
+
raise ArgumentError,
|
409
|
+
"Expected '#{orientation}' to be :left, :right, :up, or :down"
|
410
|
+
end
|
411
|
+
value = orientation_for_key(key)
|
412
|
+
cmd = "UIATarget.localTarget().setDeviceOrientation(#{value})"
|
413
|
+
uia(cmd)
|
414
|
+
end
|
415
|
+
|
416
|
+
# @!visibility private
|
417
|
+
def rotate_with_uia(direction, current_orientation)
|
418
|
+
key = orientation_key(direction, current_orientation)
|
419
|
+
value = orientation_for_key(key)
|
420
|
+
cmd = "UIATarget.localTarget().setDeviceOrientation(#{value})"
|
421
|
+
uia(cmd)
|
422
|
+
end
|
423
|
+
|
424
|
+
# @!visibility private
|
425
|
+
# Returns a query string for finding the iPad 'Hide keyboard' button.
|
426
|
+
def query_uia_hide_keyboard_button
|
427
|
+
"uia.keyboard().buttons()['Hide keyboard']"
|
428
|
+
end
|
429
|
+
|
430
|
+
# @!visibility private
|
431
|
+
#
|
432
|
+
# Don't change the single single quotes.
|
433
|
+
SPECIAL_ACTION_CHARS = {
|
434
|
+
"Delete" => '\b',
|
435
|
+
"Return" => '\n'
|
436
|
+
}
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|