Selectors¶
Examples are in Python
Kotlin has the same builders in camelCase (class_name → className, and_hint →
andHint, & / | → and / or). Kotlin + JUnit 5 shows
this page's examples in Kotlin.
A selector describes which node of the accessibility tree you mean. It is a small, immutable
value: no I/O happens when you build one. app.element(selector) (for
app = device.app("com.shop")) or device.screen.element(selector) wraps it in an Element
whose every action resolves the selector again on the device, so there is nothing to go stale.
from tap_e2e import res, text, class_name
app = device.app("com.shop")
app.element(res("buy_button")).tap() # com.shop:id/buy_button
app.element(text("Add to cart").clickable()).tap()
app.element(class_name("android.widget.EditText").and_hint("Search")).set_text("socks")
Where the selector looks, one app's nodes or the whole screen, is chosen by what you call
element on, not by the selector: see App or screen.
Entry points¶
| Builder | Matches |
|---|---|
res(name) |
resource id name in any package: a View id <pkg>:id/name, or a Compose testTag name when the app sets testTagsAsResourceId. Under app.element only that app's nodes match, so this is the app's own id |
res_id(pkg, name) |
exactly the View id pkg:id/name, for an id a library or the system defines under another package (res_id("android", "button1")) |
text(value) |
exact text |
text_contains(v), text_starts_with(v) |
substring / prefix |
text_matches(re2) |
RE2 regular expression (full match) |
desc(value) |
content description |
hint(value) |
hint of an empty text field |
class_name(value) |
widget class name |
clickable(), scrollable() |
any node with that property |
text, desc, hint and class_name take a MatchMode (EXACT, CONTAINS,
STARTS_WITH, ENDS_WITH, REGEX, also exported as plain constants). A name passed to
res / and_res is the bare name: res("pkg:id/name") is rejected
(QUALIFIED_RESOURCE_NAME); write res_id("pkg", "name"). Regexes are RE2: linear-time, no
backreferences or lookaround, and an invalid pattern is rejected before it reaches the device.
Text vs hint
An empty EditText reports its hint as its accessibility text. Tap passes that through
unchanged: text selectors match it and element.text() returns it, while
snapshot().showing_hint says the text is the hint. Use hint(...) to find an empty field
by its hint.
Refinements¶
Everything after the entry point narrows the same node:
.and_text(v),.and_desc(v),.and_hint(v),.and_class_name(v),.and_res(name),.and_res(pkg, name).clickable(),.long_clickable(),.scrollable(),.checkable(),.checked(),.enabled(),.focusable(),.focused(),.selected(), each with an optionalbool
text("Remember me").checkable().checked(False) # an unchecked checkbox labelled "Remember me"
Combining selectors¶
Two selectors combine into one with & (and) and | (or), or with all_of(...) /
any_of(...) for a list. The result is still one selector describing one node: exists(),
count(), the exactly-one rule and every action apply to it as a whole.
# the permission button, whichever wording this Android version uses
allow = text("Allow") | text("Allow only while using the app") | text("While using the app")
device.screen.element(allow).tap()
# both on the same node
app.element(text("Add") & clickable()).tap() # same as text("Add").clickable()
Combinators nest freely ((a | b) & clickable()), including inside relations
(has_child(a | b)). The result keeps the left-hand operand's first() / at(n). Nothing is
dropped silently: a right-hand operand (of & / | or a has_* relation) that carries its
own first() / at(n) is rejected with ValueError; apply it to the combined selector instead.
A selector with an or is evaluated by walking the window's node tree rather than by a native UiAutomator lookup. It returns exactly the same matches and never dumps the hierarchy, but on a very large screen it is a little slower: prefer a single distinguishing property when one exists.
Relations¶
Two flavours. Constrain by relatives keeps matching the outer node:
| Method | The matched node… |
|---|---|
.has_descendant(s) |
has some descendant matching s |
.has_child(s) |
has a direct child matching s |
.has_parent(s) |
has a parent matching s |
.has_ancestor(s) |
has an ancestor matching s |
Navigate to relatives returns the inner node:
| Method | Returns |
|---|---|
.descendant(s) |
the node matching s somewhere below |
.child(s) |
the direct child matching s |
# the "Delete" button inside the row whose title is "Socks"
row = class_name("android.view.ViewGroup").has_descendant(text("Socks"))
app.element(row.descendant(text("Delete"))).tap()
Element.descendant(...) and Element.child(...) exist too, so you can navigate from an
Element you already hold. The result keeps the inner selector's first() / at(n). The outer
selector itself cannot carry first() / at(n): rows.at(2).descendant(x) would otherwise
search under every row, so it is rejected. Narrow the outer node with properties, or pick among
the results (rows.descendant(x).at(2)).
Exactly one, or say otherwise¶
By default a selector must match exactly one node for any action (tap, set_text,
swipe, scroll containers, snapshot). Two matches fail with AMBIGUOUS, none with
NOT_FOUND, and in both cases nothing was touched. exists() and count() do not care.
When a repeated element is genuinely what you want, opt in explicitly:
app.element(text("Add").first()).tap() # first in accessibility order
app.element(class_name("Button").at(2)).tap() # third in accessibility order
Accessibility order is stable for a given screen but is not guaranteed to be visual order;
prefer a distinguishing relation (has_descendant(text(...))) when there is one.
Limits and what is not supported¶
- Depth ≤ 32, ≤ 256 nodes, ≤ 1024 characters per string; an and / or needs at least two
operands (the builders never produce fewer). Violations are
INVALID_SELECTORwith a stable detail, raised before a request is sent. - No NOT, sibling or "nearest". A negative or positional selector tends to match something unintended when the screen changes; describe the node you want instead.
- No XPath and no string query language. Selectors are an AST validated identically by the client, the server and the driver, and compiled on the device to window-scoped UiAutomator lookups. This is what keeps lookups fast and error messages precise.
- No element handles. There is no
find_element()returning an id to reuse; anElementis the selector plus the device. Re-resolving is cheap (one round trip, no hierarchy dump) and removes stale-element errors as a class. - No coordinates. Taps and gestures are always relative to a matched element. If you need a raw point, that is a sign the accessibility tree is missing something worth fixing in the app.
Debugging a selector¶
selector.render()(orstr(selector)) prints the exact expression tree the device will see; it also appears in everyCommandError.app.element(sel).count()tells you how many nodes match right now.device.dump_hierarchy()returns the accessibility XML for the current screen (diagnostic only, never used by lookups). The failure artifacts contain the same dump, and Tap Studio shows every node's selectors on a live screen.