Module: Frank::Cucumber::FrankHelper

Includes:
GestureHelper, HostScripting, KeyboardHelper, LocationHelper, ScrollHelper, WaitHelper
Included in:
Frank::Console, Launcher
Defined in:
lib/frank-cucumber/frank_helper.rb

Overview

FrankHelper provides a core set of helper functions for use when interacting with Frank.

Most helpful methods

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.

Constant Summary

Constants included from WaitHelper

WaitHelper::POLL_SLEEP, WaitHelper::TIMEOUT

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from LocationHelper

#set_location

Methods included from HostScripting

#press_home_on_simulator, #quit_double_simulator, #quit_simulator, #rotate_simulator_left, #rotate_simulator_right, #shake_simulator, #simulate_hardware_keyboard, #simulate_memory_warning, #simulator_hardware_menu_press, #simulator_reset_data, #start_recording, #stop_recording, #toggle_call_status_bar

Methods included from GestureHelper

#double_tap, #double_tap_point, #drag_thumb_in_slider, #drag_thumb_in_slider_with_default_duration, #tap_and_hold, #tap_and_hold_point

Methods included from ScrollHelper

#scroll_table_view, #scroll_view_to_bottom, #scroll_view_to_position, #scroll_view_to_top

Methods included from KeyboardHelper

#type_into_keyboard, #type_shortcut

Methods included from WaitHelper

wait_until

Class Attribute Details

.selector_engineString



39
40
41
# File 'lib/frank-cucumber/frank_helper.rb', line 39

def selector_engine
  @selector_engine
end

.server_base_urlString



41
42
43
# File 'lib/frank-cucumber/frank_helper.rb', line 41

def server_base_url
  @server_base_url
end

Class Method Details

.test_on_physical_device_via_bonjourObject

Use Bonjour to search for a running Frank server. The server found will be the recipient for all subsequent Frank commands.

Raises:

  • a generic exception if no Frank server could be found via Bonjour



50
51
52
53
# File 'lib/frank-cucumber/frank_helper.rb', line 50

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

.use_shelley_from_now_onObject

After calling this method all subsequent commands will ask Frank to use the Shelley selector engine to interpret view selectors.



44
45
46
# File 'lib/frank-cucumber/frank_helper.rb', line 44

def use_shelley_from_now_on
  @selector_engine = 'shelley_compat'
end

Instance Method Details

#accessibility_frame(selector) ⇒ Object



283
284
285
286
287
288
# File 'lib/frank-cucumber/frank_helper.rb', line 283

def accessibility_frame(selector)
  frames = frankly_map( selector, 'FEX_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

#app_exec(method_sig, *method_args) ⇒ Object

Ask Frank to invoke the specified method on the app delegate of the iOS application under automation.

Examples:

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


330
331
332
333
334
335
336
337
338
339
# File 'lib/frank-cucumber/frank_helper.rb', line 330

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

#base_server_url:String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns convient shorthand for server_base_url



80
81
82
# File 'lib/frank-cucumber/frank_helper.rb', line 80

def base_server_url
  Frank::Cucumber::FrankHelper.server_base_url
end

#check_element_does_not_exist(selector) ⇒ Object

Assert whether there are no views in the current view heirarchy which match the specified selector.

Raises:

  • an rspec exception if the assertion fails

See Also:

  • #check_element_exists


141
142
143
# File 'lib/frank-cucumber/frank_helper.rb', line 141

def check_element_does_not_exist( selector )
  element_exists( selector ).should be_false, "Found element matching selector when it should not exist (#{selector})"
end

#check_element_does_not_exist_or_is_not_visible(selector) ⇒ Object



145
146
147
# File 'lib/frank-cucumber/frank_helper.rb', line 145

def check_element_does_not_exist_or_is_not_visible( selector )
  element_is_not_hidden( selector ).should be_false, "Found visible element matching selector when it should not be visible (#{selector})"
end

#check_element_exists(selector) ⇒ Object

Assert whether there are any views in the current view heirarchy which match the specified selector.

Raises:

  • an rspec exception if the assertion fails

See Also:

  • #check_element_does_not_exist


129
130
131
# File 'lib/frank-cucumber/frank_helper.rb', line 129

def check_element_exists( selector )
  element_exists( selector ).should be_true, "Could not find element matching selector (#{selector})"
end

#check_element_exists_and_is_visible(selector) ⇒ Object



133
134
135
# File 'lib/frank-cucumber/frank_helper.rb', line 133

def check_element_exists_and_is_visible( selector )
  element_is_not_hidden( selector ).should be_true, "Could not find visible element matching selector (#{selector})"
end

#check_navigation_title_with_text_exists(expected_title) ⇒ Object

Assert the title of the navigation bar.

Raises:

  • an rspec exception if the assertion fails

  • an rspec exception if the navigation bar and its subview `UINavigationItemView` cannot be found

  • an rspec exception if the `UINavigationItemView` does not cover the center x of the navigation bar



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/frank-cucumber/frank_helper.rb', line 200

def check_navigation_title_with_text_exists(expected_title)
  quoted_text = "#{quote}#{expected_title}#{quote}"

  navFrame = frankly_map('view:"UINavigationBar"', 'frame').first
  navCenter = navFrame["size"]["width"] / 2.0

  frame = frankly_map("view:'UINavigationBar' view:'UINavigationItemView' marked:#{quoted_text}", 'frame').first
  raise "Could not find navigation bar with title (#{expected_title})" unless frame && frame["origin"] && frame["size"]

  left = frame["origin"]["x"]
  right = frame["origin"]["x"] + frame["size"]["width"]
  raise "Expected title (#{expected_title}) not in center of navigation bar" unless left && right && left < right

  ((left < navCenter) && (navCenter < right)).should be_true, "Could not find navigation title in center of navigation bar matching text (#{expected_title})"
end

#check_view_with_mark_does_not_exist(expected_mark) ⇒ Object

Assert whether there are no views in the current view heirarchy which contain the specified accessibility label.

Raises:

  • an rspec exception if the assertion fails

See Also:

  • #check_view_with_mark_exists


190
191
192
193
# File 'lib/frank-cucumber/frank_helper.rb', line 190

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

#check_view_with_mark_exists(expected_mark) ⇒ Object

Assert whether there are any views in the current view heirarchy which contain the specified accessibility label.

Raises:

  • an rspec exception if the assertion fails

See Also:



181
182
183
184
# File 'lib/frank-cucumber/frank_helper.rb', line 181

def check_view_with_mark_exists(expected_mark)
  quote = get_selector_quote(expected_mark)
  check_element_exists( "view marked:#{quote}#{expected_mark}#{quote}" )
end

#drag_with_initial_delay(args) ⇒ Object

Raises:

  • (ArgumentError)


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
# File 'lib/frank-cucumber/frank_helper.rb', line 290

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)

  if is_mac
    from_frame = accessibility_frame(from)

    frankly_map( from, 'FEX_mouseDownX:y:', from_frame.center.x, from_frame.center.y )

    sleep 0.3

    frankly_map( from, 'FEX_dragToX:y:', dest_frame.center.x, dest_frame.center.y )

    sleep 0.3

    frankly_map( from, 'FEX_mouseUpX:y:', dest_frame.center.x, dest_frame.center.y )

  else

    frankly_map( from, 'FEX_dragWithInitialDelayToX:y:', dest_frame.center.x, dest_frame.center.y )

  end

end

#element_exists(selector) ⇒ Boolean

Indicate whether there are any views in the current view heirarchy which match the specified selector.



119
120
121
122
123
# File 'lib/frank-cucumber/frank_helper.rb', line 119

def element_exists( selector )
  matches = frankly_map( selector, 'FEX_accessibilityLabel' )
  # TODO: raise warning if matches.count > 1
  !matches.empty?
end

#element_is_not_hidden(selector) ⇒ Object

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



277
278
279
280
281
# File 'lib/frank-cucumber/frank_helper.rb', line 277

def element_is_not_hidden(selector)
   matches = frankly_map( selector, 'FEX_isVisible' )
   matches.delete(false)
   !matches.empty?
end

#fill_in(placeholder_field_name, options = {}) ⇒ Object

Fill in text in a text field.

Raises:

  • an exception if the :with key DSL syntax is missing

  • an exception if a text field with the given placeholder text could not be found



105
106
107
108
109
110
111
112
113
# File 'lib/frank-cucumber/frank_helper.rb', line 105

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

#frank_serverFrank::Cucumber::Gateway

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a gateway for sending Frank commands to the application under automation



499
500
501
# File 'lib/frank-cucumber/frank_helper.rb', line 499

def frank_server
  @_frank_server ||= Frank::Cucumber::Gateway.new( base_server_url )
end

#frankly_current_orientationString

Note:

this is a low-level API. In most cases you should use frankly_oriented_portrait or frankly_oriented_landscape instead.

Returns the orientation of the device running the application under automation.



393
394
395
396
397
398
# File 'lib/frank-cucumber/frank_helper.rb', line 393

def frankly_current_orientation
  res = frank_server.send_get( 'orientation' )
  orientation = JSON.parse( res )['orientation']
  puts "orientation reported as '#{orientation}'" if $DEBUG
  orientation
end

#frankly_device_nameString

Note:

this is a low-level API. In most cases you should use #is_iphone, #is_ipad or #is_mac instead.

Returns the name of the device currently running the application



462
463
464
465
466
467
# File 'lib/frank-cucumber/frank_helper.rb', line 462

def frankly_device_name
  res = frank_server.send_get( 'device' )
  device = JSON.parse( res )['device']
  puts "device reported as '#{device}'" if $DEBUG
  device
end

#frankly_dumpObject

print a JSON-formatted dump of the current view heirarchy to stdout



357
358
359
360
# File 'lib/frank-cucumber/frank_helper.rb', line 357

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

#frankly_is_accessibility_enabledBoolean

If accessibility is not enabled then a lot of Frank functionality will not work.



412
413
414
415
# File 'lib/frank-cucumber/frank_helper.rb', line 412

def frankly_is_accessibility_enabled
  res = frank_server.send_get( 'accessibility_check' )
  JSON.parse( res )['accessibility_enabled'] == 'true'
end

#frankly_map(selector, method_name, *method_args) ⇒ Array

Ask Frank to execute an arbitrary Objective-C method on each view which matches the specified selector.



344
345
346
347
348
349
350
351
352
353
354
# File 'lib/frank-cucumber/frank_helper.rb', line 344

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

#frankly_oriented_landscape?Boolean

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.

Returns true if the device running the application currently in a landscape orientation



387
388
389
# File 'lib/frank-cucumber/frank_helper.rb', line 387

def frankly_oriented_landscape?
  'landscape' == frankly_current_orientation
end

#frankly_oriented_portrait?Boolean

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.

Returns true if the device running the application currently in a portrait orientation



381
382
383
# File 'lib/frank-cucumber/frank_helper.rb', line 381

def frankly_oriented_portrait?
  'portrait' == frankly_current_orientation
end

#frankly_os_versionString



485
486
487
488
489
490
# File 'lib/frank-cucumber/frank_helper.rb', line 485

def frankly_os_version
  res = frank_server.send_get( 'device' )
  os_version = JSON.parse( res )['os_version']
  puts "os_version reported as '#{os_version}'" if $DEBUG
  os_version
end

#frankly_pingObject

Check whether Frank is able to communicate with the application under automation



493
494
495
# File 'lib/frank-cucumber/frank_helper.rb', line 493

def frankly_ping
  frank_server.ping
end

#frankly_screenshot(filename, subframe = nil, allwindows = true) ⇒ Object

grab a screenshot of the application under automation and save it to the specified file.



367
368
369
370
371
372
373
374
375
376
377
# File 'lib/frank-cucumber/frank_helper.rb', line 367

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

#frankly_set_orientation(orientation) ⇒ Object

set the device orientation



403
404
405
406
407
408
# File 'lib/frank-cucumber/frank_helper.rb', line 403

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

#get_selector_quote(selector) ⇒ Object

Get the correct quote for the selector



57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/frank-cucumber/frank_helper.rb', line 57

def get_selector_quote(selector)
  if selector.index("'") == nil
    return "'"
  else
    return '"'
  end

# Specify ip address to run on
def test_on_physical_device_with_ip(ip_address)
    @server_base_url = ip_address
    raise 'IP Address is incorrect' unless @server_base_url.match(%r{\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b})
    puts "Running on Frank server #{@server_base_url}"
  end
end

#is_ipadBoolean



475
476
477
# File 'lib/frank-cucumber/frank_helper.rb', line 475

def is_ipad
  return frankly_device_name == "ipad"
end

#is_iphoneBoolean



470
471
472
# File 'lib/frank-cucumber/frank_helper.rb', line 470

def is_iphone
  return frankly_device_name == "iphone"
end

#is_macBoolean



480
481
482
# File 'lib/frank-cucumber/frank_helper.rb', line 480

def is_mac
  return frankly_device_name == "mac"
end

Indicate whether the title of the navigation bar matches the expected title.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/frank-cucumber/frank_helper.rb', line 161

def navigation_title_with_text_exists(expected_title)
  quoted_text = "#{quote}#{expected_title}#{quote}"

  navFrame = frankly_map('view:"UINavigationBar"', 'frame').first
  navCenter = navFrame["size"]["width"] / 2.0

  frame = frankly_map("view:'UINavigationBar' view:'UINavigationItemView' marked:#{quoted_text}", 'frame').first
  return false unless frame && frame["origin"] && frame["size"]

  left = frame["origin"]["x"]
  right = frame["origin"]["x"] + frame["size"]["width"]
  return false unless left && right && left < right

  (left < navCenter) && (navCenter < right)
end

#selector_engine:String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns convient shorthand for selector_engine, defaulting to 'uiquery'



74
75
76
# File 'lib/frank-cucumber/frank_helper.rb', line 74

def selector_engine
  Frank::Cucumber::FrankHelper.selector_engine || 'uiquery' # default to UIQuery for backwards compatibility
end

#test_on_physical_device_with_ip(ip_address) ⇒ Object

Specify ip address to run on



65
66
67
68
69
# File 'lib/frank-cucumber/frank_helper.rb', line 65

def test_on_physical_device_with_ip(ip_address)
  @server_base_url = ip_address
  raise 'IP Address is incorrect' unless @server_base_url.match(%r{\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b})
  puts "Running on Frank server #{@server_base_url}"
end

#touch(selector) ⇒ Array<Boolean>

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.

Raises:

  • an expection if no views matched the selector

  • an expection if no views which matched the selector could be touched



92
93
94
95
96
97
# File 'lib/frank-cucumber/frank_helper.rb', line 92

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

#view_with_mark_exists(expected_mark) ⇒ Boolean

Indicate whether there are any views in the current view heirarchy which contain the specified accessibility label.



153
154
155
156
# File 'lib/frank-cucumber/frank_helper.rb', line 153

def view_with_mark_exists(expected_mark)
  quote = get_selector_quote(expected_mark)
  element_exists( "view marked:#{quote}#{expected_mark}#{quote}" )
end

#wait_for_element_to_exist(*selectors, &block) ⇒ Object

Waits for any of the specified selectors to match a view.

Checks each selector in turn within a 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 Also:

  • WaitHelper#wait_until


222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/frank-cucumber/frank_helper.rb', line 222

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

#wait_for_element_to_exist_and_then_touch_it(*selectors) ⇒ Object

Waits for a view to exist and then send a touch command to that view.

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.



255
256
257
258
259
# File 'lib/frank-cucumber/frank_helper.rb', line 255

def wait_for_element_to_exist_and_then_touch_it(*selectors)
  wait_for_element_to_exist(*selectors) do |sel|
    touch(sel)
  end
end

#wait_for_element_to_not_exist(selector) ⇒ Object

Waits for the specified selector to not match any views.

Uses WaitHelper#wait_until to check for any matching views within a 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.



243
244
245
246
247
# File 'lib/frank-cucumber/frank_helper.rb', line 243

def wait_for_element_to_not_exist(selector)
  wait_until(:message => "Waited for element #{selector} to not exist") do
    !element_exists(selector)
  end
end

#wait_for_frank_to_come_upObject

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.

Raises:

  • (Timeout::TimeoutError)

    if nothing is ready within 20 seconds

  • generic error if the device hosting the application does not appear to have accessibility enabled.



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
# File 'lib/frank-cucumber/frank_helper.rb', line 423

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

#wait_for_nothing_to_be_animating(timeout = false) ⇒ Object

Waits for there to be no views which report an isAnimated property of true.

Raises an exception if there were still views animating after timeout seconds.



266
267
268
269
270
# File 'lib/frank-cucumber/frank_helper.rb', line 266

def wait_for_nothing_to_be_animating( timeout = false )
  wait_until :timeout => timeout do
    !element_exists('view isAnimating')
  end
end