In Appium, how much should one `-ios predicate string` pin down, and why?
answer
- two failure modes, opposite directions
- a loose match is a false pass
- type is free, identifiers are cheap
- find, then assert separately
basics
~20 sPin the smallest set of attributes that makes the iOS match unambiguous — usually type plus one identifier. Every extra clause buys precision but costs a rewrite when that attribute changes, and makes a failure harder to read.
solid answer
~40 sThere are two ways to get this wrong on Apple platforms and they fail differently. Too loose — `label CONTAINS 'Book'` — matches several ice-rink session rows, and Find Element quietly returns the first, so the suite books the wrong session and the test still passes. Too tight — `type == 'XCUIElementTypeButton' AND name == 'rink.session.book' AND label CONTAINS 'public' AND value == '1'` — breaks whenever any one of four attributes changes, and reports one undifferentiated no-such-element for all four. The working rule is: `type` plus the single most stable identifying attribute, and a further clause only when a match is genuinely ambiguous. Anything you want to *assert* — the session time, the price, the enabled state — belongs in an assertion after the find, where a failure names the attribute that moved.
go deeper
Know that a predicate can match more than one control and that the find returns the first. Check how many elements your expression matches before trusting it.
Explain the trade: each clause narrows the match but adds an app behaviour that must stay true, and a no-such-element failure never says which clause broke.
Show the discipline in practice — type plus one identifying clause, count the matches, scope rather than lengthen, and keep state checks in assertions where failures name themselves.
Frame it as identifier coverage, not locator style. Set the rule for which attributes predicates may compare and treat long expressions as evidence the app-side contract is missing.
## Two ways to get it wrong A predicate is a filter, and every clause you add narrows it. That gives you a dial with a bad failure mode at each end, and on an ice-rink session app both ends are easy to reach. **Too loose** matches more than one control. `label CONTAINS 'Book'` matches the booking button on every session row on screen. Nothing errors: Find Element returns the first match the framework yields, the test taps it, and the run goes green having booked the 09:00 beginners' session instead of the 18:30 freestyle one. This is the more expensive failure because it is not a failure — it is a false pass that only surfaces when someone reads the booking data. **Too tight** matches nothing the moment any pinned attribute moves. `type == 'XCUIElementTypeButton' AND name == 'rink.session.book' AND label CONTAINS 'public' AND value == '1'` is four independent bets on app behaviour. When it breaks you get one message — no such element — with no indication which of the four clauses stopped being true, so triage begins by dismantling the expression. ## What each clause buys and costs | Clause | Buys | Costs | |---|---|---| | `type == 'XCUIElementType…'` | removes whole classes of near-matches, essentially free | almost nothing; the class rarely changes | | `name == '…'` | an exact, translation-proof match | depends on the app team setting an identifier | | `label CONTAINS[cd] '…'` | works with no app-side change | breaks on translation and on copy edits | | `value == '…'` | disambiguates identical controls by state | couples the locator to state that the test is often about | | a second `label`/`value` clause | resolves a genuinely ambiguous screen | one more thing that must stay true forever | The asymmetry in that table is the whole answer. `type` is nearly free, an identifier is cheap once it exists, and everything after that is a standing liability you are choosing to carry. ## A working rule 1. Always pin `type`. It costs nothing, removes accidental matches against static text and container elements, and never breaks on a copy change. 2. Add exactly one identifying clause — `name` where the app sets an identifier, otherwise the most stable fragment of `label` with `[cd]`. 3. Stop. Run Find Elements and count the matches. One match means you are done. 4. Only if the count is above one, add a third clause — and prefer scoping the find to a parent element over adding a clause, because scope is easier to read than a long conjunction. 5. Never add a clause "for safety". Every clause is a future failure with no name attached to it. ## Logic that does not belong in the predicate The strongest version of this answer separates *finding* from *checking*: - **Assertions.** If the test cares that the 18:30 session is priced at £8 and is enabled, find the row by identifier and then assert those attributes. A failed assertion names the attribute; a failed predicate does not. - **Hierarchy.** A predicate has no path steps. When the real constraint is "the button inside that cell", scope the find to the cell you already hold, or use Appium's XCUITest `-ios class chain` strategy, which is the strategy with steps. - **Ordering.** There is no index in the predicate language. If a case depends on "the third session row", the honest expression of that is Find Elements plus indexing in the test, where it is visible. ## Making the failure legible A predicate is a single string, which is why an over-stuffed one is opaque. Two habits recover the readability without loosening the match: build the predicate from named fragments in the page object so a diff shows which clause changed, and keep the naming of ice-rink identifiers systematic — `rink.session.row.<time>.book` — so a `BEGINSWITH` clause can address a family and an `==` clause can address one member, without a compound expression in either case. ## The policy a team actually needs At suite scale this stops being a locator question. The team decides which attributes iOS predicates may compare, and that decision is a contract with the app developers: identifiers on every interactive control means predicates stay short; no identifiers means every predicate compares translated copy and the suite carries the maintenance forever. Write the rule down, and treat a four-clause predicate in review as a signal that the identifier coverage is missing — not as a locator to be tuned.
- Why is an over-loose predicate more dangerous than an over-tight one?An over-tight predicate fails loudly and gets fixed within the hour. An over-loose one matches several controls, Find Element returns the first, and the run passes having exercised the wrong ice-rink session row. The suite reports success while covering nothing you intended, which is the failure mode that survives for months.
- How would you enforce a predicate-length rule across a large iOS suite?Not by counting clauses in review. Keep locators in page objects so they are reviewable in one place, treat any predicate with more than two clauses as a request for an app-side identifier, and track the controls that still lack one. The rule is a signal for missing identifier coverage rather than a style limit.
saying these in an interview costs you the question
- Adds clauses for safety without checking the match count.
- Thinks a longer predicate is inherently more reliable.
- Puts assertions about state inside the locator expression.
- Depends on Find Element returning the row they expect.
- Tries to express parent or child constraints inside the predicate.