Hey guys, I will post today some queries to find elements in different ways with calabash-android in console !
Basic usage
Using the class of the elements (Button, Label, View, Webview, Tableview, etc…). If you don’t know what is the class, you can use only * to find all the elements on the screen.
1
2
|
irb(main):001:0> query(“Button”)
|
1
2
|
irb(main):001:0> query(“*”)
|
Results:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
[
[0] {
“id” => “login_button”,
“enabled” => true,
“contentDescription” => nil,
“class” => “android.widget.Button”,
“text” => “Login”,
“rect” => {
“center_y” => 545.0,
“center_x” => 639.5,
“height” => 64,
“y” => 513,
“width” => 195,
“x” => 542
},
}
]
|
You can use size, to know the qty of the elements with this class:
1
2
3
|
irb(main):004:0> query(“TextView”).size
3
|
Filtering by class
Simple class name
Filtering by class means that we only want to look at certain types of views. This is what we’ve done before when we were searching for Button or TextView:
1
2
3
4
5
6
7
8
|
irb(main):001:0> query(“Button”)
[0] {
“id” => “login_button”,
“enabled” => true,
“text” => “”,
“class” => “android.widget.Button”,
}
|
This is called filtering by simple name. The result shows you that the full class name is android.widget.Button
: this is what we call fully qualified class name and the andorid.widget
part is called package name.
When you query with simple name Calabash only looks at the last bit of the name. This is also case insensitive, so Button
, button
or bUtTOn
is the same in this context.
Fully qualified class name
You can also use the fully qualified class name in the query:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
irb(main):001:0> query(“android.widget.Button”)
[
[0] {
“id” => “robot_question_checkbox”,
“enabled” => true,
“text” => “”,
“class” => “android.widget.CheckBox”,
“checked” => true
},
[1] {
“id” => “login_button”,
“enabled” => true,
“class” => “android.widget.Button”,
“text” => “Login”,
}
]
|
Interestingly this gives us two results: the login button and a checkbox. Why is the CheckBox included? In order to understand this you have to understand subclasses.
Let’s look at an example:
There are a lot different buttons in Android like ToggleButton
and the Switch
. A ToogleButton
is just a specific type of Button
with some extra functionality and look. A Button
is just a simple button with a text in it where the ToggleButton
has two states (on and off). So every ToggleButton
is a Button
but not every Button
is a ToggleButton
.
The CheckBox
is also a subclass of Button
– which explains why we get two results. If you use the fully qualified name in the query it will give you the class and all of it’s subclasses. So if there were ToggleButtons and Switches it would also return those.
The main distinction is this:
- using simple name only looks at the last part of the class and does a simple comparison
- using the qualified name looks at the class hierarchy and gives back all the class and all of it’s subclasses
Filtering by property
Usually there are several buttons, textviews and edit texts on the screen so filtering by class is not enough to identify a single element.
In the query results you get back a set of properties for each view like id
, text
, class
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
“id” => “login_button”,
“enabled” => true,
“contentDescription” => nil,
“class” => “android.widget.Button”,
“text” => “Login”,
“rect” => {
“center_y” => 545.0,
“center_x” => 639.5,
“height” => 64,
“y” => 513,
“width” => 195,
“x” => 542
},
]
|
Calabash allows you to filter on these properties.
The most used property is the id
. On Android every view can have an id, which is an invisible string identifier. The id is not necessarily unique and also not every view has one. Even with these restriction using id is the most reliable way to find views.
id property
Let’s try to find the login button by id:
1
|
irb(main):001:0> query(“* id:’login_button'”)
|
The first part of the query is the class filtering: *
means give me all the views. The second part is the property filtering: id:'login_button'
means find the view that has the id login_button
.
The filtering happens on the result of the first part of the query string. Previously we used "*"
so the filtering was applied to all the views. But we could have used different class.
1
|
query(“TextView id:’login_button'”)
|
text property
Of course id
is not the only property that you can filter on. Another commonly used one is the text
. This is the label of the view which is visible on the screen:
1
|
query(“* text:’Login'”)
|
This will find the view on the screen which has a text Login
. Once I found them though I use id
instead of text
. This results in more stable tests. Labels tend to change more frequently than ids. It also eliminates all the translation and localisation problems.
What if a view doesn’t have an id
? Ask the developers to add one. It’s a 5 second task and it will make your life a lot easier.
Other properties
Let’s try to find all the disabled buttons:
1
2
|
query(“* enabled:false”)
|
Notice that while we used single quotes for string parameters we’re not using anything here for false
.
The third type of parameter is the integer parameter (numbers). One example is the width parameter:
1
2
|
query(“* width:200”)
|
This gives you all the views that are exactly 200 pixel wide. Notice that we are not using the single quotes to define the value.
You can also combine the filters using multiple criteria. Let’s get all the views which are enabled and the are 50 pixel heigh:
1
2
|
query(“* enabled:true height:50”)
|
How it works
The general syntax for property filtering is the following:
1
2
|
prop:value
|
The prop
is the name of the property and the value
is either a string, a boolean or an integer value.
We’ve already used a couple of properties like id
, text
, enabled
and width
. There are a lot more. For starters you can use all the attributes that you see in the query results. But that’s not everything.
Every view object supports a set of methods – you can find all the supported methods on the view’s documentation. For example the View class has an isEnabled() method. When Calabash tries to resolve the property it find the following methods on the view: prop()
, getProp()
and isProp()
in this order.
So for the enabled
property it tries to find the enabled()
, getEnabled()
or isEnabled()
methods.
If Calabash does find a method it calls it and compares the return value with the value you specified. If it doesn’t match it gets rid of that view. It also discards the view if it can’t find the appropriate methods.
Useful things
In addition to the basic query syntax there are a couple of useful things you can use.
marked
The marked
keyword is a shorthand for identifying an element by it’s name. It’s a convenience method to filter by id
, text
or contentDescription
properties.
1
|
query(“button marked:’login_button'”)
|
index
The result of each query is a standard Ruby array. Because of this you can access the elements directly in it. If you want to get the first view on screen you can do it like this:
1
2
|
query(“*”)[0]
|
The query language also supports the index
keyword. The index is very similar than using the array indexing – it gives you the element at a given index:
1
2
|
query(“* index:0”)
|
This is usually used with lists and other collection views. Often you just want to click on the first element on a list.
Be warned though: you should only use index if you really need it. Using index to identify elements in the layout is usually not a good idea – it leads to brittle tests and hard to diagnose problems.
Use ids, text and other identifiers and don’t rely on the ordering of the items!
Returning certain properties
Sometimes you’re only interested in certain properties not the full result. Let’s say you want to get all the id’s on the screen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
query(“*”, :id)
[
[ 0] nil,
[ 1] nil,
[ 2] “content”,
[ 3] “main_layout”,
[ 4] “logo”,
[ 5] “title”,
[ 6] “login_form_container”,
[ 7] “email_container”,
[ 8] “email_label”,
[ 9] “email_edittext”,
[10] “robot_question_container”,
[11] “robot_question_label”,
[12] “robot_question_checkbox”,
[13] “login_button”
]
|
As you can see a result is an array of strings where only the ids are shown. Isn’t this much nicer?
You can put any property as the second argument of query to get the data you’re interested in. Another example to get all the labels for the enabled views:
query("* enabled:true", :text)
Predicate
Calabash supports some filters that are not present in UIScript. Particularly we support filtering by simple NSPredicates. For example searching for a string prefix:
"label {text BEGINSWITH 'Cell 1'}"
which would return the labels with text Cell 1 and Cell 10.
In general you use a NSPredicate by writing a filter: {selector OP val}
, where selector
is the name of an Objective-C selector to perform on the object, OP
is operation, and val
is a string or integer value.
Common operations
BEGINSWITH
, prefix, e.g.,"label {text BEGINSWITH 'Cell 1'}"
ENDSWITH
, suffix, e.g.,"label {text ENDSWITH '10'}"
LIKE
, wildcard searches, e.g.,"label {text LIKE 'C*ll'}"
CONTAINS
, substring, e.g.,"label {text CONTAINS 'ell'}"
- Comparison,
<
,>
, … - Case or diacritic insensitive lookups, e.g.,
"label {text CONTAINS[cd] 'cell'}"
To understand what additional options are available, consult the full syntax for NSPredicate documented by Apple: NSPredicate Syntax
Direction
There are four directions descendant
, child
, parent
and sibling
. These determines the direction in which search proceeds.
Often, query expressions are a sequence of ClassName expressions. For example:
"tableViewCell label"
this means “first find all UITableViewCell views, then inside of those, find all the UILabel views”. The key here is the word inside. This is determined by the query direction
.
By default the direction is descendant
, which intuitively means “search amongst all subviews (or sub-views of sub-views, etc).” But you can change the traversal direction. Here is an advanced example:
label marked:'Tears in Heaven' parent tableViewCell descendant
tableViewCellReorderControl
This query finds a label ‘Tears in Heaven’, and then proceeds to find the tableViewCell that contains this label (i.e., moving in the parent
instead of descendant direction). From the tableViewCell we move down and find the tableViewCellReorderControl.
Valid directions are descendant
, parent
, child
and sibling
. Both descendant
and child
looks for subviews inside a view. The difference is that descendant
keep searching down in the sub-view’s subviews, whereas child
only looks down one level. The direction sibling
searches for views that are “at the same level” as the present view (this is the same as: first find the immediate parent, then find all subviews except for the view itself).
Thank you again !
Bye 🙂
Fonts:
https://github.com/calabash/calabash-ios/wiki/05-Query-syntax
One thought on “Query Calabash Android”