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.
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)