-
Notifications
You must be signed in to change notification settings - Fork 369
Calabash iOS Ruby API
When writing custom steps, you'll need to use the Ruby API to interact with your application. This document describes the API at a high level.
Please refer to the PUBLIC API DOCUMENTATION for in-depth information about the public API.
Query returns an array of its results. The query function gives powerful query capability from your test code. You can find views and other application objects, and make assertions about them or extract data from them.
The syntax for queries is really important, and described in a separate document: Query Syntax.
Calabash iOS tries to return results that carry useable information by default. For UIView objects this includes frame, class and description:
irb(main):003:0> query("button index:0")
=> [{"class"=>"UIRoundedRectButton", "frame"=>{"y"=>287, "width"=>72, "x"=>100, "height"=>37}, "UIType"=>"UIControl", "description"=>"<UIRoundedRectButton: 0x7d463d0; frame = (100 287; 72 37); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x7d46ae0>>"}]
A view is represented as a ruby Hash (hash map) so you can look into the result
irb(main):005:0> query("button index:0").first.keys
=> ["class", "frame", "UIType", "description"]
irb(main):006:0> query("button index:0").first["frame"]["width"]
=> 72
The *args
parameter lets you perform selectors on the query result before it is returned to your ruby script code (remember that the query is evaluated as Objective-C code inside the app and the result is sent back to the Ruby code). The form *args
is Ruby-speak for a variable number of args. For example, if you have a UITableView you can do
irb(main):031:0> query("tableView","numberOfSections")
=> [1]
This performs the selector numberOfSections
on each of the table views in the view (it always returns an array). You can perform a sequence of selectors:
irb(main):033:0> query("tableView","delegate","description")
=> ["<LPThirdViewController: 0x7d1f320>"]
For selectors with arguments you can use hashes. In Ruby 1.9 this has quite nice syntax:
irb(main):034:0> query("tableView",numberOfRowsInSection:0)
=> [30]
On Ruby 1.8 you can't use key:val as literal Hash syntax so you must do:
irb(main):035:0> query("tableView","numberOfRowsInSection"=>0)
=> [30]
For more complex selectors you use Arrays of Hashes. Here is a complex Ruby 1.9 example:
irb(main):036:0> query("pickerView",:delegate, [{pickerView:nil},{titleForRow:1},{forComponent:0}])
=> ["1,0"]
The array [{pickerView:nil},{titleForRow:1},{forComponent:0}]
maps to an Objective C invocation:
[pickerView.delegate pickerView:nil titleForRow:1 forComponent:0]
This is a helper function that will show the classes of the views that match the specified query uiquery
.
For example:
irb(main):001:0> classes("view")
=> ["UILayoutContainerView", "UITransitionView", "UIViewControllerWrapperView", "UIView", "UITextField", "UITextFieldRoundedRectBackgroundView", "UIImageView", "UIImageView", "UIImageView", "UITextFieldLabel", "UILabel", "UIRoundedRectButton", "UIButtonLabel", "UISwitch", "_UISwitchInternalView", "UIImageView", "UIView", "UIImageView", "UIImageView", "UIImageView", "UIRoundedRectButton", "UIButtonLabel", "UITabBar", "UITabBarButton", "UITabBarSelectionIndicatorView", "UITabBarSwappableImageView", "UITabBarButtonLabel", "UITabBarButton", "UITabBarSwappableImageView", "UITabBarButtonLabel", "UITabBarButton", "UITabBarSwappableImageView", "UITabBarButtonLabel", "UITabBarButton", "UITabBarSwappableImageView", "UITabBarButtonLabel"]
This is a helper function that will show the accessibility labels of the views that match the specified query uiquery
.
For example:
irb(main):001:0> label("view")
[nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "Empty list", nil, nil, nil, "Mon", "Dec 17", nil, nil, nil, "Tue", "Dec 18", nil, nil, nil, nil, "Today", nil, nil, nil, "Thu", "Dec 20", nil, nil, nil, nil, nil, "Empty list", nil, nil, "12", "12", nil, nil, "1", "1", nil, nil, "2", "2", nil, nil, "3", "3", nil, nil, nil, nil, nil, "Empty list", nil, nil, "56", "56", nil, nil, "57", "57", nil, nil, "58", "58", nil, nil, nil, nil, nil, "Empty list", nil, nil, "AM", "AM", nil, nil, "PM", "PM", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "Name", "First View", "First", "First", nil, "Second", "Second", nil, "other", "switch", nil, nil, nil, nil, nil, nil, "login", "Login", nil, "First", nil, nil, "First", "Second", nil, "Second", "Third", nil, "Third", "Fourth", nil, "Fourth"]
The element_exists
function returns true if an element exists matching query uiquery
.
The element_does_not_exist
function returns true if an element matching query uiquery
does not exist.
Waits for a condition to occur. Takes a hash of options and a block to be called repeatedly. The options (which are described below) have the following defaults:
{:timeout => 10, #maximum number of seconds to wait
:retry_frequency => 0.2, #wait this long before retrying the block
:post_timeout => 0.1, #wait this long after the block returns true
:timeout_message => "Timed out waiting...", #error message in case options[:timeout] is exceeded
:screenshot_on_error => true # take a screenshot in case of error
}
The timeout
argument should be a number indicating the maximal number of seconds you are willing to wait (after that amount of time the step will cause your test to fail). The :post_timeout
(0.1 by default) is an number of seconds to wait after the condition becomes true. The reason this is here is to avoid the common pattern of putting sleep(STEP_PAUSE) after each wait_for
-call to ensure that animations have finished.
The &block
parameter is Ruby syntax for saying that this method takes a block of code. This block specifies the condition to wait for. The block should return true
when the the condition occurs.
The :retry_frequency
is a small sleep that is made between each call to the specified block. This describes how often Calabash should poll for the condition to be true.
Here is a simple example:
irb(main):030:0> wait_for(:timeout => 5) { not query("label text:'Cell 11'").empty? }
This will check for the existence of a view matching: "label text:'Cell 11'". It will wait at most 5 seconds (failing if more than 5 seconds pass). It will issue the query repeatedly until it is found or 5 seconds pass.
A typical form uses element_exists
.
irb(main):031:0> wait_for(:timeout => 5) { element_exists("label text:'Cell 20'") }
In Ruby short blocks are written with brackets (like: { element_exists("label text:'Cell 20'") }
), and more complicated blocks are written using do
-end
. For example:
wait_for(:timeout => 30) do
res = query("button marked:'Player play icon'", :isSelected)
res.first == "1"
end
A Ruby block always returns the value of its last expression (res.first == "1"
in this case).
Notes: Waiting for a condition to occur is superior to using the sleep
function. With sleep
you end up either specifying too long waits which slows the test down or you become sensitive to timing issues. Sometimes you do need sleep (to wait for animations to complete), but try to use waiting as much as possible.
A high-level waiting function. This captures the common practice of waiting for UI elements, i.e., combining wait_for
and element_exists
.
Takes an array of queries and waits for all of those queries to return results. Calls wait_for
supplying options
.
irb(main):008:0> wait_for_elements_exist( ["label text:'Cell 11'", "tabBarButton marked:'Third'"], :timeout => 2)
Similar to wait_for_elements_exist
, but waits for all of the elements to not exist.
Waits until for animations to complete.
A high-level function which performs an action repeatedly (usually for side-effects) until a condition occurs. Takes a hash of options and a block to be called repeatedly. The options (which are described below) have the following defaults:
{:until => nil #a predicate which should return true when the condition is satisfied
:until_exists => nil #a uiquery to function as predicate (i.e. element_exists(opts[:until_exists]))
:timeout => 10, #maximum number of seconds to wait
:retry_frequency => 0.2, #wait this long before retrying the block
:post_timeout => 0.1, #wait this long after the block returns true
:timeout_message => "Timed out waiting...", #error message in case options[:timeout] is exceeded
:screenshot_on_error => true # take a screenshot in case of error
}
Either :until
or :until_exists
must be specified.
irb(main):023:0> wait_poll(:until_exists => "label text:'Cell 22'", :timeout => 20) do
irb(main):024:1* scroll("tableView", :down)
irb(main):025:1> end
Waits for the network indicator in the status bar to disappear. Options (opts) are passed to wait_for.
Will fail the test with message msg
. Takes a screenshot.
Asserts that an element exists using the query function on the parameter query
.
The function check_view_with_mark_exists(expected_mark)
is shorthand for
check_element_exists("view marked:'#{expected_mark}'")
Touches a view found by performing the query uiquery
. It is recommended that uiquery
only produce one match, but the default is to just touch the first of the results if there are several.
The touch
method is one of the most used in Calabash. It is mostly used in its simplest form:
irb(main):037:0> touch("view marked:'switch'")
Which uses accessibilityLabels or accessibilityIdentifiers.
The options
is an optional Ruby Hash of changes to the touch. Those options are passed to the playback
function so the same options apply. (See details about the playback
function below). An example would be an offset:
irb(main):040:0> touch("view marked:'First'", :offset => {:x => 50, :y => 0})
This will locate the centre of the view with accessibilityLabel 'First' and then perform the touch event 50 pixels right of that.
Touch also supports touching by screen coordinate. This is done by specifying a nil query and an offset:
irb(main):041:0> touch(nil, :offset => {:x => 50, :y => 0})
Enters a single character using the iOS Keyboard. Requires that the iOS keyboard is visible and that the character to enter is visible in the keyplane.
irb(main):043:0> keyboard_enter_char "a"
Parameter chr
must be a string of size 1 or one of the following special values:
'Dictation'
'Shift'
'Delete'
'Return'
'International'
'More'
',!'
'.?'
For example, "More" can take you to the numeric keyplane
irb(main):076:0> keyboard_enter_char "More"
Enters a sequence of characters using the iOS Keyboard. Requires that the iOS keyboard is visible.
irb(main):044:0> keyboard_enter_text "The Quick Brown Fox"
Enters "Done" or "Search" (the "Return" char, often the lower-right most button in keyboard).
Scrolls a scroll-view found by performing the query uiquery
. It is recommended that uiquery
only produce one match, but the default is to just touch the first of the results if there are several.
irb(main):082:0> scroll "scrollView", :down
The direction
argument must be one of: :up :down :left :right
. For paging scroll views, it will scroll a page.
In table views will scroll to a certain row. The query uiquery
identifies which table view (in case of several). Example:
irb(main):081:0> scroll_to_row "tableView", 2
Takes a hash of options. The options (which are described below) have the following defaults:
{:query => "tableView",
:row => 0,
:section => 0,
:scroll_position => :top,
:animate => true}
Scrolls to a particular table cell in the table found by options[:query]
.
irb(main):003:0> scroll_to_cell(:row => 13, :section => 0)
=> ["<UITableView: 0xb0d0000; frame = (0 0; 320 411); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0xa58ab90>; contentOffset: {0, 0}>. Delegate: LPThirdViewController, DataSource: LPThirdViewController"]
Options (these are all optional arguments, the defaults are specified above)
-
:row
the row to scroll to -
:section
the section to scroll to -
:scroll_position
the position to scroll to :top, :bottom, :middle -
:animate
animate the scrolling or not
Performs an action (specified by &block
) for each cell of a table.
Will scroll to each cell before performing action.
Takes the following options with default values:
{:query => "tableView", #the table view to act on
:post_scroll => 0.3, #a pause after each action taken
:skip_if => nil, #an optional proc to skip some cells
:animate => true #animate the scrolling?
}
A simple example
irb(main):008:0> each_cell(:post_scroll=>0) do |row, sec|
irb(main):009:1* puts "Row #{row} in Section #{sec}"
irb(main):010:1> end
Row 0 in Section 0
Row 1 in Section 0
Row 2 in Section 0
Row 3 in Section 0
...
A more interesting example
irb(main):001:0> table_labels = []
=> []
irb(main):002:0> each_cell(:animate => false, :post_scroll => 0.1) do |row, sec|
irb(main):003:1* txt = query("tableViewCell indexPath:#{row},#{sec} label", :text).first
irb(main):004:1> table_labels << txt
irb(main):005:1> end
=> 1
irb(main):006:0> table_labels
=> ["Cell 0", "Cell 1", "Cell 2", "Cell 3", "Cell 4", "Cell 5", "Cell 6", "Cell 7", "Cell 8", "Cell 9", "Cell 10", "Cell 11", "Cell 12", "Cell 13", "Cell 14", "Cell 15", "Cell 16", "Cell 17", "Cell 18", "Cell 19", "Cell 20", "Cell 21", "Cell 22", "Cell 23", "Cell 24", "Cell 25", "Cell 26", "Cell 27", "Cell 28", "Cell 29"]
You can use the the uia
method to make raw JavaScript calls to manipulate pickers.
uia(%Q[uia.selectPickerValues('{0 "5"}')])
uia(%Q[uia.selectPickerValues('{0 "3" 1 "59" 2 "PM"}')])
uia(%Q[uia.selectPickerValues('{0 "April" 2 "2017"}')])
For UIDatePickers, you can use the date picker API.
# Set the time to 10:45
target_time = Time.parse("10:45")
current_date = date_time_from_picker()
current_date = DateTime.new(current_date.year,
current_date.mon,
current_date.day,
target_time.hour,
target_time.min,
0,
target_time.gmt_offset)
picker_set_date_time current_date
# Set the date to July 28 2009
target_date = Date.parse("July 28 2009")
current_time = date_time_from_picker()
date_time = DateTime.new(target_date.year,
target_date.mon,
target_date.day,
current_time.hour,
current_time.min,
0,
Time.now.sec,
current_time.offset)
picker_set_date_time date_time
Rotates the device/simulator. The dir
argument can be one of: :left
and :right
.
irb(main):083:0> rotate :left
On iOS5.x+ this will simulate a change in the location of the device.
Takes either a :place => "Tower of London"
or a :latitude => ..., :longitude => ...
.
When using the :place
form, the location will be translated to coordinates using a Google API (so network is needed).
This is an escape hatch for when you need to perform a selector directly on your App Delegate.
# In your UIApplicationDelegate file:
- (NSString *)calabashBackdoorExample:(NSString *)aString {
return aString;
}
# From the ruby client:
> backdoor("calabashBackdoorExample:", "Hello")
=> "Hello"
Backdoor methods can have almost any return type, take arguments, or have no arguments.
If your app is written in Swift, you can define backdoors like this:
# App Delegate
@objc
class AppDelegate: UIResponder, UIApplicationDelegate {
@objc
func calabashBackdoor(params:String) -> String {
return "success"
}
...
# Bridging Header
@interface AppDelegate : UIResponder <UIApplicationDelegate>
- (NSString *)calabashBackdoor:(NSString*)params;
...
# Ruby client
# Swift <= 2.2
> backdoor("calabashBackdoor:", '')
# Swift > 2.2
backdoor("calabashBackdoorWithParams:", '')
Thanks @gredman and @PetarBel for the Swift details.
Takes a screenshot of the app.
screenshot({:prefix => "/Users/krukow/tmp", :name=>"my.png"})
If prefix and name are nil it will use default values (which is currently the line in the current feature).
Takes a screenshot of the app and embeds to cucumber reporters (e.g. html reports).
screenshot_embed({:prefix => "/Users/krukow/tmp", :name=>"my.png", :label => "Mine"})
If prefix and name are nil it will use default values (which is currently the line in the current feature).
Label is the label used in the cucumber report output (equals to name if not specified).
Prints information about the device/simulator and Calabash server version.
irb(main):026:0> server_version
=> {"outcome"=>"SUCCESS", "app_name"=>"LPSimpleExample-cal", "simulator_device"=>"iPhone", "iOS_version"=>"5.1", "app_version"=>"1.0", "system"=>"x86_64", "app_id"=>"com.lesspainful.example.LPSimpleExample-cal", "version"=>"0.9.126", "simulator"=>"iPhone Simulator 358.4, iPhone OS 5.1 (iPhone/9B176)"}
Prints information about the Calabash client version.
irb(main):027:0> client_version
=> "0.13.0"
Will terminate the application.
irb(main):028:0> calabash_exit
=> []
irb(main):029:0> server_version
Errno::ECONNREFUSED: Connection refused - connect(2) (http://localhost:37265)
Escapes text containing single quotes. Calabash iOS has some annoying rules for text containing single quotes. This helper frees you from manual escaping.
irb(main):007:0> quoted = escape_quotes("Karl's child")
=> "Karl\\'s child"
irb(main):008:0> query("view marked:'#{quoted}'")
Performs a 'flash' on elements matching query. This can be useful when using the console, and you want to highlight the result for presentation or clarity.