-
Notifications
You must be signed in to change notification settings - Fork 230
Expand file tree
/
Copy pathfrank_helper.rb
More file actions
429 lines (372 loc) · 17.9 KB
/
Copy pathfrank_helper.rb
File metadata and controls
429 lines (372 loc) · 17.9 KB
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
require 'json'
require 'base64'
require 'frank-cucumber/gateway'
require 'frank-cucumber/host_scripting'
require 'frank-cucumber/wait_helper'
require 'frank-cucumber/keyboard_helper'
require 'frank-cucumber/scroll_helper'
require 'frank-cucumber/gesture_helper'
require 'frank-cucumber/location_helper'
require 'frank-cucumber/bonjour'
module Frank module Cucumber
# FrankHelper provides a core set of helper functions for use when interacting with Frank.
#
# == Most helpful methods
# * {#touch}
# * {#wait_for_element_to_exist}
# * {#wait_for_element_to_exist_and_then_touch_it}
# * {#wait_for_nothing_to_be_animating}
# * {#app_exec}
#
# == Configuring the Frank driver
# There are some class-level facilities which configure how all Frank interactions work. For example you can specify which selector engine to use
# with {FrankHelper.selector_engine}. You can specify the base url which the native app's Frank server is listening on with {FrankHelper.server_base_url}.
#
# Two common use cases are covered more conveniently with {FrankHelper.use_shelley_from_now_on} and {FrankHelper.test_on_physical_device_via_bonjour}.
module FrankHelper
include WaitHelper
include KeyboardHelper
include ScrollHelper
include GestureHelper
include HostScripting
include LocationHelper
# @!attribute [rw] selector_engine
class << self
# @return [String] the selector engine we tell Frank to use when interpreting view selectors.
attr_accessor :selector_engine
# @return [String] the base url which the Frank server is running on. All Frank commands will be sent to that server.
attr_accessor :server_base_url
# After calling this method all subsequent commands will ask Frank to use the Shelley selector engine to interpret view selectors.
def use_shelley_from_now_on
@selector_engine = 'shelley_compat'
end
# Use Bonjour to search for a running Frank server. The server found will be the recipient for all subsequent Frank commands.
# @raise a generic exception if no Frank server could be found via Bonjour
def test_on_physical_device_via_bonjour
@server_base_url = Bonjour.new.lookup_frank_base_uri
raise 'could not detect running Frank server' unless @server_base_url
end
end
# Get the correct quote for the selector
def get_selector_quote(selector)
if selector.index("'") == nil
return "'"
else
return '"'
end
end
#@api private
#@return [:String] convient shorthand for {Frank::Cucumber::FrankHelper.selector_engine}, defaulting to 'uiquery'
def selector_engine
Frank::Cucumber::FrankHelper.selector_engine || 'uiquery' # default to UIQuery for backwards compatibility
end
#@api private
#@return [:String] convient shorthand for {Frank::Cucumber::FrankHelper.server_base_url}
def base_server_url
Frank::Cucumber::FrankHelper.server_base_url
end
# Ask Frank to touch all views matching the specified selector. There may be views in the view heirarchy which match the selector but
# which Frank cannot or will not touch - for example views which are outside the current viewport. You can discover which of the matching
# views were actually touched by inspecting the Array which is returned.
#
# @param [String] selector a view selector.
# @return [Array<Boolean>] an array indicating for each view which matched the selector whether it was touched or not.
# @raise an expection if no views matched the selector
# @raise an expection if no views which matched the selector could be touched
def touch( selector )
touch_successes = frankly_map( selector, 'touch' )
raise "could not find anything matching [#{selector}] to touch" if touch_successes.empty?
raise "some views could not be touched (probably because they are not within the current viewport)" if touch_successes.include?(false)
touch_successes
end
# grab screenshots of all views matching the specified selector.
# @param [String] selector a view selector.
# @return [Array<PNG>] an array of PNG-image binary.
# @raise an expection if no views matched the selector
# @raise an expection if no views which matched the selector could be capture
def capture( selector )
view_uids = frankly_map( selector, 'FEX_UID' )
raise "could not find anything matching [#{selector}] to capture" if view_uids.empty?
raise "some views could not be capture image" if view_uids.include?(nil)
frank_server.send_get( 'screenshot/snapshot-all-views' )
screenshots = view_uids.map{|uid|
path = "screenshot/view-snapshot/#{uid}"
frank_server.send_get( path )
}
raise "some views failed to capture image" if screenshots.include?(nil)
screenshots
end
# Fill in text in a text field.
#
# @param [String] the placeholder text for the desired text field
# @param [Hash{Symbol => String}] a hash with a :with key and a string of text to fill in
# @raise an exception if the :with key DSL syntax is missing
# @raise an exception if a text field with the given placeholder text could not be found
def fill_in( placeholder_field_name, options={} )
raise "Must pass a hash containing the key :with" unless (options.is_a?(Hash) && options.has_key?(:with))
text_to_type = options[:with]
quote = get_selector_quote(placeholder_field_name)
text_fields_modified = frankly_map( "textField placeholder:#{quote}#{placeholder_field_name}#{quote}", "setText:", text_to_type )
raise "could not find text fields with placeholder #{quote}#{placeholder_field_name}#{quote}" if text_fields_modified.empty?
#TODO raise warning if text_fields_modified.count > 1
end
# Indicate whether there are any views in the current view heirarchy which match the specified selector.
# @param [String] selector a view selector.
# @return [Boolean]
# @see #check_element_exists
def element_exists( selector )
matches = frankly_map( selector, 'accessibilityLabel' )
# TODO: raise warning if matches.count > 1
!matches.empty?
end
# Assert whether there are any views in the current view heirarchy which match the specified selector.
# @param [String] selector a view selector.
# @raise an rspec exception if the assertion fails
# @see #element_exists, #check_element_does_not_exist
def check_element_exists( selector )
element_exists( selector ).should be_true
end
def check_element_exists_and_is_visible( selector )
element_is_not_hidden( selector ).should be_true
end
# Assert whether there are no views in the current view heirarchy which match the specified selector.
# @param [String] selector a view selector.
# @raise an rspec exception if the assertion fails
# @see #element_exists, #check_element_exists
def check_element_does_not_exist( selector )
element_exists( selector ).should be_false
end
def check_element_does_not_exist_or_is_not_visible( selector )
element_is_not_hidden( selector ).should be_false
end
# Indicate whether there are any views in the current view heirarchy which contain the specified accessibility label.
# @param [String] expected_mark the expected accessibility label
# @return [Boolean]
# @see #check_view_with_mark_exists
def view_with_mark_exists(expected_mark)
quote = get_selector_quote(expected_mark)
element_exists( "view marked:#{quote}#{expected_mark}#{quote}" )
end
# Assert whether there are any views in the current view heirarchy which contain the specified accessibility label.
# @param [String] expected_mark the expected accessibility label
# @raise an rspec exception if the assertion fails
# @see #view_with_mark_exists
def check_view_with_mark_exists(expected_mark)
quote = get_selector_quote(expected_mark)
check_element_exists( "view marked:#{quote}#{expected_mark}#{quote}" )
end
# Assert whether there are no views in the current view heirarchy which contain the specified accessibility label.
# @param [String] expected_mark the expected accessibility label
# @raise an rspec exception if the assertion fails
# @see #view_with_mark_exists, #check_view_with_mark_exists
def check_view_with_mark_does_not_exist(expected_mark)
quote = get_selector_quote(expected_mark)
check_element_does_not_exist( "view marked:#{quote}#{expected_mark}#{quote}" )
end
# Waits for any of the specified selectors to match a view.
#
# Checks each selector in turn within a {http://sauceio.com/index.php/2011/04/how-to-lose-races-and-win-at-selenium/ spin assert} loop and yields the first one which is found to exist in the view heirarchy.
# Raises an exception if no views could be found to match any of the provided selectors within {WaitHelper::TIMEOUT} seconds.
#
# @see WaitHelper#wait_until
def wait_for_element_to_exist(*selectors,&block)
wait_until(:message => "Waited for element matching any of #{selectors.join(', ')} to exist") do
at_least_one_exists = false
selectors.each do |selector|
if element_exists( selector )
at_least_one_exists = true
block.call(selector) if block
end
end
at_least_one_exists
end
end
# Waits for the specified selector to not match any views.
#
# Uses {WaitHelper#wait_until} to check for any matching views within a {http://sauceio.com/index.php/2011/04/how-to-lose-races-and-win-at-selenium/ spin assert} loop.
# Returns as soon as no views match the specified selector.
# Raises an exception if there continued to be at least one view which matched the selector by the time {WaitHelper::TIMEOUT} seconds passed.
#
# @see check_element_does_not_exist
# @see wait_for_element_to_not_exist
def wait_for_element_to_not_exist(selector)
wait_until(:message => "Waited for element #{selector} to not exist") do
!element_exists(selector)
end
end
# Waits for a view to exist and then send a touch command to that view.
#
# @param selectors takes one or more selectors to use to search for a view. The first selector which is found to matches a view is the selector
# which is then used to send a touch command.
#
# Raises an exception if no views could be found to match any of the provided selectors within {WaitHelper::TIMEOUT} seconds.
def wait_for_element_to_exist_and_then_touch_it(*selectors)
wait_for_element_to_exist(*selectors) do |sel|
touch(sel)
end
end
# Waits for there to be no views which report an isAnimated property of true.
#
# @param timeout [Number] number of seconds to wait for nothing to be animating before timeout out. Defaults to {WaitHelper::TIMEOUT}
#
# Raises an exception if there were still views animating after {timeout} seconds.
def wait_for_nothing_to_be_animating( timeout = false )
wait_until :timeout => timeout do
!element_exists('view isAnimating')
end
end
# Checks that the specified selector matches at least one view, and that at least one of the matched
# views has an isHidden property set to false
#
# a better name for this method would be element_exists_and_is_not_hidden
def element_is_not_hidden(selector)
matches = frankly_map( selector, 'FEX_isVisible' )
matches.delete(false)
!matches.empty?
end
def accessibility_frame(selector)
frames = frankly_map( selector, 'accessibilityFrame' )
raise "the supplied selector [#{selector}] did not match any views" if frames.empty?
raise "the supplied selector [#{selector}] matched more than one views (#{frames.count} views matched)" if frames.count > 1
Rect.from_api_repr( frames.first )
end
def drag_with_initial_delay(args)
from, to = args.values_at(:from,:to)
raise ArgumentError.new('must specify a :from parameter') if from.nil?
raise ArgumentError.new('must specify a :to parameter') if to.nil?
dest_frame = accessibility_frame(to)
frankly_map( from, 'FEX_dragWithInitialDelayToX:y:', dest_frame.center.x, dest_frame.center.y )
end
# Ask Frank to invoke the specified method on the app delegate of the iOS application under automation.
# @param method_sig [String] the method signature
# @param method_args the method arguments
#
# @example
# # the same as calling
# # [[[UIApplication sharedApplication] appDelegate] setServiceBaseUrl:@"http://example.com/my_api" withPort:8080]
# # from your native app
# app_exec( "setServiceBaseUrl:withPort:", "http://example.com/my_api", 8080 )
#
#
def app_exec(method_sig, *method_args)
operation_map = Gateway.build_operation_map(method_sig.to_s, method_args)
res = frank_server.send_post(
'app_exec',
:operation => operation_map
)
return Gateway.evaluate_frankly_response( res, "app_exec #{method_sig}" )
end
# Ask Frank to execute an arbitrary Objective-C method on each view which matches the specified selector.
#
# @return [Array] an array with an element for each view matched by the selector, each element in the array gives the return value from invoking the specified method on that view.
def frankly_map( selector, method_name, *method_args )
operation_map = Gateway.build_operation_map(method_name.to_s, method_args)
res = frank_server.send_post(
'map',
:query => selector,
:operation => operation_map,
:selector_engine => selector_engine
)
return Gateway.evaluate_frankly_response( res, "frankly_map #{selector} #{method_name}" )
end
# print a JSON-formatted dump of the current view heirarchy to stdout
def frankly_dump
res = frank_server.send_get( 'dump' )
puts JSON.pretty_generate(JSON.parse(res)) rescue puts res #dumping a super-deep DOM causes errors
end
# grab a screenshot of the application under automation and save it to the specified file.
#
# @param filename [String] where to save the screenshot image file
# @param subframe describes which section of the screen to grab. If unspecified then the entire screen will be captured. #TODO document what format this parameter takes.
# @param allwindows [Boolean] If true then all UIWindows in the current UIScreen will be included in the screenshot. If false then only the main window will be captured.
def frankly_screenshot(filename, subframe=nil, allwindows=true)
path = 'screenshot'
path += '/allwindows' if allwindows
path += "/frame/" + URI.escape(subframe) if (subframe != nil)
data = frank_server.send_get( path )
open(filename, "wb") do |file|
file.write(data)
end
end
# @return [Boolean] true if the device running the application currently in a portrait orientation
# @note wil return false if the device is in a flat or unknown orientation. Sometimes the iOS simulator will report this state when first launched.
def frankly_oriented_portrait?
'portrait' == frankly_current_orientation
end
# @return [Boolean] true if the device running the application currently in a landscape orientation
# @note wil return false if the device is in a flat or unknown orientation. Sometimes the iOS simulator will report this state when first launched.
def frankly_oriented_landscape?
'landscape' == frankly_current_orientation
end
# @return [String] the orientation of the device running the application under automation.
# @note this is a low-level API. In most cases you should use {frankly_oriented_portrait} or {frankly_oriented_landscape} instead.
def frankly_current_orientation
res = frank_server.send_get( 'orientation' )
orientation = JSON.parse( res )['orientation']
puts "orientation reported as '#{orientation}'" if $DEBUG
orientation
end
# set the device orientation
# @param orientation can be 'landscape','landscape_left','landscape_right','portrait', or 'portrait_upside_down'
def frankly_set_orientation(orientation)
orientation = orientation.to_s
orientation = 'landscape_left' if orientation == 'landscape'
res = frank_server.send_post( 'orientation', orientation )
return Gateway.evaluate_frankly_response( res, "set_orientation #{orientation}" )
end
# @return [Boolean] Does the device running the application have accessibility enabled.
# If accessibility is not enabled then a lot of Frank functionality will not work.
def frankly_is_accessibility_enabled
res = frank_server.send_get( 'accessibility_check' )
JSON.parse( res )['accessibility_enabled'] == 'true'
end
# wait for the application under automation to be ready to receive automation commands.
#
# Has some basic heuristics to cope with cases where the Frank server is intermittently available when first launching.
#
# @raise [Timeout::TimeoutError] if nothing is ready within 20 seconds
# @raise generic error if the device hosting the application does not appear to have accessibility enabled.
def wait_for_frank_to_come_up
num_consec_successes = 0
num_consec_failures = 0
Timeout.timeout(20) do
while num_consec_successes <= 6
if frankly_ping
num_consec_failures = 0
num_consec_successes += 1
else
num_consec_successes = 0
num_consec_failures += 1
if num_consec_failures >= 5 # don't show small timing errors
print (num_consec_failures == 5 ) ? "\n" : "\r"
print "PING FAILED" + "!"*num_consec_failures
end
end
STDOUT.flush
sleep 0.2
end
if num_consec_successes < 6
print (num_consec_successes == 1 ) ? "\n" : "\r"
print "FRANK!".slice(0,num_consec_successes)
STDOUT.flush
puts ''
end
if num_consec_failures >= 5
puts ''
end
end
unless frankly_is_accessibility_enabled
raise "ACCESSIBILITY DOES NOT APPEAR TO BE ENABLED ON YOUR SIMULATOR. Hit the home button, go to settings, select Accessibility, and turn the inspector on."
end
end
# Check whether Frank is able to communicate with the application under automation
def frankly_ping
frank_server.ping
end
#@api private
#@return [Frank::Cucumber::Gateway] a gateway for sending Frank commands to the application under automation
def frank_server
@_frank_server ||= Frank::Cucumber::Gateway.new( base_server_url )
end
end
end end