skip to content

In an Appium `-android uiautomator` expression, how do chained UiSelector matchers combine?

level: middleimportance: must knowfreq 54%

answer

  1. criteria narrow, never widen
  2. no OR inside one expression
  3. childSelector and fromParent move the node
  4. index is siblings, instance is matches

basics

~20 s

Chained UiSelector calls narrow the match: every criterion must hold on the same node. Two calls are exceptions — childSelector moves the rest of the expression to a descendant, and fromParent moves it to a sibling subtree.

solid answer

~40 s

In an `-android uiautomator` value, each call chained onto `new UiSelector()` adds a criterion, and Android's UiAutomator framework ANDs them: `new UiSelector().className("android.widget.EditText").enabled(true)` matches one node that is both. There is no OR on a chain — you widen with a regex form such as `textMatches(...)`, or you issue a second find. Two calls are not criteria at all: `childSelector(...)` re-points the rest of the chain at a descendant of the current match, and `fromParent(...)` at a sibling subtree. Text and description come in exact, `*Contains` and `*Matches` forms, and `text` is exact by default. Finally `index(n)` is a node's position among its siblings while `instance(n)` is the n-th screen-wide match of the whole selector — confusing the two is the usual reason a right-looking expression grabs the wrong row.

go deeper

for a junior

Know that chaining calls onto new UiSelector() makes the match stricter, not looser, and that every criterion must hold on one and the same element.

for a middle

Explain the matcher families, the exact-versus-contains-versus-matches split, and how childSelector and fromParent move the expression to a different node instead of constraining the current one.

for a senior

Demonstrate the index-versus-instance distinction with a concrete failure, and describe how you narrow an over-specified expression down to the criterion that is actually false.

for a principal

Be able to say what the chain cannot express — no disjunction, no arithmetic, nothing beyond one framework's matcher API — so reaching for it stays a considered move rather than a habit.

## Criteria narrow, and they narrow one node Inside an Appium `-android uiautomator` selector, every call chained onto `new UiSelector()` adds a **criterion**, and Android's UiAutomator framework requires all of them to hold on the **same** node. `new UiSelector().className("android.widget.EditText").enabled(true)` matches a node that is an `EditText` *and* is enabled. Chaining never widens a match, and `UiSelector` has no disjunction on the chain, so an "either/or" match is expressed either with a regex form or by issuing two separate finds. That single rule explains most surprises. An expression returning nothing is usually **over-specified** — one criterion in the chain is false — and the quickest diagnosis is to strip criteria until it matches, then add them back. ## The matcher families The chain's vocabulary comes in families, and most families have three shapes: - **Exact** — `text("Submit")`, `description("Scan pass")`, `className("android.widget.Button")`. - **Substring** — `textContains("Sub")`, `descriptionContains("Scan")`. - **Regex** — `textMatches("Sub.*")`, `resourceIdMatches(".*:id/submit")`, `classNameMatches(...)`. Alongside them sit the boolean state matchers — `enabled`, `checked`, `checkable`, `clickable`, `longClickable`, `focusable`, `focused`, `selected`, `scrollable` — and the identity matchers `resourceId` and `packageName`. Two notes matter in practice. `text` is **exact** by default; expecting substring behaviour from it is a standard mistake, and `textContains` is the call that was wanted. And `resourceId` wants the fully qualified `package:id/name` form, because the expression reaches the framework exactly as you typed it — nothing on the host expands a bare id for you. ## The two calls that are not criteria `childSelector(...)` and `fromParent(...)` behave differently: each takes a `UiSelector` of its own and **re-points the rest of the expression at another node**. - `new UiSelector().resourceId("com.example.museum:id/row").childSelector(new UiSelector().className("android.widget.CheckBox"))` matches a checkbox **inside** a matched row, and the checkbox is what the find returns. - `new UiSelector().text("February").fromParent(new UiSelector().resourceId("com.example.museum:id/count"))` climbs to the parent of the matched node and then descends into a **sibling** subtree. Reading a chain therefore means asking, at every call, "is this narrowing the current node, or moving to a new one?" A weak answer treats `childSelector` as just another criterion and then cannot explain why the returned element is not the one the first half of the chain described. ## `index` is not `instance` The two integer matchers are the classic trap, and they answer different questions. | call | what the number means | |---|---| | `index(n)` | the node's position among its **siblings** in the layout hierarchy | | `instance(n)` | the **n-th match** of the whole selector, counted across what the framework can currently see | So `new UiSelector().text("February").instance(1)` means *the second element on screen whose text is February*, while `new UiSelector().text("February").index(1)` means *an element whose text is February and which happens to be the second child of its parent* — a completely different question, and usually not the one intended. Both are zero-based. `instance` carries a further subtlety on a scrolling screen: it counts the matches present in the hierarchy at that moment, and a list that recycles its rows does not have the off-screen ones there to be counted. That is one of the reasons the scrolling variant of this dialect exists at all. ## Putting a chain together A workable habit for building an expression: 1. Start with the single most selective criterion you trust — usually `resourceId`. 2. Add one criterion at a time and re-run the find, so you always know which addition broke it. 3. Reach for `childSelector` or `fromParent` only when the target genuinely has no stable identity of its own. 4. Keep `instance` as a last resort, and never write `index` when you meant `instance`. The reason to work incrementally is the strategy's nature: the expression is a string that nothing on your machine validates, so the find itself is the only feedback loop you have. ## Why the shape is what it is The chain is not an accident of syntax. Android's UiAutomator framework walks a tree of accessibility nodes and tests cheap predicates as it descends, so a set of ANDed criteria on one node is exactly what it can evaluate as it goes, and a nested `UiSelector` is how the API expresses "now continue from somewhere else". Reading the chain as *criteria plus two navigation moves* is enough to understand or write any expression you will meet in an Appium Android suite.

  • How would you express an OR inside a single `-android uiautomator` selector?
    You would not. A `UiSelector` chain only narrows, and the API has no disjunction on it. Either widen with a regex form such as `textMatches("Save|Submit")`, or issue two finds. Appium sends one expression per find request, so the alternation has to live inside a single matcher if it lives anywhere.
  • Why might `new UiSelector().text("February").instance(1)` match nothing on a screen that visibly shows two February rows?
    `instance` counts the matches the framework can currently see. A list that recycles its rows keeps only the rendered ones in the hierarchy, so the second `February` may not exist yet to be counted. Indexing into a virtualised list is precisely the case the scrolling form of this dialect was added for.

saying these in an interview costs you the question

  • Thinks chained UiSelector criteria are ORed together
  • Uses index where instance was meant, then blames flakiness
  • Believes childSelector adds another criterion to the same node
  • Expects a bare resource id to work without its package prefix
  • Assumes the text matcher is substring by default