Skip to content
Fetching contributors…
Cannot retrieve contributors at this time
391 lines (337 sloc) 16.1 KB
require 'json'
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/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 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)
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
Something went wrong with that request. Please try again.