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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/dylibs/libCalabashDyn.dylib +0 -0
  3. data/dylibs/libCalabashDynSim.dylib +0 -0
  4. data/lib/calabash-cucumber.rb +9 -2
  5. data/lib/calabash-cucumber/abstract.rb +23 -0
  6. data/lib/calabash-cucumber/automator/automator.rb +158 -0
  7. data/lib/calabash-cucumber/automator/coordinates.rb +401 -0
  8. data/lib/calabash-cucumber/automator/device_agent.rb +424 -0
  9. data/lib/calabash-cucumber/automator/instruments.rb +441 -0
  10. data/lib/calabash-cucumber/connection_helpers.rb +1 -0
  11. data/lib/calabash-cucumber/core.rb +632 -138
  12. data/lib/calabash-cucumber/device_agent.rb +346 -0
  13. data/lib/calabash-cucumber/dot_dir.rb +1 -0
  14. data/lib/calabash-cucumber/environment.rb +1 -0
  15. data/lib/calabash-cucumber/environment_helpers.rb +4 -3
  16. data/lib/calabash-cucumber/http/http.rb +6 -4
  17. data/lib/calabash-cucumber/keyboard_helpers.rb +97 -679
  18. data/lib/calabash-cucumber/launcher.rb +107 -31
  19. data/lib/calabash-cucumber/log_tailer.rb +46 -0
  20. data/lib/calabash-cucumber/map.rb +7 -1
  21. data/lib/calabash-cucumber/rotation_helpers.rb +47 -139
  22. data/lib/calabash-cucumber/status_bar_helpers.rb +51 -20
  23. data/lib/calabash-cucumber/store/preferences.rb +3 -0
  24. data/lib/calabash-cucumber/uia.rb +333 -2
  25. data/lib/calabash-cucumber/usage_tracker.rb +2 -0
  26. data/lib/calabash-cucumber/version.rb +2 -2
  27. data/lib/calabash-cucumber/wait_helpers.rb +2 -0
  28. data/lib/calabash/formatters/html.rb +6 -1
  29. data/lib/frank-calabash.rb +10 -4
  30. data/scripts/.irbrc +3 -0
  31. data/staticlib/calabash.framework.zip +0 -0
  32. data/staticlib/libFrankCalabash.a +0 -0
  33. metadata +11 -6
  34. data/lib/calabash-cucumber/actions/instruments_actions.rb +0 -155
@@ -3,6 +3,7 @@ require 'calabash-cucumber/connection'
3
3
  module Calabash
4
4
  module Cucumber
5
5
 
6
+ # @!visibility private
6
7
  class ResponseError < RuntimeError ; end
7
8
 
8
9
  # @!visibility private
@@ -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 result of calling 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 animating.
185
- # If the view is not visible `touch` will fail. If the view is animating
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
- # @param {String} uiquery query describing view to tap. Note `nil` is allowed and is interpreted as
192
- # `tap_point(options[:offset][:x],options[:offset][:y])`
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. Offset supports an `:x` and `:y` key
195
- # and causes the touch to be offset with `(x,y)` relative to the center (`center + (offset[:x], offset[:y])`).
196
- # @return {Array<Hash>} array containing the serialized version of the tapped view.
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? && options[:offset].nil?
199
- raise "called touch(nil) without specifying an offset in options (#{options})"
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 animating
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. Offset supports an `:x` and `:y` key
249
- # and causes the touch to be offset with `(x,y)` relative to the center (`center + (offset[:x], offset[:y])`).
250
- # @return {Array<Hash>} array containing the serialized version of the tapped view.
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 "flick" gesture on the (first) view that matches
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 animating
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. Offset supports an `:x` and `:y` key
310
- # and causes the touch to be offset with `(x,y)` relative to the center (`center + (offset[:x], offset[:y])`).
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 touched view.
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
- # @todo `swipe` is an old style API which doesn't take a query as its
321
- # first argument. We should migrate this.
372
+ # @example
322
373
  #
323
- # @note Due to a bug in Apple's UIAutomation, swipe is broken on certain
324
- # views in the iOS Simulator. Swiping works on devices.
325
- # {https://github.com/calabash/calabash-ios/issues/253}
374
+ # # Swipe left on first view match by "*"
375
+ # swipe(:left)
326
376
  #
327
- # @example
328
- # swipe :left
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
- # @return {Array<Hash>,String} array containing the serialized version of the touched view if `options[:query]` is given.
340
- def swipe(dir, options={})
341
- merged_options = options.dup
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
- # I don't understand why the :status_bar_orientation value is being overwritten
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
- force = merged_options[:force]
349
- if force
350
- unless [:light, :strong, :normal].include?(force)
351
- raise ArgumentError,
352
- "Expected :force option '#{force}' to be :light, :strong, or :normal"
353
- end
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
- launcher.actions.swipe(dir.to_sym, merged_options)
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 "pan" or "drag-n-drop" gesture on from the `from` parameter
361
- # to the `to` parameter (both are queries).
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
- # q1="* marked:'Cell 3' parent tableViewCell descendant tableViewCellReorderControl"
364
- # q2="* marked:'Cell 6' parent tableViewCell descendant tableViewCellReorderControl"
480
+ # # Reorder table view rows.
481
+ # q1="* marked:'Reorder Apple'"
482
+ # q2="* marked:'Reorder Google'"
365
483
  # pan q1, q2, duration:4
366
- # @param {String} from query describing view to start the gesture
367
- # @param {String} to query describing view to end the gesture
368
- # @option options {Hash} :offset (nil) optional offset to touch point. Offset supports an `:x` and `:y` key
369
- # and causes the touch to be offset with `(x,y)` relative to the center (`center + (offset[:x], offset[:y])`).
370
- # @option options {Numeric} :duration (1) duration of the 'pan'.
371
- # @return {Array<Hash>} array containing the serialized version of the touched view.
372
- def pan(from, to, options={})
373
- launcher.actions.pan(from, to, options)
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.actions.pinch(in_out.to_sym,options)
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
- # @deprecated
395
- def cell_swipe(options={})
396
- if uia_available?
397
- raise 'cell_swipe not supported with instruments, simply use swipe with a query that matches the cell'
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
- playback('cell_swipe', options)
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 unpredicatable results.
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
- rescue Errno::ECONNREFUSED, HTTPClient::KeepAliveDisconnected
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 execution context.
957
- # The advantage of using `page` to instantiate a page object class is that it
958
- # will automatically store a reference to the current Cucumber world
959
- # which is needed in the page object methods to call Cucumber-specific methods
960
- # like puts or embed.
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 cucumber world and `args`)
969
- # @param {Array} args optional additional arguments to pass to the page object constructor
970
- # @return {Object} a fresh instance of `Class clz` which has been passed a reference to the cucumber World object.
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
- l = Calabash::Cucumber::Launcher.launcher_if_used
1192
- l && l.run_loop
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
- l = run_loop
1198
- unless l
1199
- raise 'Unable to tail run_loop since there is not active run_loop...'
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
- l = run_loop
1208
- unless l
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.actions.send(action, options)
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)