calabash-cucumber 0.19.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|