In an Appium `-android uiautomator` expression, how do chained UiSelector matchers combine?
answer
- criteria narrow, never widen
- no OR inside one expression
- childSelector and fromParent move the node
- index is siblings, instance is matches
basics
~20 sChained 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 sIn 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
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.
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.
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.
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