Calabash iOS Ruby API

Joshua Moody edited this page Dec 16, 2016 · 12 revisions

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

query(uiquery, *args)

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]

classes(uiquery)

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"]

label(uiquery)

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"]

element_does_not_exist(uiquery)

element_exists(uiquery)

view_with_mark_exists(expected_mark)

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.

Waiting

wait_for(options, &block)

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.

wait_for_elements_exist(elements_arr, options={})

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)

wait_for_elements_do_not_exist(elements_arr, options={})

Similar to wait_for_elements_exist, but waits for all of the elements to not exist.

wait_for_none_animating(options={})

Waits until for animations to complete.

wait_poll(opts, &block)

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

wait_for_no_network_indicator(opts)

Waits for the network indicator in the status bar to disappear. Options (opts) are passed to wait_for.

Assertions

fail(msg="Error. Check log for details.")

Will fail the test with message msg. Takes a screenshot.

check_element_exists(query)

check_element_does_not_exist(query)

check_view_with_mark_exists(expected_mark)

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}'")

Touch

touch(uiquery, options={})

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

Keyboard

keyboard_enter_char(chr)

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"

keyboard_enter_text(text)

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"

tap_keyboard_action_key

Enters "Done" or "Search" (the "Return" char, often the lower-right most button in keyboard).

Scroll

scroll(uiquery, direction)

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.

Tables

scroll_to_row(uiquery, number)

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

scroll_to_cell(options)

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

each_cell(options, &block)

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"]

Pickers

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

Rotation

rotate(dir)

Rotates the device/simulator. The dir argument can be one of: :left and :right.

irb(main):083:0> rotate :left

Location

set_location(options)

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

Backdoor

backdoor(sel, arg)

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.

Screenshot

screenshot(options={:prefix=>nil, :name=>nil})

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

screenshot_embed(options={:prefix=>nil, :name=>nil, :label => nil})

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

Misc

server_version

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)"}

client_version

Prints information about the Calabash client version.

irb(main):027:0> client_version
=> "0.13.0"

calabash_exit

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)

escape_quotes(str)

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}'")

flash(uiquery)

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.

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.