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
@@ -26,6 +26,9 @@ module Calabash
|
|
26
26
|
include Calabash::Cucumber::StatusBarHelpers
|
27
27
|
include Calabash::Cucumber::RotationHelpers
|
28
28
|
|
29
|
+
require "calabash-cucumber/keyboard_helpers"
|
30
|
+
include Calabash::Cucumber::KeyboardHelpers
|
31
|
+
|
29
32
|
# @!visibility private
|
30
33
|
# @deprecated Use Cucumber's step method.
|
31
34
|
#
|
@@ -158,9 +161,7 @@ module Calabash
|
|
158
161
|
#
|
159
162
|
# @param [String] uiquery a query specifying which objects to flash
|
160
163
|
# @param [Array] args argument is ignored and should be deprecated
|
161
|
-
# @return [Array] an array of that contains the
|
162
|
-
# objc selector `description` on each matching view.
|
163
|
-
#
|
164
|
+
# @return [Array] an array of that contains all the view matched.
|
164
165
|
def flash(uiquery, *args)
|
165
166
|
# todo deprecate the *args argument in the flash method
|
166
167
|
# todo :flash operation should return views as JSON objects
|
@@ -180,23 +181,111 @@ module Calabash
|
|
180
181
|
Calabash::Cucumber::VERSION
|
181
182
|
end
|
182
183
|
|
184
|
+
# Rotates the home button to a position relative to the status bar.
|
185
|
+
#
|
186
|
+
# @example portrait
|
187
|
+
# rotate_home_button_to :down
|
188
|
+
#
|
189
|
+
# @example upside down
|
190
|
+
# rotate_home_button_to :up
|
191
|
+
#
|
192
|
+
# @example landscape with left home button AKA: _right_ landscape
|
193
|
+
# rotate_home_button_to :left
|
194
|
+
#
|
195
|
+
# @example landscape with right home button AKA: _left_ landscape
|
196
|
+
# rotate_home_button_to :right
|
197
|
+
#
|
198
|
+
# Refer to Apple's documentation for clarification about left vs.
|
199
|
+
# right landscape orientations.
|
200
|
+
#
|
201
|
+
# For legacy support the `dir` argument can be a String or Symbol.
|
202
|
+
# Please update your code to pass a Symbol.
|
203
|
+
#
|
204
|
+
# For legacy support `:top` and `top` are synonyms for `:up`.
|
205
|
+
# Please update your code to pass `:up`.
|
206
|
+
#
|
207
|
+
# For legacy support `:bottom` and `bottom` are synonyms for `:down`.
|
208
|
+
# Please update your code to pass `:down`.
|
209
|
+
#
|
210
|
+
# @param [Symbol] position The position of the home button after the rotation.
|
211
|
+
# Can be one of `{:down | :left | :right | :up }`.
|
212
|
+
#
|
213
|
+
# @note A rotation will only occur if your view controller and application
|
214
|
+
# support the target orientation.
|
215
|
+
#
|
216
|
+
# @return [Symbol] The position of the home button relative to the status
|
217
|
+
# bar when all rotations have been completed.
|
218
|
+
def rotate_home_button_to(position)
|
219
|
+
|
220
|
+
normalized_symbol = expect_valid_rotate_home_to_arg(position)
|
221
|
+
current_orientation = status_bar_orientation.to_sym
|
222
|
+
|
223
|
+
return current_orientation if current_orientation == normalized_symbol
|
224
|
+
|
225
|
+
launcher.automator.send(:rotate_home_button_to, normalized_symbol)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Rotates the device in the direction indicated by `direction`.
|
229
|
+
#
|
230
|
+
# @example rotate left
|
231
|
+
# rotate :left
|
232
|
+
#
|
233
|
+
# @example rotate right
|
234
|
+
# rotate :right
|
235
|
+
#
|
236
|
+
# @param [Symbol] direction The direction to rotate. Can be :left or :right.
|
237
|
+
#
|
238
|
+
# @return [Symbol] The position of the home button relative to the status
|
239
|
+
# bar after the rotation. Will be one of `{:down | :left | :right | :up }`.
|
240
|
+
# @raise [ArgumentError] If direction is not :left or :right.
|
241
|
+
def rotate(direction)
|
242
|
+
as_symbol = direction.to_sym
|
243
|
+
|
244
|
+
if as_symbol != :left && as_symbol != :right
|
245
|
+
raise ArgumentError,
|
246
|
+
"Expected '#{direction}' to be :left or :right"
|
247
|
+
end
|
248
|
+
|
249
|
+
launcher.automator.send(:rotate, as_symbol)
|
250
|
+
end
|
251
|
+
|
183
252
|
# Performs the `tap` gesture on the (first) view that matches
|
184
|
-
# query `uiquery`. Note that `touch` assumes the view is visible and not
|
185
|
-
# If the view is not visible `touch` will fail. If the view is
|
186
|
-
# `touch` will *silently* fail.
|
253
|
+
# query `uiquery`. Note that `touch` assumes the view is visible and not
|
254
|
+
# animating. If the view is not visible `touch` will fail. If the view is
|
255
|
+
# animating `touch` will *silently* fail.
|
256
|
+
#
|
187
257
|
# By default, taps the center of the view.
|
188
258
|
# @see Calabash::Cucumber::WaitHelpers#wait_tap
|
189
259
|
# @see Calabash::Cucumber::Operations#tap_mark
|
190
260
|
# @see #tap_point
|
191
|
-
#
|
192
|
-
#
|
261
|
+
#
|
262
|
+
# @param {String} uiquery query describing view to tap. If this value is
|
263
|
+
# `nil` then an :offset must be passed as an option. This can be used
|
264
|
+
# to tap a specific coordinate.
|
193
265
|
# @param {Hash} options option for modifying the details of the touch
|
194
|
-
# @option options {Hash} :offset (nil) optional offset to touch point.
|
195
|
-
#
|
196
|
-
#
|
266
|
+
# @option options {Hash} :offset (nil) optional offset to touch point.
|
267
|
+
# Offset supports an `:x` and `:y` key and causes the touch to be offset
|
268
|
+
# with `(x,y)` relative to the center.
|
269
|
+
#
|
270
|
+
# @return {Array<Hash>} array containing the serialized version of the
|
271
|
+
# tapped view.
|
272
|
+
#
|
273
|
+
# @raise [RuntimeError] If query is non nil and matches no views.
|
274
|
+
# @raise [ArgumentError] If query is nil and there is no :offset in the
|
275
|
+
# the options. The offset must contain both an :x and :y value.
|
197
276
|
def touch(uiquery, options={})
|
198
|
-
if uiquery.nil?
|
199
|
-
|
277
|
+
if uiquery.nil?
|
278
|
+
offset = options[:offset]
|
279
|
+
|
280
|
+
if !(offset && offset[:x] && offset[:y])
|
281
|
+
raise ArgumentError, %Q[
|
282
|
+
If query is nil, there must be a valid offset in the options.
|
283
|
+
|
284
|
+
Expected: options[:offset] = {:x => NUMERIC, :y => NUMERIC}
|
285
|
+
Actual: options[:offset] = #{offset ? offset : "nil"}
|
286
|
+
|
287
|
+
]
|
288
|
+
end
|
200
289
|
end
|
201
290
|
query_action_with_options(:touch, uiquery, options)
|
202
291
|
end
|
@@ -236,8 +325,8 @@ module Calabash
|
|
236
325
|
#
|
237
326
|
# @note This assumes the view is visible and not animating.
|
238
327
|
#
|
239
|
-
# If the view is not visible it will fail with an error. If the view is
|
240
|
-
# it will *silently* fail.
|
328
|
+
# If the view is not visible it will fail with an error. If the view is
|
329
|
+
# animating it will *silently* fail.
|
241
330
|
#
|
242
331
|
# By default, taps the center of the view.
|
243
332
|
#
|
@@ -245,60 +334,22 @@ module Calabash
|
|
245
334
|
# two_finger_tap "view marked:'Third'", offset:{x:100}
|
246
335
|
# @param {String} uiquery query describing view to touch.
|
247
336
|
# @param {Hash} options option for modifying the details of the touch.
|
248
|
-
# @option options {Hash} :offset (nil) optional offset to touch point.
|
249
|
-
#
|
250
|
-
#
|
337
|
+
# @option options {Hash} :offset (nil) optional offset to touch point.
|
338
|
+
# Offset supports an `:x` and `:y` key and causes the touch to be offset
|
339
|
+
# with `(x,y)` relative to the center (`center + (offset[:x], offset[:y])`).
|
340
|
+
# @return {Array<Hash>} array containing the serialized version of the
|
341
|
+
# tapped view.
|
251
342
|
def two_finger_tap(uiquery,options={})
|
252
343
|
query_action_with_options(:two_finger_tap, uiquery, options)
|
253
344
|
end
|
254
345
|
|
255
|
-
# Performs the "
|
256
|
-
# query `uiquery`.
|
346
|
+
# Performs the "long press" or "touch and hold" gesture on the (first)
|
347
|
+
# view that matches query `uiquery`.
|
257
348
|
#
|
258
349
|
# @note This assumes the view is visible and not animating.
|
259
350
|
#
|
260
|
-
# If the view is not visible it will fail with an error. If the view is
|
261
|
-
# it will *silently* fail.
|
262
|
-
#
|
263
|
-
# By default, the gesture starts at the center of the view and "flicks" according to `delta`.
|
264
|
-
#
|
265
|
-
# A flick is similar to a swipe.
|
266
|
-
#
|
267
|
-
# @example
|
268
|
-
# flick("MKMapView", {x:100,y:50})
|
269
|
-
# @note Due to a bug in the iOS Simulator (or UIAutomation on the simulator)
|
270
|
-
# swiping and other 'advanced' gestures are not supported in certain
|
271
|
-
# scroll views (e.g. UITableView or UIScrollView). It does work when running
|
272
|
-
# on physical devices, though, Here is a link to a relevant Stack Overflow post
|
273
|
-
# http://stackoverflow.com/questions/18792965/uiautomations-draginsidewithoptions-has-no-effect-on-ios7-simulator
|
274
|
-
# It is not a bug in Calabash itself but rather in UIAutomation and hence we can't just
|
275
|
-
# fix it. The work around is typically to use the scroll_to_* functions.
|
276
|
-
#
|
277
|
-
# @param {String} uiquery query describing view to touch.
|
278
|
-
# @param {Hash} delta coordinate describing the direction to flick
|
279
|
-
# @param {Hash} options option for modifying the details of the touch.
|
280
|
-
# @option options {Hash} :offset (nil) optional offset to touch point. Offset supports an `:x` and `:y` key
|
281
|
-
# and causes the touch to be offset with `(x,y)` relative to the center (`center + (offset[:x], offset[:y])`).
|
282
|
-
# @option delta {Numeric} :x (0) optional. The force and direction of the flick on the `x`-axis
|
283
|
-
# @option delta {Numeric} :y (0) optional. The force and direction of the flick on the `y`-axis
|
284
|
-
# @return {Array<Hash>} array containing the serialized version of the touched view.
|
285
|
-
def flick(uiquery, delta, options={})
|
286
|
-
uiquery, options = extract_query_and_options(uiquery, options)
|
287
|
-
options[:delta] = delta
|
288
|
-
views_touched = launcher.actions.flick(options)
|
289
|
-
unless uiquery.nil?
|
290
|
-
screenshot_and_raise "flick could not find view: '#{uiquery}', args: #{options}" if views_touched.empty?
|
291
|
-
end
|
292
|
-
views_touched
|
293
|
-
end
|
294
|
-
|
295
|
-
# Performs the "long press" or "touch and hold" gesture on the (first) view that matches
|
296
|
-
# query `uiquery`.
|
297
|
-
#
|
298
|
-
# @note This assumes the view is visible and not animating.
|
299
|
-
#
|
300
|
-
# If the view is not visible it will fail with an error. If the view is animating
|
301
|
-
# it will *silently* fail.
|
351
|
+
# If the view is not visible it will fail with an error. If the view is
|
352
|
+
# animating it will *silently* fail.
|
302
353
|
#
|
303
354
|
# By default, the gesture starts at the center of the view.
|
304
355
|
#
|
@@ -306,71 +357,230 @@ module Calabash
|
|
306
357
|
# touch_hold "webView css:'input'", duration:10, offset:{x: -40}
|
307
358
|
# @param {String} uiquery query describing view to touch.
|
308
359
|
# @param {Hash} options option for modifying the details of the touch.
|
309
|
-
# @option options {Hash} :offset (nil) optional offset to touch point.
|
310
|
-
#
|
360
|
+
# @option options {Hash} :offset (nil) optional offset to touch point.
|
361
|
+
# Offset supports an `:x` and `:y` key and causes the touch to be offset
|
362
|
+
# with `(x,y)` relative to the center (`center + (offset[:x], offset[:y])`).
|
311
363
|
# @option options {Numeric} :duration (3) duration of the 'hold'.
|
312
|
-
# @return {Array<Hash>} array containing the serialized version of the
|
364
|
+
# @return {Array<Hash>} array containing the serialized version of the
|
365
|
+
# touched view.
|
313
366
|
def touch_hold(uiquery, options={})
|
314
367
|
query_action_with_options(:touch_hold, uiquery, options)
|
315
368
|
end
|
316
369
|
|
317
370
|
# Performs a "swipe" gesture.
|
318
|
-
# By default, the gesture starts at the center of the screen.
|
319
371
|
#
|
320
|
-
# @
|
321
|
-
# first argument. We should migrate this.
|
372
|
+
# @example
|
322
373
|
#
|
323
|
-
#
|
324
|
-
#
|
325
|
-
# {https://github.com/calabash/calabash-ios/issues/253}
|
374
|
+
# # Swipe left on first view match by "*"
|
375
|
+
# swipe(:left)
|
326
376
|
#
|
327
|
-
#
|
328
|
-
# swipe :
|
329
|
-
# @example
|
330
|
-
# swipe :down, offset:{x:10,y:50}, query:"MKMapView"
|
331
|
-
# @param {String} dir the direction to swipe (symbols can also be used).
|
332
|
-
# @param {Hash} options option for modifying the details of the touch.
|
333
|
-
# @option options {Hash} :offset (nil) optional offset to touch point. Offset supports an `:x` and `:y` key
|
334
|
-
# and causes the touch to be offset with `(x,y)` relative to the center (`center + (offset[:x], offset[:y])`).
|
335
|
-
# @option options {String} :query (nil) if specified, the swipe will be made relative to this query.
|
336
|
-
# @option options [Symbol] :force (nil) Indicates the force of the swipe.
|
337
|
-
# Valid values are :strong, :normal, :light.
|
377
|
+
# # Swipe up on 'my scroll view'
|
378
|
+
# swipe(:up, {:query => "* marked:'my scroll view'"})
|
338
379
|
#
|
339
|
-
# @
|
340
|
-
|
341
|
-
|
380
|
+
# @param {String, Symbol} direction The direction to swipe
|
381
|
+
# @param {Hash} options Options for modifying the details of the swipe.
|
382
|
+
# @option options {Hash} :offset (nil) optional offset to touch point.
|
383
|
+
# Offset supports an `:x` and `:y` key and causes the touch to be
|
384
|
+
# offset with `(x,y)` relative to the center.
|
385
|
+
# @option options {String} :query (nil) If specified, the swipe will be
|
386
|
+
# made on the first view matching this query. If this option is nil
|
387
|
+
# (the default), the swipe will happen on the first view matched by "*".
|
388
|
+
# @option options [Symbol] :force (normal) Indicates the force of the
|
389
|
+
# swipe. Valid values are :strong, :normal, :light.
|
390
|
+
#
|
391
|
+
# @return {Array<Hash>,String} An array with one element; the view that
|
392
|
+
# was swiped.
|
393
|
+
#
|
394
|
+
# @raise [ArgumentError] If :force is invalid.
|
395
|
+
# @raise [ArgumentError] If direction is invalid
|
396
|
+
def swipe(direction, options={})
|
397
|
+
merged_options = {
|
398
|
+
:query => nil,
|
399
|
+
:force => :normal
|
400
|
+
}.merge(options)
|
342
401
|
|
343
|
-
|
344
|
-
unless uia_available?
|
345
|
-
merged_options[:status_bar_orientation] = status_bar_orientation
|
346
|
-
end
|
402
|
+
merged_options[:direction] = direction.to_sym
|
347
403
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
404
|
+
if ![:up, :down, :left, :right].include?(merged_options[:direction])
|
405
|
+
raise ArgumentError, %Q[
|
406
|
+
Invalid direction argument: '#{direction}'.
|
407
|
+
|
408
|
+
Valid directions are: :up, :down, :left, and :right
|
409
|
+
|
410
|
+
]
|
354
411
|
end
|
355
412
|
|
356
|
-
|
413
|
+
if ![:light, :strong, :normal].include?(merged_options[:force])
|
414
|
+
raise ArgumentError, %Q[
|
415
|
+
Invalid force option: '#{merged_options[:force]}'.
|
416
|
+
|
417
|
+
Valid forces are: :strong, :normal, :light
|
418
|
+
|
419
|
+
]
|
420
|
+
end
|
421
|
+
|
422
|
+
launcher.automator.swipe(merged_options)
|
357
423
|
end
|
358
424
|
|
425
|
+
# Performs the "flick" gesture on the first view that matches `uiquery`.
|
426
|
+
#
|
427
|
+
# If the view is not visible it will fail with an error.
|
428
|
+
#
|
429
|
+
# If the view is animating it will *silently* fail.
|
430
|
+
#
|
431
|
+
# By default, the gesture starts at the center of the view and "flicks"
|
432
|
+
# according to `delta`.
|
433
|
+
#
|
434
|
+
# A flick is a swipe with velocity.
|
435
|
+
#
|
436
|
+
# @example
|
437
|
+
# # Flick left: move screen to the right
|
438
|
+
# delta = {:x => -124.0, :y => 0.0}
|
439
|
+
#
|
440
|
+
# # Flick right: move screen to the left
|
441
|
+
# delta = {:x => 124.0, :y => 0.0}
|
442
|
+
#
|
443
|
+
# # Flick up: move screen to the bottom
|
444
|
+
# delta = {:x => 0, :y => -124.0}
|
445
|
+
#
|
446
|
+
# # Flick down: move screen to the top
|
447
|
+
# delta = {:x => 0, :y => 124.0}
|
448
|
+
#
|
449
|
+
# # Flick up and to the left: move the screen to the lower right corner
|
450
|
+
# delta = {:x => -88, :y => -88}
|
451
|
+
#
|
452
|
+
# flick("MKMapView", delta)
|
453
|
+
#
|
454
|
+
# @param {String} uiquery query describing view to flick.
|
455
|
+
# @param {Hash} delta coordinate describing the direction to flick
|
456
|
+
# @param {Hash} options option for modifying the details of the flick.
|
457
|
+
# @option options {Hash} :offset (nil) optional offset to touch point.
|
458
|
+
# Offset supports an `:x` and `:y` key and causes the first touch to be
|
459
|
+
# offset with `(x,y)` relative to the center.
|
460
|
+
# @return {Array<Hash>} array containing the serialized version of the touched view.
|
461
|
+
#
|
462
|
+
# @raise [ArgumentError] If query is nil.
|
463
|
+
def flick(uiquery, delta, options={})
|
464
|
+
if uiquery.nil?
|
465
|
+
raise ArgumentError, "Query argument cannot be nil"
|
466
|
+
end
|
467
|
+
|
468
|
+
merged_options = {
|
469
|
+
:delta => delta
|
470
|
+
}.merge(options)
|
471
|
+
|
472
|
+
query_action_with_options(:flick, uiquery, merged_options)
|
473
|
+
end
|
359
474
|
|
360
|
-
# Performs the
|
361
|
-
#
|
475
|
+
# Performs the pan gesture between two coordinates.
|
476
|
+
#
|
477
|
+
# Swipes, scrolls, drag-and-drop, and flicks are all pan gestures.
|
478
|
+
#
|
362
479
|
# @example
|
363
|
-
#
|
364
|
-
#
|
480
|
+
# # Reorder table view rows.
|
481
|
+
# q1="* marked:'Reorder Apple'"
|
482
|
+
# q2="* marked:'Reorder Google'"
|
365
483
|
# pan q1, q2, duration:4
|
366
|
-
#
|
367
|
-
# @param {String}
|
368
|
-
# @
|
369
|
-
#
|
370
|
-
#
|
371
|
-
#
|
372
|
-
|
373
|
-
|
484
|
+
#
|
485
|
+
# @param {String} from_query query describing view to start the gesture
|
486
|
+
# @param {String} to_query query describing view to end the gesture
|
487
|
+
# @option options {Hash} :offset (nil) optional offset to touch point.
|
488
|
+
# Offset supports an `:x` and `:y` key and causes the pan to be offset
|
489
|
+
# with `(x,y)` relative to the center.
|
490
|
+
# @option options {Numeric} :duration (1.0) duration of the 'pan'. The
|
491
|
+
# minimum value of pan in UIAutomation is 0.5. For DeviceAgent, the
|
492
|
+
# duration must be > 0.
|
493
|
+
# @return {Array<Hash>} array containing the serialized version of the
|
494
|
+
# touched views. The first element is the first view matched by
|
495
|
+
# the from_query and the second element is the first view matched by
|
496
|
+
# the to_query.
|
497
|
+
#
|
498
|
+
# @raise [ArgumentError] If duration is < 0.5 for UIAutomation and <= 0
|
499
|
+
# for DeviceAgent.
|
500
|
+
def pan(from_query, to_query, options={})
|
501
|
+
merged_options = {
|
502
|
+
# Minimum value for UIAutomation is 0.5.
|
503
|
+
# DeviceAgent duration must be > 0.
|
504
|
+
:duration => 1.0
|
505
|
+
}.merge(options)
|
506
|
+
|
507
|
+
duration = merged_options[:duration]
|
508
|
+
|
509
|
+
if uia_available? && duration < 0.5
|
510
|
+
raise ArgumentError, %Q[
|
511
|
+
Invalid duration: #{duration}
|
512
|
+
|
513
|
+
The minimum duration is 0.5
|
514
|
+
|
515
|
+
]
|
516
|
+
elsif duration <= 0.0
|
517
|
+
raise ArgumentError, %Q[
|
518
|
+
Invalid duration: #{duration}
|
519
|
+
|
520
|
+
The minimum duration is 0.0.
|
521
|
+
|
522
|
+
]
|
523
|
+
end
|
524
|
+
|
525
|
+
launcher.automator.pan(from_query, to_query, merged_options)
|
526
|
+
end
|
527
|
+
|
528
|
+
# Performs the pan gesture between two coordinates.
|
529
|
+
#
|
530
|
+
# Swipes, scrolls, drag-and-drop, and flicks are all pan gestures.
|
531
|
+
#
|
532
|
+
# @example
|
533
|
+
# # Pan to go back in UINavigationController
|
534
|
+
# element = query("*").first
|
535
|
+
# y = element["rect"]["center_y"]
|
536
|
+
# pan_coordinates({10, y}, {160, y})
|
537
|
+
#
|
538
|
+
# # Pan to reveal Today and Notifications
|
539
|
+
# element = query("*").first
|
540
|
+
# x = element["rect"]["center_x"]
|
541
|
+
# pan_coordinates({x, 0}, {x, 240})
|
542
|
+
#
|
543
|
+
# # Pan to reveal Control Panel
|
544
|
+
# element = query("*").first
|
545
|
+
# x = element["rect"]["center_x"]
|
546
|
+
# y = element["rect"]["height"]
|
547
|
+
# pan_coordinates({x, height}, {x, 240})
|
548
|
+
#
|
549
|
+
# @param {Hash} from_point where to start the pan.
|
550
|
+
# @param {Hash} to_point where to end the pan.
|
551
|
+
# @option options {Numeric} :duration (1.0) duration of the 'pan'. The
|
552
|
+
# minimum value of pan in UIAutomation is 0.5. For DeviceAgent, the
|
553
|
+
# duration must be > 0.
|
554
|
+
#
|
555
|
+
# @raise [ArgumentError] If duration is < 0.5 for UIAutomation and <= 0
|
556
|
+
# for DeviceAgent.
|
557
|
+
def pan_coordinates(from_point, to_point, options={})
|
558
|
+
merged_options = {
|
559
|
+
# Minimum value for UIAutomation is 0.5.
|
560
|
+
# DeviceAgent duration must be > 0.
|
561
|
+
:duration => 1.0
|
562
|
+
}.merge(options)
|
563
|
+
|
564
|
+
duration = merged_options[:duration]
|
565
|
+
|
566
|
+
if uia_available? && duration < 0.5
|
567
|
+
raise ArgumentError, %Q[
|
568
|
+
Invalid duration: #{duration}
|
569
|
+
|
570
|
+
The minimum duration is 0.5
|
571
|
+
|
572
|
+
]
|
573
|
+
elsif duration <= 0.0
|
574
|
+
raise ArgumentError, %Q[
|
575
|
+
Invalid duration: #{duration}
|
576
|
+
|
577
|
+
The minimum duration is 0.0.
|
578
|
+
|
579
|
+
]
|
580
|
+
end
|
581
|
+
|
582
|
+
launcher.automator.pan_coordinates(from_point, to_point,
|
583
|
+
merged_options)
|
374
584
|
end
|
375
585
|
|
376
586
|
# Performs a "pinch" gesture.
|
@@ -387,16 +597,188 @@ module Calabash
|
|
387
597
|
# @option options {String} :query (nil) if specified, the pinch will be made relative to this query.
|
388
598
|
# @return {Array<Hash>,String} array containing the serialized version of the touched view if `options[:query]` is given.
|
389
599
|
def pinch(in_out, options={})
|
390
|
-
launcher.
|
600
|
+
launcher.automator.pinch(in_out.to_sym, options)
|
601
|
+
end
|
602
|
+
|
603
|
+
# @deprecated 0.21.0 Use #keyboard_enter_text
|
604
|
+
#
|
605
|
+
# Use keyboard to enter a character.
|
606
|
+
#
|
607
|
+
# @note
|
608
|
+
# There are several special 'characters', some of which do not appear on
|
609
|
+
# all keyboards; e.g. `Delete`, `Return`.
|
610
|
+
#
|
611
|
+
# @see #keyboard_enter_text
|
612
|
+
#
|
613
|
+
# @raise [RuntimeError] If there is no visible keyboard
|
614
|
+
# @raise [RuntimeError] If the keyboard (layout) is not supported
|
615
|
+
#
|
616
|
+
# @param [String] char The character to type
|
617
|
+
# @param [Hash] options Controls the behavior of the method.
|
618
|
+
# @option opts [Numeric] :wait_after_char (0.05) How long to sleep after
|
619
|
+
# typing a character.
|
620
|
+
def keyboard_enter_char(char, options={})
|
621
|
+
expect_keyboard_visible!
|
622
|
+
|
623
|
+
default_opts = {:wait_after_char => 0.05}
|
624
|
+
merged_options = default_opts.merge(options)
|
625
|
+
|
626
|
+
special_char = launcher.automator.char_for_keyboard_action(char)
|
627
|
+
|
628
|
+
if special_char
|
629
|
+
launcher.automator.enter_char_with_keyboard(special_char)
|
630
|
+
elsif char.length == 1
|
631
|
+
launcher.automator.enter_char_with_keyboard(char)
|
632
|
+
else
|
633
|
+
raise ArgumentError, %Q[
|
634
|
+
Expected '#{char}' to be a single character or a special string like:
|
635
|
+
|
636
|
+
* Return
|
637
|
+
* Delete
|
638
|
+
|
639
|
+
To type strings with more than one character, use keyboard_enter_text.
|
640
|
+
]
|
641
|
+
end
|
642
|
+
|
643
|
+
duration = merged_options[:wait_after_char]
|
644
|
+
if duration > 0
|
645
|
+
Kernel.sleep(duration)
|
646
|
+
end
|
647
|
+
|
648
|
+
[]
|
649
|
+
end
|
650
|
+
|
651
|
+
# Touches the keyboard action key.
|
652
|
+
#
|
653
|
+
# The action key depends on the keyboard. Some examples include:
|
654
|
+
#
|
655
|
+
# * Return
|
656
|
+
# * Next
|
657
|
+
# * Go
|
658
|
+
# * Join
|
659
|
+
# * Search
|
660
|
+
#
|
661
|
+
# @note
|
662
|
+
# Not all keyboards have an action key. For example, numeric keyboards
|
663
|
+
# do not have an action key.
|
664
|
+
#
|
665
|
+
# @raise [RuntimeError] If the keyboard is not visible.
|
666
|
+
def tap_keyboard_action_key
|
667
|
+
expect_keyboard_visible!
|
668
|
+
launcher.automator.tap_keyboard_action_key
|
669
|
+
end
|
670
|
+
|
671
|
+
# Touches the keyboard delete key.
|
672
|
+
#
|
673
|
+
# @raise [RuntimeError] If the keyboard is not visible.
|
674
|
+
def tap_keyboard_delete_key
|
675
|
+
expect_keyboard_visible!
|
676
|
+
launcher.automator.tap_keyboard_delete_key
|
677
|
+
end
|
678
|
+
|
679
|
+
# Uses the keyboard to enter text.
|
680
|
+
#
|
681
|
+
# @param [String] text the text to type.
|
682
|
+
# @raise [RuntimeError] If the keyboard is not visible.
|
683
|
+
def keyboard_enter_text(text)
|
684
|
+
expect_keyboard_visible!
|
685
|
+
existing_text = text_from_first_responder
|
686
|
+
escaped = existing_text.gsub("\n","\\n")
|
687
|
+
launcher.automator.enter_text_with_keyboard(text, escaped)
|
391
688
|
end
|
392
689
|
|
393
690
|
# @!visibility private
|
394
|
-
#
|
395
|
-
|
396
|
-
|
397
|
-
|
691
|
+
#
|
692
|
+
# Enters text into view identified by a query
|
693
|
+
#
|
694
|
+
# This behavior of this method depends on the Gesture::Performer
|
695
|
+
# implementation.
|
696
|
+
#
|
697
|
+
# ### UIAutomation
|
698
|
+
#
|
699
|
+
# defaults to calling 'setValue' in UIAutomation on the UITextField or
|
700
|
+
# UITextView. This is fast, but in some cases might result in slightly
|
701
|
+
# different behaviour than using `keyboard_enter_text`.
|
702
|
+
# To force use of #keyboard_enter_text option :use_keyboard
|
703
|
+
#
|
704
|
+
# ### DeviceAgent
|
705
|
+
#
|
706
|
+
# This method calls #keyboard_enter_text regardless of the options passed.
|
707
|
+
#
|
708
|
+
# @param [String] uiquery the element to enter text into
|
709
|
+
# @param [String] text the text to enter
|
710
|
+
# @param [Hash] options controls details of text entry
|
711
|
+
# @option options [Boolean] :use_keyboard (false) use the iOS keyboard
|
712
|
+
# to enter each character separately
|
713
|
+
# @option options [Boolean] :wait (true) call wait_for_element_exists with
|
714
|
+
# uiquery
|
715
|
+
# @option options [Hash] :wait_options ({}) if :wait pass this as options
|
716
|
+
# to wait_for_element_exists
|
717
|
+
def enter_text_in(uiquery, text, options = {})
|
718
|
+
default_opts = {:use_keyboard => false, :wait => true, :wait_options => {}}
|
719
|
+
options = default_opts.merge(options)
|
720
|
+
wait_for_element_exists(uiquery, options[:wait_options]) if options[:wait]
|
721
|
+
touch(uiquery, options)
|
722
|
+
wait_for_keyboard
|
723
|
+
if options[:use_keyboard]
|
724
|
+
keyboard_enter_text(text)
|
725
|
+
else
|
726
|
+
fast_enter_text(text)
|
398
727
|
end
|
399
|
-
|
728
|
+
end
|
729
|
+
|
730
|
+
alias_method :enter_text, :enter_text_in
|
731
|
+
|
732
|
+
# @!visibility private
|
733
|
+
#
|
734
|
+
# Enters text into current text input field
|
735
|
+
#
|
736
|
+
# This behavior of this method depends on the Gesture::Performer
|
737
|
+
# implementation.
|
738
|
+
#
|
739
|
+
# ### UIAutomation
|
740
|
+
#
|
741
|
+
# defaults to calling 'setValue' in UIAutomation on the UITextField or
|
742
|
+
# UITextView. This is fast, but in some cases might result in slightly
|
743
|
+
# different behaviour than using `keyboard_enter_text`.
|
744
|
+
# To force use of #keyboard_enter_text option :use_keyboard
|
745
|
+
#
|
746
|
+
# ### DeviceAgent
|
747
|
+
#
|
748
|
+
# This method calls #keyboard_enter_text.
|
749
|
+
#
|
750
|
+
# @param [String] text the text to enter
|
751
|
+
def fast_enter_text(text)
|
752
|
+
expect_keyboard_visible!
|
753
|
+
launcher.automator.fast_enter_text(text)
|
754
|
+
end
|
755
|
+
|
756
|
+
# Dismisses a iPad keyboard by touching the 'Hide keyboard' button and waits
|
757
|
+
# for the keyboard to disappear.
|
758
|
+
#
|
759
|
+
# @note
|
760
|
+
# the dismiss keyboard key does not exist on the iPhone or iPod
|
761
|
+
#
|
762
|
+
# @raise [RuntimeError] If the device is not an iPad
|
763
|
+
# @raise [Calabash::Cucumber::WaitHelpers::WaitError] If the keyboard does
|
764
|
+
# not disappear.
|
765
|
+
def dismiss_ipad_keyboard
|
766
|
+
# TODO Maybe relax this restriction; turn it into a nop on iPhones?
|
767
|
+
# TODO Support iPhone 6 Plus form factor dismiss keyboard key.
|
768
|
+
if device_family_iphone?
|
769
|
+
screenshot_and_raise %Q[
|
770
|
+
There is no Hide Keyboard key on an iPhone.
|
771
|
+
|
772
|
+
Use `ipad?` to branch in your test.
|
773
|
+
|
774
|
+
]
|
775
|
+
end
|
776
|
+
|
777
|
+
expect_keyboard_visible!
|
778
|
+
|
779
|
+
launcher.automator.dismiss_ipad_keyboard
|
780
|
+
|
781
|
+
wait_for_no_keyboard
|
400
782
|
end
|
401
783
|
|
402
784
|
# Scroll a scroll view in a direction. By default scrolls half the frame size.
|
@@ -657,7 +1039,7 @@ module Calabash
|
|
657
1039
|
# Sends the app to the background.
|
658
1040
|
#
|
659
1041
|
# Sending the app to the background for more than 60 seconds may
|
660
|
-
# cause
|
1042
|
+
# cause unpredictable results.
|
661
1043
|
#
|
662
1044
|
# @param [Numeric] seconds How long to send the app to the background.
|
663
1045
|
# @raise [ArgumentError] if `seconds` argument is < 1.0
|
@@ -915,9 +1297,20 @@ arguments => '#{arguments}'
|
|
915
1297
|
:exit_code => merged_opts[:exit_code]
|
916
1298
|
}
|
917
1299
|
)
|
918
|
-
|
1300
|
+
|
1301
|
+
rescue Errno::ECONNREFUSED, HTTPClient::KeepAliveDisconnected, SocketError
|
919
1302
|
[]
|
920
1303
|
end
|
1304
|
+
|
1305
|
+
if launcher.automator
|
1306
|
+
if launcher.automator.name == :device_agent
|
1307
|
+
delay = merged_opts[:post_resign_active_delay] +
|
1308
|
+
merged_opts[:post_will_terminate_delay] + 0.4
|
1309
|
+
sleep(delay)
|
1310
|
+
launcher.automator.send(:session_delete)
|
1311
|
+
end
|
1312
|
+
end
|
1313
|
+
true
|
921
1314
|
end
|
922
1315
|
|
923
1316
|
# Get the Calabash server log level.
|
@@ -953,11 +1346,14 @@ arguments => '#{arguments}'
|
|
953
1346
|
launcher
|
954
1347
|
end
|
955
1348
|
|
956
|
-
# Helper method to easily create page object instances from a cucumber
|
957
|
-
#
|
958
|
-
#
|
959
|
-
#
|
960
|
-
#
|
1349
|
+
# Helper method to easily create page object instances from a cucumber
|
1350
|
+
# execution context.
|
1351
|
+
#
|
1352
|
+
# The advantage of using `page` to instantiate a page object class is that
|
1353
|
+
# it will automatically store a reference to the current Cucumber world
|
1354
|
+
# which is needed in the page object methods to call Cucumber-specific
|
1355
|
+
# methods like puts or embed.
|
1356
|
+
#
|
961
1357
|
# @example Instantiating a `LoginPage` from a step definition
|
962
1358
|
# Given(/^I am about to login to a self-hosted site$/) do
|
963
1359
|
# @current_page = page(LoginPage).await(timeout: 30)
|
@@ -965,9 +1361,12 @@ arguments => '#{arguments}'
|
|
965
1361
|
# end
|
966
1362
|
#
|
967
1363
|
# @see Calabash::IBase
|
968
|
-
# @param {Class} clz the page object class to instantiate (passing the
|
969
|
-
#
|
970
|
-
# @
|
1364
|
+
# @param {Class} clz the page object class to instantiate (passing the
|
1365
|
+
# Cucumber world and `args`)
|
1366
|
+
# @param {Array} args optional additional arguments to pass to the page
|
1367
|
+
# object constructor
|
1368
|
+
# @return {Object} a fresh instance of `Class clz` which has been passed
|
1369
|
+
# a reference to the cucumber World object.
|
971
1370
|
def page(clz,*args)
|
972
1371
|
clz.new(self,*args)
|
973
1372
|
end
|
@@ -1181,42 +1580,137 @@ arguments => '#{arguments}'
|
|
1181
1580
|
launcher.attach({:uia_strategy => uia_strategy})
|
1182
1581
|
end
|
1183
1582
|
|
1583
|
+
# Returns an object that provides an interface to the DeviceAgent
|
1584
|
+
# public query and gesture API.
|
1585
|
+
#
|
1586
|
+
# @see Calabash::Cucumber::DeviceAgent
|
1587
|
+
#
|
1588
|
+
# @example
|
1589
|
+
# device_agent.query({marked: "Cancel"})
|
1590
|
+
# device_agent.touch({marked: "Cancel"})
|
1591
|
+
#
|
1592
|
+
# @return [Calabash::Cucumber::DeviceAgent]
|
1593
|
+
#
|
1594
|
+
# @raise [RuntimeError] If the application has not been launched.
|
1595
|
+
# @raise [RuntimeError] If there is no automator attached to the
|
1596
|
+
# current launcher
|
1597
|
+
# @raise [RuntimeError] If the automator attached the current
|
1598
|
+
# launcher is not DeviceAgent
|
1599
|
+
# @raise [RuntimeError] If the automator is not running.
|
1600
|
+
def device_agent
|
1601
|
+
launcher = Calabash::Cucumber::Launcher.launcher_if_used
|
1602
|
+
if !launcher
|
1603
|
+
raise RuntimeError, %Q[
|
1604
|
+
There is no launcher.
|
1605
|
+
|
1606
|
+
If you are in the Calabash console, you can try to attach to an already running
|
1607
|
+
Calabash test using:
|
1608
|
+
|
1609
|
+
> console_attach
|
1610
|
+
|
1611
|
+
If you are running from Cucumber or rspec, call Launcher#relaunch before calling
|
1612
|
+
this method.
|
1613
|
+
|
1614
|
+
]
|
1615
|
+
end
|
1616
|
+
|
1617
|
+
if !launcher.automator
|
1618
|
+
raise RuntimeError, %Q[
|
1619
|
+
The launcher is not attached to an automator.
|
1620
|
+
|
1621
|
+
If you are in the Calabash console, you can try to attach to an already running
|
1622
|
+
Calabash test using:
|
1623
|
+
|
1624
|
+
> console_attach
|
1625
|
+
|
1626
|
+
If you are running from Cucumber or rspec, call Launcher#relaunch before calling
|
1627
|
+
this method.
|
1628
|
+
|
1629
|
+
]
|
1630
|
+
end
|
1631
|
+
|
1632
|
+
if launcher.automator.name != :device_agent
|
1633
|
+
raise RuntimeError, %Q[
|
1634
|
+
The launcher automator is not DeviceAgent:
|
1635
|
+
|
1636
|
+
#{launcher.automator}
|
1637
|
+
|
1638
|
+
#device_agent is only available for Xcode 8.
|
1639
|
+
|
1640
|
+
In your tests, use this pattern to branch on the availability of DeviceAgent.
|
1641
|
+
|
1642
|
+
if uia_available?
|
1643
|
+
# Make a UIA call
|
1644
|
+
else
|
1645
|
+
# Make a DeviceAgent call
|
1646
|
+
end
|
1647
|
+
|
1648
|
+
]
|
1649
|
+
end
|
1650
|
+
automator = launcher.automator
|
1651
|
+
|
1652
|
+
if !automator.running?
|
1653
|
+
raise RuntimeError, %Q[The DeviceAgent is not running.]
|
1654
|
+
else
|
1655
|
+
require "calabash-cucumber/device_agent"
|
1656
|
+
Calabash::Cucumber::DeviceAgent.new(automator.client, self)
|
1657
|
+
end
|
1658
|
+
end
|
1659
|
+
|
1184
1660
|
# @!visibility private
|
1661
|
+
# TODO should be private
|
1185
1662
|
def launcher
|
1186
1663
|
Calabash::Cucumber::Launcher.launcher
|
1187
1664
|
end
|
1188
1665
|
|
1189
1666
|
# @!visibility private
|
1667
|
+
# TODO should be private
|
1190
1668
|
def run_loop
|
1191
|
-
|
1192
|
-
|
1669
|
+
launcher = Calabash::Cucumber::Launcher.launcher_if_used
|
1670
|
+
if launcher
|
1671
|
+
launcher.run_loop
|
1672
|
+
else
|
1673
|
+
nil
|
1674
|
+
end
|
1193
1675
|
end
|
1194
1676
|
|
1195
1677
|
# @!visibility private
|
1196
1678
|
def tail_run_loop_log
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1679
|
+
if !run_loop
|
1680
|
+
raise "Unable to tail instruments log because there is no active run-loop"
|
1681
|
+
end
|
1682
|
+
|
1683
|
+
require "calabash-cucumber/log_tailer"
|
1684
|
+
|
1685
|
+
if launcher.instruments?
|
1686
|
+
Calabash::Cucumber::LogTailer.tail_in_terminal(run_loop[:log_file])
|
1687
|
+
else
|
1688
|
+
# TODO Tail the .run_loop/xcuitest/<launcher>.log?
|
1689
|
+
raise "Cannot tail a non-instruments run-loop"
|
1200
1690
|
end
|
1201
|
-
cmd = %Q[osascript -e 'tell application "Terminal" to do script "tail -n 10000 -f #{l[:log_file]} | grep -v \\"Default: \\\\*\\""']
|
1202
|
-
raise "Unable to " unless system(cmd)
|
1203
1691
|
end
|
1204
1692
|
|
1205
1693
|
# @!visibility private
|
1206
1694
|
def dump_run_loop_log
|
1207
|
-
|
1208
|
-
|
1209
|
-
raise 'Unable to dump run_loop since there is not active run_loop...'
|
1695
|
+
if !run_loop
|
1696
|
+
raise "Unable to dump run-loop log because there is no active run-loop"
|
1210
1697
|
end
|
1211
|
-
cmd = %Q[cat "#{l[:log_file]}" | grep -v "Default: \\*\\*\\*"]
|
1212
|
-
puts `#{cmd}`
|
1213
|
-
end
|
1214
1698
|
|
1699
|
+
if launcher.instruments?
|
1700
|
+
cmd = %Q[cat "#{run_loop[:log_file]}" | grep -v "Default: \\*\\*\\*"]
|
1701
|
+
RunLoop.log_unix_cmd(cmd)
|
1702
|
+
puts `#{cmd}`
|
1703
|
+
true
|
1704
|
+
else
|
1705
|
+
# TODO What should we dump in non-instruments runs?
|
1706
|
+
raise "Cannot dump non-instruments run-loop"
|
1707
|
+
end
|
1708
|
+
end
|
1215
1709
|
|
1216
1710
|
# @!visibility private
|
1217
1711
|
def query_action_with_options(action, uiquery, options)
|
1218
1712
|
uiquery, options = extract_query_and_options(uiquery, options)
|
1219
|
-
views_touched = launcher.
|
1713
|
+
views_touched = launcher.automator.send(action, options)
|
1220
1714
|
unless uiquery.nil?
|
1221
1715
|
msg = "#{action} could not find view: '#{uiquery}', args: #{options}"
|
1222
1716
|
Map.assert_map_results(views_touched, msg)
|