-
Notifications
You must be signed in to change notification settings - Fork 41
/
client.rb
1766 lines (1473 loc) · 51.3 KB
/
client.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
module RunLoop
# @!visibility private
module DeviceAgent
# @!visibility private
class Client
require "run_loop/shell"
include RunLoop::Shell
require "run_loop/encoding"
include RunLoop::Encoding
require "run_loop/cache"
require "run_loop/dylib_injector"
class HTTPError < RuntimeError; end
# @!visibility private
#
# These defaults may change at any time.
#
# You can override these values if they do not work in your environment.
#
# For cucumber users, the best place to override would be in your
# features/support/env.rb.
#
# For example:
#
# RunLoop::DeviceAgent::Client::DEFAULTS[:http_timeout] = 60
# RunLoop::DeviceAgent::Client::DEFAULTS[:device_agent_install_timeout] = 120
DEFAULTS = {
:port => 27753,
:simulator_ip => "127.0.0.1",
:http_timeout => (RunLoop::Environment.ci? || RunLoop::Environment.xtc?) ? 120 : 20,
:route_version => "1.0",
# Ignored in the XTC.
# This key is subject to removal or changes
:device_agent_install_timeout => RunLoop::Environment.ci? ? 240 : 120,
# This value must always be false on the XTC.
# This is should only be used by gem maintainers or very advanced users.
:shutdown_device_agent_before_launch => false,
# This value controls whether or not DeviceAgent should terminate the
# the Application Under Test (AUT) when a new testing session is
# started. The default behavior is to _not_ terminate the AUT if it
# is already running. If you want your next test to start with your
# application in a freshly launched state, set this option to true.
#
# If the AUT is not running, DeviceAgent performs no action.
:terminate_aut_before_test => false,
# This value was derived empirically by typing hundreds of strings
# using XCUIElement#typeText. It corresponds to the DeviceAgent
# constant CBX_DEFAULT_SEND_STRING_FREQUENCY which is 60. _Decrease_
# this value if you are timing out typing strings.
:characters_per_second => 12,
# The number of attempts for relaunch DeviceAgent
# when health check is failed
:device_agent_launch_retries => 3
}
AUT_LAUNCHED_BY_RUN_LOOP_ARG = "LAUNCHED_BY_RUN_LOOP"
# @!visibility private
#
# These defaults may change at any time.
#
# You can override these values if they do not work in your environment.
#
# For cucumber users, the best place to override would be in your
# features/support/env.rb.
#
# For example:
#
# RunLoop::DeviceAgent::Client::WAIT_DEFAULTS[:timeout] = 30
WAIT_DEFAULTS = {
timeout: (RunLoop::Environment.ci? ||
RunLoop::Environment.xtc?) ? 30 : 15,
# This key is subject to removal or changes.
retry_frequency: 0.1,
# This key is subject to removal or changes.
exception_class: Timeout::Error
}
# @!visibility private
def self.run(options={})
simctl = options[:sim_control] || options[:simctl] || RunLoop::Simctl.new
xcode = options[:xcode] || RunLoop::Xcode.new
instruments = options[:instruments] || RunLoop::Instruments.new
# Find the Device under test, the App under test, and reset options.
device = RunLoop::Device.detect_device(options, xcode, simctl, instruments)
app_details = RunLoop::DetectAUT.detect_app_under_test(options)
reset_options = RunLoop::Core.send(:detect_reset_options, options)
app = app_details[:app]
bundle_id = app_details[:bundle_id]
# process name and dylib path
dylib_injection_details = Client.details_for_dylib_injection(device,
options,
app_details)
default_options = {
:xcode => xcode
}
merged_options = default_options.merge(options)
if device.simulator? && app
RunLoop::Core.expect_simulator_compatible_arch(device, app)
# Enable or disable keyboard autocorrection, caps lock and
# autocapitalization when running on simulator, disables these value by default
# unless user don't pass true values for these keys
sim_keyboard = RunLoop::SimKeyboardSettings.new(device)
sim_keyboard.enable_autocorrection(options[:autocorrection_enabled])
sim_keyboard.enable_caps_lock(options[:capslock_enabled])
sim_keyboard.enable_autocapitalization(options[:autocapitalization_enabled])
if merged_options[:relaunch_simulator]
RunLoop.log_debug("Detected :relaunch_simulator option; will force simulator to restart")
RunLoop::CoreSimulator.quit_simulator
end
core_sim = RunLoop::CoreSimulator.new(device, app, merged_options)
if reset_options
core_sim.reset_app_sandbox
end
core_sim.install
end
if !RunLoop::Environment.xtc?
if device.physical_device? && app
if reset_options
idm = RunLoop::PhysicalDevice::IOSDeviceManager.new(device)
idm.reset_app_sandbox(app)
end
end
end
cbx_launcher = Client.detect_cbx_launcher(merged_options, device)
code_sign_identity = options[:code_sign_identity]
if !code_sign_identity
code_sign_identity = RunLoop::Environment::code_sign_identity
end
provisioning_profile = options[:provisioning_profile]
if !provisioning_profile
provisioning_profile = RunLoop::Environment::provisioning_profile
end
install_timeout = options.fetch(:device_agent_install_timeout,
DEFAULTS[:device_agent_install_timeout])
shutdown_device_agent_before_launch = options.fetch(:shutdown_device_agent_before_launch,
DEFAULTS[:shutdown_device_agent_before_launch])
terminate_aut_before_test = options.fetch(:terminate_aut_before_test,
DEFAULTS[:terminate_aut_before_test])
device_agent_launch_retries = options.fetch(:device_agent_launch_retries,
DEFAULTS[:device_agent_launch_retries])
aut_args = options.fetch(:args, [])
aut_env = options.fetch(:env, {})
if !aut_args.include?(AUT_LAUNCHED_BY_RUN_LOOP_ARG)
aut_args << AUT_LAUNCHED_BY_RUN_LOOP_ARG
end
launcher_options = {
code_sign_identity: code_sign_identity,
provisioning_profile: provisioning_profile,
device_agent_install_timeout: install_timeout,
shutdown_device_agent_before_launch: shutdown_device_agent_before_launch,
terminate_aut_before_test: terminate_aut_before_test,
dylib_injection_details: dylib_injection_details,
aut_args: aut_args,
aut_env: aut_env,
device_agent_launch_retries: device_agent_launch_retries
}
xcuitest = RunLoop::DeviceAgent::Client.new(bundle_id, device,
cbx_launcher, launcher_options)
xcuitest.launch
if !RunLoop::Environment.xtc?
cache = {
:udid => device.udid,
:app => bundle_id,
:automator => :device_agent,
:code_sign_identity => code_sign_identity,
:provisioning_profile => provisioning_profile,
:launcher => cbx_launcher.name,
:launcher_pid => xcuitest.launcher_pid,
:launcher_options => xcuitest.launcher_options
}
RunLoop::Cache.default.write(cache)
end
xcuitest
end
# @!visibility private
#
# @param [RunLoop::Device] device the device under test
def self.default_cbx_launcher(device)
RunLoop::DeviceAgent::IOSDeviceManager.new(device)
end
# @!visibility private
# @param [Hash] options the options passed by the user
# @param [RunLoop::Device] device the device under test
def self.detect_cbx_launcher(options, device)
value = options[:cbx_launcher]
if value
if value == :xcodebuild
RunLoop::DeviceAgent::Xcodebuild.new(device)
elsif value == :ios_device_manager
RunLoop::DeviceAgent::IOSDeviceManager.new(device)
else
raise(ArgumentError,
"Expected :cbx_launcher => #{value} to be :xcodebuild or :ios_device_manager")
end
else
Client.default_cbx_launcher(device)
end
end
def self.details_for_dylib_injection(device, options, app_details)
dylib_path = RunLoop::DylibInjector.dylib_path_from_options(options)
return nil if !dylib_path
if device.physical_device?
raise ArgumentError, %Q[
Detected :inject_dylib option when targeting a physical device:
#{device}
Injecting the Calabash iOS Server is not supported on physical devices.
]
end
app = app_details[:app]
bundle_id = app_details[:bundle_id]
details = { dylib_path: dylib_path }
if !app
# Special case handling of the Settings.app
if bundle_id == "com.apple.Preferences"
details[:process_name] = "Preferences"
else
raise ArgumentError, %Q[
Detected :inject_dylib option, but the target application is a bundle identifier:
app: #{bundle_id}
To use dylib injection, you must provide a path to an .app bundle.
]
end
else
details[:process_name] = app.executable_name
end
details
end
=begin
INSTANCE METHODS
=end
attr_reader :bundle_id, :device, :cbx_launcher, :launcher_options, :launcher_pid
# @!visibility private
#
# The app with `bundle_id` needs to be installed.
#
# @param [String] bundle_id The identifier of the app under test.
# @param [RunLoop::Device] device The device under test.
# @param [RunLoop::DeviceAgent::LauncherStrategy] cbx_launcher The entity that
# launches the CBXRunner.
def initialize(bundle_id, device, cbx_launcher, launcher_options)
@bundle_id = bundle_id
@device = device
@cbx_launcher = cbx_launcher
@launcher_options = launcher_options
if !@launcher_options[:device_agent_install_timeout]
default = DEFAULTS[:device_agent_install_timeout]
@launcher_options[:device_agent_install_timeout] = default
end
if !@launcher_options[:device_agent_launch_retries]
default = DEFAULTS[:device_agent_launch_retries]
@launcher_options[:device_agent_launch_retries] = default
end
end
# @!visibility private
def to_s
"#<DeviceAgent #{url} : #{bundle_id} : #{device} : #{cbx_launcher}>"
end
# @!visibility private
def inspect
to_s
end
def launcher_options!(new_options)
@launcher_options = new_options.dup
end
# @!visibility private
def launch
start = Time.now
launch_cbx_runner
launch_aut
elapsed = Time.now - start
RunLoop.log_debug("Took #{elapsed} seconds to launch #{bundle_id} on #{device}")
true
end
# @!visibility private
def running?
begin
health(ping_options)
rescue => _
nil
end
end
# @!visibility private
def stop
if RunLoop::Environment.xtc?
RunLoop.log_error("Calling shutdown on the XTC is not supported.")
return
end
begin
shutdown
rescue => _
nil
end
end
# @!visibility private
#
# Experimental!
#
# This will launch the other app using the same arguments and environment
# as the AUT.
def launch_other_app(bundle_id)
launch_aut(bundle_id)
end
# @!visibility private
def device_info
options = http_options
request = request("device")
client = http_client(options)
response = client.get(request)
expect_300_response(response)
end
# @!visibility private
def server_version
options = http_options
request = request("version")
client = http_client(options)
response = client.get(request)
expect_300_response(response)
end
# @!visibility private
def tree
options = tree_http_options
request = request("tree")
client = http_client(options)
response = client.get(request)
expect_300_response(response)
end
# @!visibility private
def keyboard_visible?
options = http_options
parameters = { :type => "Keyboard" }
request = request("query", parameters)
client = http_client(options)
response = client.post(request)
hash = expect_300_response(response)
result = hash["result"]
return false if result.count == 0
return false if result[0].count == 0
element = result[0]
hit_point = element["hit_point"]
hit_point["x"] != -1 && hit_point["y"] != -1
end
# @!visibility private
def clear_text
# Tries to touch the keyboard delete key, but falls back on typing the
# backspace character.
options = enter_text_http_options("\b")
parameters = {
:gesture => "clear_text"
}
request = request("gesture", parameters)
client = http_client(options)
response = client.post(request)
expect_300_response(response)
end
# @!visibility private
def enter_text(string)
if !keyboard_visible?
raise RuntimeError, "Keyboard must be visible"
end
options = enter_text_http_options(string.to_s)
parameters = {
:gesture => "enter_text",
:options => {
:string => string.to_s
}
}
request = request("gesture", parameters)
client = http_client(options)
response = client.post(request)
expect_300_response(response)
end
# @!visibility private
#
# Some clients are performing keyboard checks _before_ calling #enter_text.
#
# 1. Removes duplicate check.
# 2. It turns out DeviceAgent query can be very slow.
def enter_text_without_keyboard_check(string)
options = enter_text_http_options(string.to_s)
parameters = {
:gesture => "enter_text",
:options => {
:string => string.to_s
}
}
request = request("gesture", parameters)
client = http_client(options)
response = client.post(request)
expect_300_response(response)
end
ALLOWED_KEYS = [:all,
:id,
:index,
:marked,
:text,
:type,
:descendant_element].freeze
# @!visibility private
#
# @example
# query({id: "login", :type "Button"})
#
# query({marked: "login"})
#
# query({marked: "login", type: "TextField"})
#
# query({type: "Button", index: 2})
#
# query({text: "Log in"})
#
# query({id: "hidden button", :all => true})
#
# query({descendant_element: { parent_type: 'Keyboard',
# descendant_type: 'Button' }})
#
# # Escaping single quote is not necessary, but supported.
# query({text: "Karl's problem"})
# query({text: "Karl\'s problem"})
#
# # Escaping double quote is not necessary, but supported.
# query({text: "\"To know is not enough.\""})
# query({text: %Q["To know is not enough."]})
#
# # Equivalent to Calabash query("*")
# query({})
#
# # Equivalent to Calabash query("all *")
# query({all: true})
#
# Querying for text with newlines is not supported yet.
#
# The query language supports the following keys:
# * :marked - accessibilityIdentifier, accessibilityLabel, text, and value
# * :id - accessibilityIdentifier
# * :type - an XCUIElementType shorthand, e.g. XCUIElementTypeButton =>
# Button. See the link below for available types. Note, however that
# some XCUIElementTypes are not available on iOS.
# * :index - Applied after all other specifiers.
# * :all - Filter the result by visibility. Defaults to false. See the
# discussion below about visibility.
#
# ### Visibility
#
# The rules for visibility are:
#
# 1. If any part of the view is visible, the visible.
# 2. If the view has alpha 0, it is not visible.
# 3. If the view has a size (0,0) it is not visible.
# 4. If the view is not within the bounds of the screen, it is not visible.
#
# Visibility is determined using the "hitable" XCUIElement property.
# XCUITest, particularly under Xcode 7, is not consistent about setting
# the "hitable" property correctly. Views that are not "hitable" might
# respond to gestures.
#
# Regarding rule #1 - this is different from the Calabash iOS and Android
# definition of visibility which requires the mid-point of the view to be
# visible.
#
# ### Results
#
# Results are returned as an Array of Hashes.
#
# ```
# [
# {
# "enabled": true,
# "id": "mostly hidden button",
# "hitable": true,
# "rect": {
# "y": 459,
# "x": 24,
# "height": 25,
# "width": 100
# },
# "label": "Mostly Hidden",
# "type": "Button",
# "hit_point": {
# "x": 25,
# "y": 460
# },
# "test_id": 1
# }
# ]
# ```
#
# @see https://developer.apple.com/documentation/xctest/xcuielementtype
# @param [Hash] uiquery A hash describing the query.
# @return [Array<Hash>] An array of elements matching the `uiquery`.
def query(uiquery)
merged_options = {
all: false
}.merge(uiquery)
unknown_keys = uiquery.keys - ALLOWED_KEYS
formatted_keys = ALLOWED_KEYS.map { |key| ":#{key}" }.join(", ")
if !unknown_keys.empty?
raise ArgumentError, %Q[
Unsupported key or keys found: '#{unknown_keys}'.
Allowed keys for a query are:
#{formatted_keys}
]
end
if _wildcard_query?(uiquery)
elements = _flatten_tree
else
parameters = merged_options.dup.tap { |hs| hs.delete(:all) }
if parameters.empty?
raise ArgumentError, %Q[
Query must contain at least one of these keys:
#{formatted_keys}
]
end
request = request("query", parameters)
client = http_client(http_options)
RunLoop.log_debug %Q[Sending query with parameters:
#{JSON.pretty_generate(parameters)}
]
response = client.post(request)
hash = expect_300_response(response)
elements = hash["result"]
end
if merged_options[:all]
elements
else
elements.select do |element|
element["hitable"]
end
end
end
# @!visibility private
def alert
parameters = { :type => "Alert" }
request = request("query", parameters)
client = http_client(http_options)
response = client.post(request)
hash = expect_300_response(response)
hash["result"]
end
# @!visibility private
def alert_visible?
!alert.empty?
end
# @!visibility private
def springboard_alert
request = request("springboard-alert")
client = http_client(http_options)
response = client.get(request)
expect_300_response(response)
end
# @!visibility private
def springboard_alert_visible?
!springboard_alert.empty?
end
# @!visibility private
def dismiss_springboard_alert(button_title)
parameters = { :button_title => button_title }
request = request("dismiss-springboard-alert", parameters)
client = http_client(http_options)
response = client.post(request)
hash = expect_300_response(response)
if hash["error"]
raise RuntimeError, %Q[
Could not dismiss SpringBoard alert by touching button with title '#{button_title}':
#{hash["error"]}
]
end
true
end
# @!visibility private
def set_dismiss_springboard_alerts_automatically(true_or_false)
if ![true, false].include?(true_or_false)
raise ArgumentError, "Expected #{true_or_false} to be a boolean true or false"
end
parameters = { :dismiss_automatically => true_or_false }
@springboard_alert_enabled = true_or_false
request = request("set-dismiss-springboard-alerts-automatically", parameters)
client = http_client(http_options)
response = client.post(request)
hash = expect_300_response(response)
hash["is_dismissing_alerts_automatically"]
end
# @!visibility private
# @see #query
def query_for_coordinate(uiquery)
element = wait_for_view(uiquery)
coordinate_from_query_result([element])
end
# @!visibility private
#
# :num_fingers
# :duration
# :repetitions
# @see #query
def touch(uiquery, options={})
coordinate = query_for_coordinate(uiquery)
perform_coordinate_gesture("touch", coordinate[:x], coordinate[:y], options)
end
# @!visibility private
# @see #touch
def touch_coordinate(coordinate, options={})
x = coordinate[:x] || coordinate["x"]
y = coordinate[:y] || coordinate["y"]
touch_point(x, y, options)
end
# @!visibility private
# @see #touch
def touch_point(x, y, options={})
perform_coordinate_gesture("touch", x, y, options)
end
# @!visibility private
# @see #touch
# @see #query
def double_tap(uiquery, options={})
coordinate = query_for_coordinate(uiquery)
perform_coordinate_gesture("double_tap",
coordinate[:x], coordinate[:y],
options)
end
# @!visibility private
# @see #touch
# @see #query
def two_finger_tap(uiquery, options={})
coordinate = query_for_coordinate(uiquery)
perform_coordinate_gesture("two_finger_tap",
coordinate[:x], coordinate[:y],
options)
end
# @!visibility private
# @see #touch
# @see #query
def long_press(uiquery, options={})
merged_options = {
:duration => 1.1
}.merge(options)
coordinate = query_for_coordinate(uiquery)
perform_coordinate_gesture("touch", coordinate[:x], coordinate[:y],
merged_options)
end
# @!visibility private
def rotate_home_button_to(position, sleep_for=1.0)
orientation = normalize_orientation_position(position)
parameters = {
:orientation => orientation,
:seconds_to_sleep_after => sleep_for
}
request = request("rotate_home_button_to", parameters)
client = http_client(http_options)
response = client.post(request)
expect_300_response(response)
end
# @!visibility private
def orientations
request = request("orientations")
client = http_client(http_options)
response = client.get(request)
expect_300_response(response)
end
# @!visibility private
def pan_between_coordinates(start_point, end_point, options={})
default_options = {
:num_fingers => 1,
:duration => 0.5,
# How long the first touch needs to activate or grab the element.
:first_touch_hold_duration => 0.0
}
merged_options = default_options.merge(options)
parameters = {
:gesture => "drag",
:specifiers => {
:coordinates => [start_point, end_point]
},
:options => merged_options
}
make_gesture_request(parameters)
end
# @!visibility private
def perform_coordinate_gesture(gesture, x, y, options={})
parameters = {
:gesture => gesture,
:specifiers => {
:coordinate => {x: x, y: y}
},
:options => options
}
make_gesture_request(parameters)
end
# @!visibility private
def make_gesture_request(parameters)
RunLoop.log_debug %Q[Sending request to perform '#{parameters[:gesture]}' with:
#{JSON.pretty_generate(parameters)}
]
request = request("gesture", parameters)
client = http_client(http_options)
response = client.post(request)
expect_300_response(response)
end
# @!visibility private
def coordinate_from_query_result(matches)
if matches.nil? || matches.empty?
raise "Expected #{hash} to contain some results"
end
rect = matches.first["rect"]
h = rect["height"]
w = rect["width"]
x = rect["x"]
y = rect["y"]
touchx = x + (w/2.0)
touchy = y + (h/2.0)
new_rect = rect.dup
new_rect[:center_x] = touchx
new_rect[:center_y] = touchy
RunLoop.log_debug(%Q[Rect from query:
#{JSON.pretty_generate(new_rect)}
])
{:x => touchx,
:y => touchy}
end
# @!visibility private
def change_volume(up_or_down)
string = up_or_down.to_s
parameters = {
:volume => string
}
request = request("volume", parameters)
client = http_client(http_options)
response = client.post(request)
json = expect_300_response(response)
# Set in the route
sleep(0.2)
json
end
def element_types
request = request("element-types")
client = http_client(http_options)
response = client.get(request)
expect_300_response(response)["types"]
end
# TODO: animation model
def wait_for_animations
sleep(0.5)
end
# @!visibility private
def wait_for(timeout_message, options={}, &block)
wait_options = WAIT_DEFAULTS.merge(options)
timeout = wait_options[:timeout]
exception_class = wait_options[:exception_class]
with_timeout(timeout, timeout_message, exception_class) do
loop do
value = block.call
return value if value
sleep(wait_options[:retry_frequency])
end
end
end
# @!visibility private
def wait_for_keyboard(timeout=WAIT_DEFAULTS[:timeout])
options = WAIT_DEFAULTS.dup
options[:timeout] = timeout
message = %Q[
Timed out after #{timeout} seconds waiting for the keyboard to appear.
]
wait_for(message, options) do
keyboard_visible?
end
end
# @!visibility private
def wait_for_no_keyboard(timeout=WAIT_DEFAULTS[:timeout])
options = WAIT_DEFAULTS.dup
options[:timeout] = timeout
message = %Q[
Timed out after #{timeout} seconds waiting for the keyboard to disappear.
]
wait_for(message, options) do
!keyboard_visible?
end
end
# @!visibility private
def wait_for_alert(timeout=WAIT_DEFAULTS[:timeout])
options = WAIT_DEFAULTS.dup
options[:timeout] = timeout
message = %Q[
Timed out after #{timeout} seconds waiting for an alert to appear.
]
wait_for(message, options) do
alert_visible?
end
end
# @!visibility private
def wait_for_springboard_alert(timeout=WAIT_DEFAULTS[:timeout])
options = WAIT_DEFAULTS.dup
options[:timeout] = timeout
message = %Q[
Timed out after #{timeout} seconds waiting for a SpringBoard alert to appear.
]
wait_for(message, options) do
springboard_alert_visible?
end
end
# @!visibility private
def wait_for_no_alert(timeout=WAIT_DEFAULTS[:timeout])
options = WAIT_DEFAULTS.dup
options[:timeout] = timeout
message = %Q[
Timed out after #{timeout} seconds waiting for an alert to disappear.
]
wait_for(message, options) do
!alert_visible?
end
end
# @!visibility private
def wait_for_no_springboard_alert(timeout=WAIT_DEFAULTS[:timeout])
options = WAIT_DEFAULTS.dup
options[:timeout] = timeout
message = %Q[
Timed out after #{timeout} seconds waiting for a SpringBoard alert to disappear.
]
wait_for(message, options) do
!springboard_alert_visible?
end
end
# @!visibility private
def wait_for_text_in_view(text, uiquery, options={})
merged_options = WAIT_DEFAULTS.merge(options)
begin
wait_for("TMP", merged_options) do
view = query(uiquery).first
if view
# Guard against this edge case:
#
# Text is "" and value or label keys do not exist in view which
# implies that value or label was the empty string (see the
# DeviceAgent JSONUtils and Facebook macros).
if text == "" || text == nil
view["value"] == nil && view["label"] == nil
else
[view["value"], view["label"]].any? { |elm| elm == text }
end
else
false
end
end
rescue merged_options[:exception_class] => e
view = query(uiquery)
if !view
message = %Q[
Timed out wait after #{merged_options[:timeout]} seconds waiting for a view to match:
#{uiquery}
]
else