ai newsSa., 24. Okt.
EilmeldungYour Claude Code Worktree Subagent Can't See Your Feature Branch. Set baseRef to head

UiSelector schlägt XPath auf Android in allen 30 Benchmark-Fällen

Android-Tests zeigen: UiSelector ist schneller als XPath und spart je nach Elementzahl bis zu 49 Prozent pro Lookup. Zudem lassen sich 73 Prozent von 1645 echten XPath-Locatoren in einen UiSelector umwandeln, der im Emulator läuft.

dev.toSamstag 15:15 · 24.10.DE7 Min. Lesezeit
Bild: dev.to

npx xpath-to-uiselector./tests # preview the diff npx xpath-to-uiselector./tests --write # apply it Enter fullscreen mode Exit fullscreen mode Or paste your XPaths into the online converter.

This is the Android follow-up to my iOS posts. In part 1 I built a converter from XPath to iOS class chain; in part 2 I asked Claude to do the same job. On iOS the best model came within a few points of the converter.

On Android I did both in one go: I built xpath-to-uiselector, then asked Claude Opus, Sonnet and Haiku 5.5 to convert the same locators. Android turned out to be much harder for the models, and the reason is in the API, not in the models.

With the Appium UiAutomator2 driver, an XPath lookup first serializes the whole UI tree to XML and then runs the query on it. A UiSelector (the -android uiautomator strategy) walks the live tree and, for findElement, stops at the first match. I measured both in a separate benchmark: 30 cases (10 XPath shapes × 3 targets), each found by both strategies, on screens of 100 to 2000 elements.

UiSelector is the androidx UiAutomator query builder. Appium takes it as a Java-like string and evaluates it on the device:

It has no direct-child or parent step and no position among siblings: childSelector means any descendant, and fromParent searches among the siblings.

The simple rows above are what everyone converts correctly. The trouble is the methods whose names promise the same thing as XPath but behave differently. All of these were checked on the emulator:

The server accepts all of these: no syntax error, just other elements (or none). The first one is debatable (more on that below ), but it changes what the locator finds without anyone noticing: the test passes today and taps another element when someone adds a "TOTAL" label above it.

xpath-to-uiselector scans your test files (Java, Kotlin, Python, JS/TS, C#, Ruby, JSON, YAML), finds the XPath strings and rewrites each one together with its strategy call:

- AppiumBy.xpath("(//android.widget.TextView[@content-desc='price'])") + AppiumBy.androidUIAutomator("new UiSelector().className(\"android.widget.TextView\").description(\"price\").instance(1)") - (AppiumBy.XPATH, '//*[@content-desc="Cart" and contains(@text, "USD")]') + (AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().description("Cart").textMatches("(?s).*USD.*")') [skip] UiSelector has no direct-child criterion: //android.widget.ListView/android.widget.TextView Enter fullscreen mode Exit fullscreen mode Every XPath ends up in one of three groups:

The default is strict: a converted locator finds the same elements as your XPath, so contains(@text,"Total") becomes the case-sensitive textMatches("(?s).*Total.*"). If you prefer simpler, more tolerant locators, --loose writes textContains / textStartsWith (they ignore case) and turns a direct child //A/B into childSelector. Each loose form is marked close with a warning. On the real locators, --loose converts 77 % instead of 73 %.

The bare command only previews. --write saves, --json writes a report with every skip and warning. Node.js 20+, no dependencies. To try XPaths without installing anything, use the online converter: it runs the same code in the browser.

The order was facts first, the converter last:

Android 16 emulator (and Android 17 for most edge cases), Appium 3.8.0, UiAutomator2 driver 8.7.0, default driver settings. The result: 73 % convert (64 % exact, 9 % close). Most of the rest have no exact UiSelector at all: direct children, absolute /hierarchy/… paths, sibling positions, web and iOS attributes.

I used the same setup as in the iOS comparison:

Scoring: a correct UiSelector earns 1 point, a close one 0.5, a skip 0, and a wrong one −1: it is worse than a skip, because it looks like a valid locator and finds another element. The converter's 471.5 points are 100 %.

"Missed" means the model skipped a locator that the converter converts correctly. Cost is the API list price the CLI reports.

More than a third of all wrong answers are one pattern: textContains for contains(). Every model wrote it 20–23 times. The next group is a regex without (?s), where. stops at a line break; real resource ids have no line breaks, so those are mostly harmless. Then come the Java-style \\ (Sonnet alone: 15), values that exist only in the page source (an empty text, a toast, a slider's value), and positions inside childSelector.

Mistakes per model Mistake Opus Sonnet Haiku Case-insensitive textContains / descriptionContains / textStartsWith 20 23 22 Missing (?s) in a regex 14 8 16 Java-style \\ in a regex or value 1 15 2 Values only the page source has ( @text="", a toast, a range widget's value) 0 9 9 Positions: instance() in childSelector, //X 8 0 3 classNameMatches, (?i) for translate(), dropped existence tests, criteria UiSelector does not have 4 5 11 Wrong 47 60 63 Each wrong answer is counted once, under the first mistake it shows.

By the definition of this test, yes: contains(@text,"Total") does not find "TOTAL", and textContains("Total") does. The judge screens include such traps on purpose, so every one of the 65 answers found other elements than the XPath there.

In a real app it is less clear. Many people who write contains(@text,"Total") probably never thought about case, and a case-insensitive locator survives a designer switching a label to upper case. The cost is the other side of the same thing: a migration of hundreds of locators quietly makes each one wider. Where a "TOTAL" header sits above the "Total: $5" line, findElement now returns the header. And a check that the label reads "Total" no longer catches a change to "TOTAL".

That is why the converter is strict by default and --loose is a flag you turn on knowingly. In fact, 55 of the 65 answers are character for character what the converter writes with --loose; the other 10 are edge cases where --loose keeps the strict form ( substring(), matches(), text(), a value with \ ). So without rules, Claude converts the way --loose does, only without saying that the locator became wider. If you count textContains as acceptable, the models made at least 27, 37 and 41 wrong answers without rules instead of 47, 60 and 63. Which side are you on? Tell me in the comments.

As on iOS, most of these mistakes are about knowing the driver, not about intelligence. So I wrote them down as general rules (about 8,000 characters, rules.md, no answers for particular locators) and ran the three models again:

The comparison also tested the converter. The first Opus run found one real converter bug: contains(@text,"1.5") was marked exact, but a slider at 1.5 has that text only in the page source, so the XPath finds the slider and the UiSelector does not. It is now a close match with a warning. Across the six runs, the models also found 8 locators the converter skipped although an exact UiSelector exists ( normalize-space(), string-length(), translate(), if … then … else, and a few more). I checked each one on the emulator and added all 8 to the converter. After that, no model beats the converter on any locator.

On iOS, the same three Claude 5.5 models with a plain request made 4–7 wrong class chains out of 676. On Android they made 47–63 out of 680, roughly ten times as many, and still at least 27–41 if textContains counts as fine.

My explanation: a class chain filter is an NSPredicate, a language the models know well, and its CONTAINS is case-sensitive unless you add [c], just like XPath. UiSelector reuses XPath-like names with different meanings: textContains ignores case, the string literal is not Java, instance inside childSelector counts per container. Those are exactly the mistakes in the chart above. A model that "knows" UiSelector from Java code samples has seen these methods used, but rarely with the details that make them differ from XPath.

The two runs are not identical (iOS ran at low and medium effort, Android at medium; the locators are different), so take the ratio as a direction, not a measurement.

Google plans to deprecate and remove UiSelector from UiAutomator, and the UiAutomator2 driver warns that every -android uiautomator locator will then stop working. No date or version is set: UiAutomator 2.4.0 (July 2026) still ships it, and the driver's server bundles 2.3.0. Appium has not announced a replacement yet ( appium-uiautomator2-driver#812 is open).

So converting today is a bet that UiSelector stays until Appium moves on; no date is set yet. XPath locators are not affected, and the converter only rewrites strings, so going back is a git revert. The repository has a monthly job that opens an issue when the driver changes a file the conversion rules depend on.

If you use Claude Code, the repository has a skill that does exactly that: it runs the converter, then reviews each skipped locator and suggests a rewrite, marked as unverified.

Thanks to Anthropic: Claude Code helped me build the converter, the checks and the comparison, and the Claude models are also the subject of the test. All numbers come from real runs.

Mikhail Yesipchuk, SDET/Senior QA Automation Engineer. Have you moved Android locators off XPath, and what broke? Tell me on LinkedIn or open an issue on GitHub.

Originaltitel: XPath to Android UiSelector: can Claude 5.5 replace a converter?Quelle: dev.to (dev.to). Original öffnen