<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mikhail Yesipchuk</title>
    <description>The latest articles on DEV Community by Mikhail Yesipchuk (@mikhail_yesipchuk).</description>
    <link>https://dev.to/mikhail_yesipchuk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4186569%2Fa1489eed-4925-4bf8-a347-4d2564a9b0c0.png</url>
      <title>DEV Community: Mikhail Yesipchuk</title>
      <link>https://dev.to/mikhail_yesipchuk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mikhail_yesipchuk"/>
    <language>en</language>
    <item>
      <title>XPath to Android UiSelector: can Claude 5.5 replace a converter?</title>
      <dc:creator>Mikhail Yesipchuk</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:15:55 +0000</pubDate>
      <link>https://dev.to/mikhail_yesipchuk/xpath-to-android-uiselector-can-claude-55-replace-a-converter-g75</link>
      <guid>https://dev.to/mikhail_yesipchuk/xpath-to-android-uiselector-can-claude-55-replace-a-converter-g75</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UiSelector is faster than XPath on Android:&lt;/strong&gt; it won all 30 benchmark cases. It saved 49 % per lookup at 100 elements and 12 % at 2000 (emulator). Below level 70 of the tree, XPath finds nothing at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;73 % of 1645 real XPath locators convert&lt;/strong&gt; to a &lt;code&gt;UiSelector&lt;/code&gt;. Every converted one ran on an emulator with &lt;strong&gt;0 wrong&lt;/strong&gt; results; the rest are skipped with a reason. Strict by default; &lt;code&gt;--loose&lt;/code&gt; gives simpler, case-insensitive forms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude 5.5 without rules: 47–63 wrong UiSelectors out of 680&lt;/strong&gt; (7–9 %). They look right and often pass a quick check. 65 of the 170 are &lt;code&gt;textContains&lt;/code&gt;, which ignores case: wrong by a strict definition, and open to debate. With a rules prompt, Opus and Sonnet made 1 and Haiku 4, and still no model beat the converter.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx xpath-to-uiselector ./tests          &lt;span class="c"&gt;# preview the diff&lt;/span&gt;
npx xpath-to-uiselector ./tests &lt;span class="nt"&gt;--write&lt;/span&gt;  &lt;span class="c"&gt;# apply it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or paste your XPaths into the &lt;a href="https://domovou69.github.io/xpath-to-uiselector/" rel="noopener noreferrer"&gt;online converter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why leave XPath on Android&lt;/li&gt;
&lt;li&gt;What a UiSelector is&lt;/li&gt;
&lt;li&gt;Why converting by hand goes wrong&lt;/li&gt;
&lt;li&gt;What the converter does&lt;/li&gt;
&lt;li&gt;How I checked it&lt;/li&gt;
&lt;li&gt;
Claude 5.5 against the converter

&lt;ul&gt;
&lt;li&gt;Is textContains really wrong?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Android is harder than iOS&lt;/li&gt;
&lt;li&gt;The future of UiSelector&lt;/li&gt;
&lt;li&gt;What I would use&lt;/li&gt;
&lt;li&gt;Limits&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;This is the Android follow-up to my iOS posts. In &lt;a href="https://dev.to/mikhail_yesipchuk/xpath-to-ios-class-chain-a-converter-you-can-trust-and-when-it-is-actually-faster-c6o"&gt;part 1&lt;/a&gt; I built a converter from XPath to iOS class chain; in &lt;a href="https://dev.to/mikhail_yesipchuk/xpath-to-ios-class-chain-can-claude-do-it-instead-of-a-converter-17m9"&gt;part 2&lt;/a&gt; I asked Claude to do the same job. On iOS the best model came within a few points of the converter.&lt;/p&gt;

&lt;p&gt;On Android I did both in one go: I built &lt;a href="https://github.com/domovou69/xpath-to-uiselector" rel="noopener noreferrer"&gt;xpath-to-uiselector&lt;/a&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why leave XPath on Android
&lt;/h2&gt;

&lt;p&gt;With the Appium UiAutomator2 driver, an XPath lookup first serializes the whole UI tree to XML and then runs the query on it. A &lt;code&gt;UiSelector&lt;/code&gt; (the &lt;code&gt;-android uiautomator&lt;/code&gt; strategy) walks the live tree and, for &lt;code&gt;findElement&lt;/code&gt;, stops at the first match. I measured both in a &lt;a href="https://github.com/domovou69/appium-android-locator-benchmark" rel="noopener noreferrer"&gt;separate benchmark&lt;/a&gt;: 30 cases (10 XPath shapes × 3 targets), each found by both strategies, on screens of 100 to 2000 elements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4qjhpjc90il3lucg49u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4qjhpjc90il3lucg49u.png" alt="Time per lookup against elements in the tree, and the deepest element each strategy finds" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UiSelector won all 30 cases at every size.&lt;/strong&gt; The saving in milliseconds grows with the tree (22 ms → 133 ms), the saving in percent shrinks (49 % → 12 %).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The depth limit matters more than the speed.&lt;/strong&gt; The driver's &lt;code&gt;snapshotMaxDepth&lt;/code&gt; is 70: below that, XPath and the page source see nothing. On a chain 100 levels deep, XPath found its last marker at level 66, and &lt;code&gt;UiSelector&lt;/code&gt; found every marker down to level 100.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not 10–50× faster.&lt;/strong&gt; Blog posts often say that, without showing a measurement. I measured 12–49 %: worth having on big screens and in long suites, but not a miracle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What a UiSelector is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;UiSelector&lt;/code&gt; is the androidx UiAutomator query builder. Appium takes it as a Java-like string and evaluates it on the device:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;XPath&lt;/th&gt;
&lt;th&gt;UiSelector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//android.widget.Button&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new UiSelector().className("android.widget.Button")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//*[@text="Pay"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new UiSelector().text("Pay")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//*[@content-desc="Cart"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new UiSelector().description("Cart")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//*[@resource-id="com.app:id/ok"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new UiSelector().resourceId("com.app:id/ok")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//*[@checked="true"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new UiSelector().checked(true)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(//android.widget.EditText)[2]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new UiSelector().className("android.widget.EditText").instance(1)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It has no direct-child or parent step and no position among siblings: &lt;code&gt;childSelector&lt;/code&gt; means any descendant, and &lt;code&gt;fromParent&lt;/code&gt; searches among the siblings.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why converting by hand goes wrong
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;contains(@text,"Total")&lt;/code&gt; → &lt;code&gt;textContains("Total")&lt;/code&gt;
&lt;code&gt;textContains&lt;/code&gt; and &lt;code&gt;textStartsWith&lt;/code&gt; &lt;strong&gt;ignore case&lt;/strong&gt;: this also finds "TOTAL". Exact: &lt;code&gt;textMatches("(?s).*Total.*")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;matches(@resource-id,".*\.ok")&lt;/code&gt; → &lt;code&gt;resourceIdMatches(".*\\.ok")&lt;/code&gt; (in the locator text)
The server's string literal unescapes only &lt;code&gt;\"&lt;/code&gt;, so the regex gets &lt;code&gt;\\.&lt;/code&gt;, a literal backslash. It matches nothing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(//ScrollView//ImageView)[3]&lt;/code&gt; → &lt;code&gt;…childSelector(…ImageView.instance(2))&lt;/code&gt;
Inside &lt;code&gt;childSelector&lt;/code&gt;, &lt;code&gt;instance()&lt;/code&gt; counts per container, not across the screen.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;//*&lt;/code&gt; → &lt;code&gt;classNameMatches(".*")&lt;/code&gt;
With &lt;code&gt;classNameMatches&lt;/code&gt;, &lt;code&gt;findElements&lt;/code&gt; iterates &lt;code&gt;index(i)&lt;/code&gt; and loses elements. Exact: &lt;code&gt;new UiSelector()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;//ListView/TextView&lt;/code&gt; → &lt;code&gt;…childSelector(…)&lt;/code&gt;
That is any descendant. UiSelector has no direct child, so there is no exact form.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ends-with(@text,"USD")&lt;/code&gt; → "skip: XPath 2.0"
The driver runs XPath 2.0 unless &lt;code&gt;enforceXPath1&lt;/code&gt; is set: &lt;code&gt;matches()&lt;/code&gt;, &lt;code&gt;ends-with()&lt;/code&gt;, &lt;code&gt;lower-case()&lt;/code&gt; all work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the converter does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/domovou69/xpath-to-uiselector" rel="noopener noreferrer"&gt;xpath-to-uiselector&lt;/a&gt; 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:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- AppiumBy.xpath("(//android.widget.TextView[@content-desc='price'])[2]")
&lt;/span&gt;&lt;span class="gi"&gt;+ AppiumBy.androidUIAutomator("new UiSelector().className(\"android.widget.TextView\").description(\"price\").instance(1)")
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- (AppiumBy.XPATH, '//*[@content-desc="Cart" and contains(@text, "USD")]')
&lt;/span&gt;&lt;span class="gi"&gt;+ (AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().description("Cart").textMatches("(?s).*USD.*")')
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;[skip] UiSelector has no direct-child criterion: //android.widget.ListView/android.widget.TextView[1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every XPath ends up in one of three groups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact:&lt;/strong&gt; the &lt;code&gt;UiSelector&lt;/code&gt; finds the same elements in the same order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close:&lt;/strong&gt; the same first element, but not always the same list, for example &lt;code&gt;//A//B&lt;/code&gt; → &lt;code&gt;childSelector&lt;/code&gt;. It comes with a warning, and &lt;code&gt;--exact&lt;/code&gt; leaves these alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip:&lt;/strong&gt; there is no exact &lt;code&gt;UiSelector&lt;/code&gt;. Most skips come with a hint on how to rewrite the locator by hand.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The bare command only previews. &lt;code&gt;--write&lt;/code&gt; saves, &lt;code&gt;--json&lt;/code&gt; writes a report with every skip and warning. Node.js 20+, no dependencies. To try a single XPath without installing anything, use the &lt;a href="https://domovou69.github.io/xpath-to-uiselector/" rel="noopener noreferrer"&gt;online converter&lt;/a&gt;: it runs the same code in the browser.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I checked it
&lt;/h2&gt;

&lt;p&gt;The order was facts first, the converter last:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The source code:&lt;/strong&gt; the UiAutomator2 server, androidx &lt;code&gt;UiSelector&lt;/code&gt; and the XPath engine the driver uses, at pinned versions. That is where the case-insensitive &lt;code&gt;Contains&lt;/code&gt; and the &lt;code&gt;\"&lt;/code&gt;-only unescaping came from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;170 edge cases&lt;/strong&gt; (6 of them for &lt;code&gt;--loose&lt;/code&gt;), each with a hand-written expected answer, &lt;strong&gt;ran on an Android emulator before the converter existed&lt;/strong&gt;: XPath and &lt;code&gt;UiSelector&lt;/code&gt; side by side, comparing the elements they find. Where the device disagreed with me, the case was corrected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1645 real XPath locators from 20 open-source Appium projects.&lt;/strong&gt; Every converted one (1169 distinct XPaths) ran on random screens built from its own values and on screens built to break each condition. &lt;strong&gt;0 wrong.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checking the check:&lt;/strong&gt; 1718 deliberately wrong &lt;code&gt;UiSelector&lt;/code&gt;s; the screens caught 99.5 % of them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Android 16 emulator (and Android 17 for the edge cases), Appium 3.8.0, UiAutomator2 driver 8.7.0, default driver settings. The result: &lt;strong&gt;73 % convert (64 % exact, 9 % close)&lt;/strong&gt;. Most of the rest have no exact &lt;code&gt;UiSelector&lt;/code&gt; at all: direct children, absolute &lt;code&gt;/hierarchy/…&lt;/code&gt; paths, sibling positions, web and iOS attributes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Claude 5.5 against the converter
&lt;/h2&gt;

&lt;p&gt;I used the same setup as in the &lt;a href="https://dev.to/mikhail_yesipchuk/xpath-to-ios-class-chain-can-claude-do-it-instead-of-a-converter-17m9"&gt;iOS comparison&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;680 XPaths:&lt;/strong&gt; 149 edge cases and 531 real locators from 19 projects, shuffled, with neutral ids.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One request per run:&lt;/strong&gt; "convert each one to a UiSelector, or skip it with a reason". Claude Code CLI in an empty folder, Read and Write tools only, effort &lt;code&gt;medium&lt;/code&gt;, one run per model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The judge:&lt;/strong&gt; every answer that differs from the converter's ran on the emulator next to the XPath.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scoring:&lt;/strong&gt; a correct &lt;code&gt;UiSelector&lt;/code&gt; earns 1 point, a close one 0.5, a skip 0, and a &lt;strong&gt;wrong one −1&lt;/strong&gt; (on iOS a wrong chain scored 0): it is worse than a skip, because it looks like a valid locator and finds another element. &lt;strong&gt;S&lt;/strong&gt; is the score on the edge cases, &lt;strong&gt;R&lt;/strong&gt; on the real-world locators, each out of 100 over the locators that have a &lt;code&gt;UiSelector&lt;/code&gt; at all: 104 of 149 edge cases and 387 of 531 real ones. The converter scores &lt;strong&gt;S 94.2 / R 96.5&lt;/strong&gt;, not 100, because for some locators it has only a close form, worth half a point. No model beat it on any locator.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fna8zq40z8mzhqz2aff0e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fna8zq40z8mzhqz2aff0e.png" alt="S score against cost for three Claude 5.5 models, with and without the rules prompt, and a table with S, R, wrong answers, tokens, time and cost" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;th&gt;Close&lt;/th&gt;
&lt;th&gt;Wrong&lt;/th&gt;
&lt;th&gt;Skip&lt;/th&gt;
&lt;th&gt;Missed&lt;/th&gt;
&lt;th&gt;S&lt;/th&gt;
&lt;th&gt;R&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Converter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;452&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;189&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;94.2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;&amp;nbsp;0.1&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;179&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;56.7&lt;/td&gt;
&lt;td&gt;80.1&lt;/td&gt;
&lt;td&gt;12.2&lt;/td&gt;
&lt;td&gt;$3.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;176&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;43.8&lt;/td&gt;
&lt;td&gt;80.5&lt;/td&gt;
&lt;td&gt;4.9&lt;/td&gt;
&lt;td&gt;$0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 5.5&lt;/td&gt;
&lt;td&gt;390&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;169&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;37.0&lt;/td&gt;
&lt;td&gt;78.7&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;$0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Missed" means the model skipped a locator that the converter converts correctly. Cost is the API list price the CLI reports.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On real locators the three models are close to each other&lt;/strong&gt; (R 79–80), but 16 points below the converter, and each still wrote 31–32 wrong answers there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases tell the models apart:&lt;/strong&gt; S 37 to 57 against 94.2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7–9 % of all answers were wrong&lt;/strong&gt;, and they look exactly like the correct ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opus thought longest and was still wrong 47 times.&lt;/strong&gt; Most of its 22 missed locators came from assuming that the driver runs XPath 1.0.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  What the models get wrong
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fawnubm3x61rubidta36y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fawnubm3x61rubidta36y.png" alt="170 wrong UiSelectors of the three runs without rules, by kind of mistake" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;/p&gt;
  Mistakes per model
  &lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Opus&lt;/th&gt;
&lt;th&gt;Sonnet&lt;/th&gt;
&lt;th&gt;Haiku&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Case-insensitive &lt;code&gt;textContains&lt;/code&gt; / &lt;code&gt;descriptionContains&lt;/code&gt; / &lt;code&gt;textStartsWith&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;(?s)&lt;/code&gt; in a regex&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java-style &lt;code&gt;\\&lt;/code&gt; in a regex or value&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Values only the page source has (&lt;code&gt;@text=""&lt;/code&gt;, a toast, a range widget's value)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positions: &lt;code&gt;instance()&lt;/code&gt; in &lt;code&gt;childSelector&lt;/code&gt;, &lt;code&gt;//X[1]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;classNameMatches&lt;/code&gt;, &lt;code&gt;(?i)&lt;/code&gt; for &lt;code&gt;translate()&lt;/code&gt;, dropped existence tests, criteria UiSelector does not have&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wrong&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;47&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;63&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each wrong answer is counted once, under the first mistake it shows.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Is textContains really wrong?
&lt;/h3&gt;

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

&lt;p&gt;In a real app it is less clear. Many people who write &lt;code&gt;contains(@text,"Total")&lt;/code&gt; 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, &lt;code&gt;findElement&lt;/code&gt; now returns the header. And a check that the label reads "Total" no longer catches a change to "TOTAL".&lt;/p&gt;

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

&lt;h3&gt;
  
  
  A prompt with rules
&lt;/h3&gt;

&lt;p&gt;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, &lt;a href="https://github.com/domovou69/xpath-to-uiselector/blob/main/audit/llm/rules.md" rel="noopener noreferrer"&gt;&lt;code&gt;rules.md&lt;/code&gt;&lt;/a&gt;, no answers for particular locators) and ran the three models again:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;th&gt;Close&lt;/th&gt;
&lt;th&gt;Wrong&lt;/th&gt;
&lt;th&gt;Skip&lt;/th&gt;
&lt;th&gt;Missed&lt;/th&gt;
&lt;th&gt;S&lt;/th&gt;
&lt;th&gt;R&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Converter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;452&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;189&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;94.2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;&amp;nbsp;0.1&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5 + rules&lt;/td&gt;
&lt;td&gt;448&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;189&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;89.4&lt;/td&gt;
&lt;td&gt;96.3&lt;/td&gt;
&lt;td&gt;5.9&lt;/td&gt;
&lt;td&gt;$1.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5 + rules&lt;/td&gt;
&lt;td&gt;448&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;189&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;89.4&lt;/td&gt;
&lt;td&gt;96.3&lt;/td&gt;
&lt;td&gt;4.1&lt;/td&gt;
&lt;td&gt;$0.89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 5.5 + rules&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;189&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;87.5&lt;/td&gt;
&lt;td&gt;94.7&lt;/td&gt;
&lt;td&gt;6.3&lt;/td&gt;
&lt;td&gt;$0.33&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong answers dropped from 47–63 to 1–4, S rose from 37–57 to 88–89 and R from 79–80 to 95–96.&lt;/strong&gt; The one left for Opus and Sonnet is a line-break case: &lt;code&gt;string-length(@text)=5&lt;/code&gt; → &lt;code&gt;textMatches("(?s).{5}")&lt;/code&gt;, where ICU's &lt;code&gt;.&lt;/code&gt; takes CR LF as one character. Haiku also dropped the &lt;code&gt;className&lt;/code&gt; three times, against an explicit rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opus got cheaper and faster with the rules:&lt;/strong&gt; half the time, 60 % of the cost, a quarter of the thinking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This is an upper bound.&lt;/strong&gt; The rules were written from the mistakes on the same 680 locators, and every run was done once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What the models gave back
&lt;/h3&gt;

&lt;p&gt;The comparison also tested the converter. The first Opus run found &lt;strong&gt;one real converter bug&lt;/strong&gt;: &lt;code&gt;contains(@text,"1.5")&lt;/code&gt; 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 &lt;code&gt;UiSelector&lt;/code&gt; does not. It is now a close match with a warning. Across the six runs, the models also found &lt;strong&gt;8 locators the converter skipped although an exact &lt;code&gt;UiSelector&lt;/code&gt; exists&lt;/strong&gt; (&lt;code&gt;normalize-space()&lt;/code&gt;, &lt;code&gt;string-length()&lt;/code&gt;, &lt;code&gt;translate()&lt;/code&gt;, &lt;code&gt;if … then … else&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android is harder than iOS
&lt;/h2&gt;

&lt;p&gt;On iOS, the same three Claude 5.5 models with a plain request made &lt;strong&gt;4–7 wrong class chains out of 676&lt;/strong&gt;. On Android they made &lt;strong&gt;47–63 out of 680&lt;/strong&gt;, roughly ten times as many. The gap shows on the real locators too: on iOS the models were within 2 points of the converter (R 94–95 against 96.0), on Android 16 points below it (R 79–80 against 96.5). Part of that is the scoring, since a wrong chain scored 0 on iOS and a wrong &lt;code&gt;UiSelector&lt;/code&gt; costs 1 here: at 0, the Android R would be 87–89.&lt;/p&gt;

&lt;p&gt;The reason is the target language, not the models. Both converters are strict in the same way: a converted locator must find the same elements as the XPath. On iOS, the obvious answer is usually also the strict one. A class chain filter is an NSPredicate, and its string operators behave like XPath's: &lt;code&gt;CONTAINS&lt;/code&gt; and &lt;code&gt;BEGINSWITH&lt;/code&gt; are case-sensitive unless you add &lt;code&gt;[c]&lt;/code&gt;. So &lt;code&gt;[contains(@label,"Total")]&lt;/code&gt; becomes &lt;code&gt;[`label CONTAINS "Total"`]&lt;/code&gt;, and that is exact.&lt;/p&gt;

&lt;p&gt;On Android, the obvious answer is often wider than the XPath. &lt;code&gt;UiSelector&lt;/code&gt; has methods with XPath-like names and different behaviour:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;textContains&lt;/code&gt; and &lt;code&gt;textStartsWith&lt;/code&gt; &lt;strong&gt;ignore case&lt;/strong&gt;. This one trap is 65 of the 170 wrong answers, and it has no iOS counterpart.&lt;/li&gt;
&lt;li&gt;The string literal is not Java: the server unescapes only &lt;code&gt;\"&lt;/code&gt;, so a Java-style &lt;code&gt;\\&lt;/code&gt; stays two characters.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;instance()&lt;/code&gt; inside &lt;code&gt;childSelector&lt;/code&gt; counts per container, not across the screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A model that "knows" &lt;code&gt;UiSelector&lt;/code&gt; from Java code samples has seen these methods used, but rarely with the details that make them differ from XPath. So the case-insensitive, &lt;code&gt;--loose&lt;/code&gt;-like answers are an Android problem: on iOS the same habit would produce the exact chain. Even if &lt;code&gt;textContains&lt;/code&gt; counts as fine, the Android models made at least 27–41 wrong answers, about four to ten times the iOS count. The iOS mistakes were mostly of other kinds: raw line breaks inside a chain and positions such as &lt;code&gt;[-1]&lt;/code&gt;.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The future of UiSelector
&lt;/h2&gt;

&lt;p&gt;Google plans to &lt;a href="https://developer.android.com/training/testing/other-components/ui-automator-legacy#ui-automator" rel="noopener noreferrer"&gt;deprecate and remove&lt;/a&gt; &lt;code&gt;UiSelector&lt;/code&gt; from UiAutomator, and the UiAutomator2 driver warns that every &lt;code&gt;-android uiautomator&lt;/code&gt; 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 (&lt;a href="https://github.com/appium/appium-uiautomator2-driver/issues/812" rel="noopener noreferrer"&gt;appium-uiautomator2-driver#812&lt;/a&gt; is open).&lt;/p&gt;

&lt;p&gt;So converting today is a bet that &lt;code&gt;UiSelector&lt;/code&gt; 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 &lt;code&gt;git revert&lt;/code&gt;. The repository has a monthly job that opens an issue when the driver changes a file the conversion rules depend on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The short answer:&lt;/strong&gt; without rules, no Claude model is safe for a bulk migration; with rules, all three get close to the converter. But the rules are what building and device-checking the converter taught me. Without that work, there would be nothing to put in the prompt.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The converter first.&lt;/strong&gt; It is free, offline, takes under a second for 680 locators, gives the same answer every time and made no wrong &lt;code&gt;UiSelector&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A model only for what the converter skips&lt;/strong&gt;, with &lt;a href="https://github.com/domovou69/xpath-to-uiselector/blob/main/audit/llm/rules.md" rel="noopener noreferrer"&gt;the rules&lt;/a&gt; in the prompt. With the rules, Sonnet 5.5 at &lt;code&gt;medium&lt;/code&gt; scored the same as Opus for half the cost. Most skips have no exact &lt;code&gt;UiSelector&lt;/code&gt; at all, so expect an explanation rather than a conversion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never paste a model's &lt;code&gt;UiSelector&lt;/code&gt; into a test without running it&lt;/strong&gt; on the screen it is meant for. Without the rules, about one in twelve was wrong, and none of them looked wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the strictness you want.&lt;/strong&gt; By default the converter writes exact conversions and close ones with a warning (&lt;code&gt;//A//B&lt;/code&gt; → &lt;code&gt;childSelector&lt;/code&gt;); &lt;code&gt;--exact&lt;/code&gt; writes only the exact ones. &lt;code&gt;--loose&lt;/code&gt; adds simpler, case-insensitive forms, also as close with a warning. That is roughly how Claude converts without rules (55 of its 65 &lt;code&gt;textContains&lt;/code&gt; answers are the same as &lt;code&gt;--loose&lt;/code&gt;), except that the converter tells you the locator became wider.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you use Claude Code, the repository has a &lt;a href="https://github.com/domovou69/xpath-to-uiselector/tree/main/.claude/skills/xpath-to-uiselector" rel="noopener noreferrer"&gt;skill&lt;/a&gt; that does exactly that: it runs the converter, then reviews each skipped locator and suggests a rewrite, marked as unverified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Emulator only.&lt;/strong&gt; Android 16 (and 17 for part of the checks), AOSP and Google Play images on an M1 Pro. No real phone, no older Android versions, no OEM builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default driver settings only.&lt;/strong&gt; &lt;code&gt;enforceXPath1&lt;/code&gt;, &lt;code&gt;allowInvisibleElements&lt;/code&gt; and similar settings change the answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed was measured on one synthetic View-based app.&lt;/strong&gt; Compose and WebView screens were not measured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One run per model.&lt;/strong&gt; Two runs with the same rules gave Haiku different mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rules were fitted on the same 680 locators&lt;/strong&gt; they are scored on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only Claude models.&lt;/strong&gt; Other LLMs were not tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Online converter: &lt;a href="https://domovou69.github.io/xpath-to-uiselector/" rel="noopener noreferrer"&gt;domovou69.github.io/xpath-to-uiselector&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npx xpath-to-uiselector ./tests&lt;/code&gt; (&lt;a href="https://www.npmjs.com/package/xpath-to-uiselector" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, &lt;a href="https://github.com/domovou69/xpath-to-uiselector" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The full LLM report: &lt;a href="https://github.com/domovou69/xpath-to-uiselector/blob/main/audit/llm/report.md" rel="noopener noreferrer"&gt;&lt;code&gt;audit/llm/report.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;iOS counterpart: &lt;a href="https://github.com/domovou69/xpath-to-class-chain" rel="noopener noreferrer"&gt;xpath-to-class-chain&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;Thanks to Anthropic: &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; 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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://www.linkedin.com/in/mikhail-yesipchuk/" rel="noopener noreferrer"&gt;Mikhail Yesipchuk&lt;/a&gt;, SDET/Senior QA Automation Engineer. Have you moved Android locators off XPath, and what broke? Tell me on &lt;a href="https://www.linkedin.com/in/mikhail-yesipchuk/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or open an issue on &lt;a href="https://github.com/domovou69/xpath-to-uiselector/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>appium</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>XPath to iOS Class Chain: can Claude 5.5 do it instead of a converter?</title>
      <dc:creator>Mikhail Yesipchuk</dc:creator>
      <pubDate>Fri, 16 Oct 2026 11:30:31 +0000</pubDate>
      <link>https://dev.to/mikhail_yesipchuk/xpath-to-ios-class-chain-can-claude-do-it-instead-of-a-converter-17m9</link>
      <guid>https://dev.to/mikhail_yesipchuk/xpath-to-ios-class-chain-can-claude-do-it-instead-of-a-converter-17m9</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No model beat the converter.&lt;/strong&gt; The best Claude run reached 96.7 % of the converter's usable chains on 676 locators and was ahead of it on none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Without rules, every model run made wrong chains&lt;/strong&gt; (4 to 59 per run, the converter 0). A prompt with the rules brought Opus 5.5 and Sonnet 5.5 to &lt;strong&gt;zero&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the latest model at &lt;code&gt;low&lt;/code&gt; or &lt;code&gt;medium&lt;/code&gt; effort, with the rules.&lt;/strong&gt; &lt;code&gt;max&lt;/code&gt; cost up to 4.6× more than &lt;code&gt;medium&lt;/code&gt; for a few points; older generations were slower and made more mistakes.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The test&lt;/li&gt;
&lt;li&gt;A plain request&lt;/li&gt;
&lt;li&gt;Does a higher effort help?&lt;/li&gt;
&lt;li&gt;Older models&lt;/li&gt;
&lt;li&gt;What the models get wrong&lt;/li&gt;
&lt;li&gt;A prompt with rules&lt;/li&gt;
&lt;li&gt;What I would use&lt;/li&gt;
&lt;li&gt;Limits&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;In &lt;a href="https://dev.to/mikhail_yesipchuk/xpath-to-ios-class-chain-a-converter-you-can-trust-and-when-it-is-actually-faster-c6o"&gt;part 1&lt;/a&gt; I built &lt;a href="https://github.com/domovou69/xpath-to-class-chain" rel="noopener noreferrer"&gt;xpath-to-class-chain&lt;/a&gt;, a converter from Appium XPath locators to iOS class chain, and checked it on 678 locators. The obvious question: why write a converter at all, if you can paste the locators into an LLM?&lt;/p&gt;

&lt;p&gt;So I asked Claude to do the same job and scored the answers with the same checks. This is not me against Claude: Claude Code helped me write the converter too. The real question is whether you should let a model convert every locator directly, or use a tool built once and checked on devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The same 678 locators:&lt;/strong&gt; 563 real ones from production test suites and 115 hand-made edge cases. 676 are compared (one is not an XPath, one duplicates another at runtime).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One request per run:&lt;/strong&gt; the whole list in one file, shuffled, with neutral ids, so the model cannot tell real locators from edge cases. The model writes a chain or a skip with a reason for each id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A clean environment:&lt;/strong&gt; Claude Code CLI in an empty folder, only &lt;code&gt;Read&lt;/code&gt; and &lt;code&gt;Write&lt;/code&gt; tools, no settings, no web, no examples beyond one chain in the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same judge as the converter:&lt;/strong&gt; a WDA emulator built from WebDriverAgent's own code runs each chain and its XPath on hundreds of generated screens and compares what they find.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scoring&lt;/strong&gt;, as in part 1: an exact chain earns 1 point, a documented approximation 0.5, a skip or a wrong chain 0. &lt;strong&gt;R&lt;/strong&gt; is the score on real locators, &lt;strong&gt;S&lt;/strong&gt; on edge cases, both out of 100. The converter scores &lt;strong&gt;R 96.0 / S 93.6&lt;/strong&gt;, which is the maximum for this scoring (some locators only have an approximation). I also count the converter's &lt;strong&gt;579 usable chains as 100 %&lt;/strong&gt; and measure each model against that number.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;wrong chain&lt;/strong&gt; below is one that WDA rejects or that finds other elements. It is worse than a skip, because it looks like a valid locator: a chain that finds another element taps the wrong button, and the Appium driver reports an invalid chain as "no such element", so it looks like a flaky test, not a syntax error.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  The prompt and the run details
  &lt;p&gt;The plain request (full text in &lt;a href="https://github.com/domovou69/xpath-to-class-chain/blob/main/audit/llm/prompt.md" rel="noopener noreferrer"&gt;&lt;code&gt;audit/llm/prompt.md&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;input.json&lt;/code&gt; in the current folder has 678 XPath locators from iOS UI tests (Appium, XCUITest driver). Convert each one to an iOS class chain locator (&lt;code&gt;-ios class chain&lt;/code&gt;) that finds the same elements. If a locator cannot be expressed as a class chain that finds the same elements, skip it and give the reason.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then the output format: one JSON entry per id, either a chain with the &lt;code&gt;-ios class chain:&lt;/code&gt; prefix or &lt;code&gt;skip&lt;/code&gt; with a reason.&lt;/p&gt;

&lt;p&gt;One run per model and effort level. Cost is the API-equivalent figure the CLI reports. I actually ran everything on a Claude Pro plan (€20 a month), so its usage limits apply: that is why &lt;code&gt;xhigh&lt;/code&gt; was skipped and Opus &lt;code&gt;max&lt;/code&gt; could not finish. Time is wall time, with three runs sharing one account in parallel. Scripts and raw outputs: &lt;a href="https://github.com/domovou69/xpath-to-class-chain/tree/main/audit/llm" rel="noopener noreferrer"&gt;&lt;code&gt;audit/llm/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A plain request
&lt;/h2&gt;

&lt;p&gt;The best effort level of each Claude 5.5 model, plain request:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;R&lt;/th&gt;
&lt;th&gt;S&lt;/th&gt;
&lt;th&gt;vs conv.&lt;/th&gt;
&lt;th&gt;Wrong&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Converter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;93.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100 %&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5 &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;95.1&lt;/td&gt;
&lt;td&gt;81.9&lt;/td&gt;
&lt;td&gt;96.7 %&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;7.1&lt;/td&gt;
&lt;td&gt;$2.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5 &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.8&lt;/td&gt;
&lt;td&gt;68.5&lt;/td&gt;
&lt;td&gt;95.9 %&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;$1.03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 5.5 &lt;code&gt;medium&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.1&lt;/td&gt;
&lt;td&gt;63.6&lt;/td&gt;
&lt;td&gt;93.8 %&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;$0.28&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"vs conv." is usable chains against the converter's 579, "Wrong" the wrong chains, "Min" the run time in minutes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On real locators the models are close.&lt;/strong&gt; R 94–95 against 96.0: most real locators are simple (&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;label&lt;/code&gt;, type, &lt;code&gt;contains&lt;/code&gt;), and every model converts them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The gap is in edge cases.&lt;/strong&gt; S 64–82 against 93.6: positions, string functions, nested predicates, escaping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every run made wrong chains.&lt;/strong&gt; Four in 564 sounds like little, until one of them is the locator of your checkout button.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Does a higher effort help?
&lt;/h2&gt;

&lt;p&gt;Claude Code has effort levels from &lt;code&gt;low&lt;/code&gt; to &lt;code&gt;max&lt;/code&gt;; a higher one lets the model think longer. I ran each 5.5 model at &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; (Opus &lt;code&gt;max&lt;/code&gt; did not finish within the usage limit).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr5yzqm6lw2wnuxgwu0zr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr5yzqm6lw2wnuxgwu0zr.png" alt="S against cost per run, every effort level of the three 5.5 models, plain request" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Opus:&lt;/strong&gt; &lt;code&gt;low&lt;/code&gt; and &lt;code&gt;medium&lt;/code&gt; gave identical answers; &lt;code&gt;high&lt;/code&gt; added 1 point of S for 39 % more cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sonnet:&lt;/strong&gt; the one real step is &lt;code&gt;low&lt;/code&gt; → &lt;code&gt;medium&lt;/code&gt; (S 68.5 → 77.6, same cost). &lt;code&gt;max&lt;/code&gt; added 3.6 more for 4.6× the cost and 36 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Haiku:&lt;/strong&gt; best at &lt;code&gt;medium&lt;/code&gt;. At &lt;code&gt;low&lt;/code&gt; it used zero-based indexes (&lt;code&gt;[0]&lt;/code&gt;) and made 39 wrong chains; at &lt;code&gt;max&lt;/code&gt; it skipped a quarter of the list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At &lt;code&gt;max&lt;/code&gt;, 79–89 % of the output tokens were thinking that did not improve the answer. &lt;strong&gt;&lt;code&gt;medium&lt;/code&gt; is enough; for Opus, &lt;code&gt;low&lt;/code&gt; is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Every effort level
  &lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model and effort&lt;/th&gt;
&lt;th&gt;R&lt;/th&gt;
&lt;th&gt;S&lt;/th&gt;
&lt;th&gt;Wrong chains&lt;/th&gt;
&lt;th&gt;Time, min&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Haiku &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;91.3&lt;/td&gt;
&lt;td&gt;51.3&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;td&gt;$0.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku &lt;code&gt;medium&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.1&lt;/td&gt;
&lt;td&gt;63.6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;$0.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku &lt;code&gt;high&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.5&lt;/td&gt;
&lt;td&gt;61.5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;7.6&lt;/td&gt;
&lt;td&gt;$0.36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku &lt;code&gt;max&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;91.6&lt;/td&gt;
&lt;td&gt;57.1&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;13.5&lt;/td&gt;
&lt;td&gt;$0.64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.8&lt;/td&gt;
&lt;td&gt;68.5&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;$1.03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet &lt;code&gt;medium&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.1&lt;/td&gt;
&lt;td&gt;77.6&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5.3&lt;/td&gt;
&lt;td&gt;$1.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet &lt;code&gt;high&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.2&lt;/td&gt;
&lt;td&gt;77.9&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;$1.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet &lt;code&gt;max&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;95.1&lt;/td&gt;
&lt;td&gt;81.2&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;36.0&lt;/td&gt;
&lt;td&gt;$4.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;95.1&lt;/td&gt;
&lt;td&gt;81.9&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;7.1&lt;/td&gt;
&lt;td&gt;$2.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus &lt;code&gt;medium&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;95.1&lt;/td&gt;
&lt;td&gt;81.9&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;td&gt;$2.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus &lt;code&gt;high&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;95.1&lt;/td&gt;
&lt;td&gt;82.9&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;10.7&lt;/td&gt;
&lt;td&gt;$2.83&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;xhigh&lt;/code&gt; was not run. Opus &lt;code&gt;max&lt;/code&gt; hit the Pro plan's 5-hour usage limit twice (81 minutes and about $13 in total) without a usable result.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Older models
&lt;/h2&gt;

&lt;p&gt;The same plain request to the previous generations, one run each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;R&lt;/th&gt;
&lt;th&gt;S&lt;/th&gt;
&lt;th&gt;vs conv.&lt;/th&gt;
&lt;th&gt;Wrong&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Converter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;96.0&lt;/td&gt;
&lt;td&gt;93.6&lt;/td&gt;
&lt;td&gt;100 %&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5 &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;95.1&lt;/td&gt;
&lt;td&gt;81.9&lt;/td&gt;
&lt;td&gt;96.7 %&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;7.1&lt;/td&gt;
&lt;td&gt;$2.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5 &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.5&lt;/td&gt;
&lt;td&gt;62.5&lt;/td&gt;
&lt;td&gt;94.5 %&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;11.6&lt;/td&gt;
&lt;td&gt;$3.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 4.6 &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;93.9&lt;/td&gt;
&lt;td&gt;51.5&lt;/td&gt;
&lt;td&gt;91.7 %&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;47.1&lt;/td&gt;
&lt;td&gt;$5.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5 &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.8&lt;/td&gt;
&lt;td&gt;68.5&lt;/td&gt;
&lt;td&gt;95.9 %&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;$1.03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5 &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;95.2&lt;/td&gt;
&lt;td&gt;56.5&lt;/td&gt;
&lt;td&gt;94.5 %&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;21.8&lt;/td&gt;
&lt;td&gt;$2.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 4.6 &lt;code&gt;low&lt;/code&gt; ¹&lt;/td&gt;
&lt;td&gt;89.1&lt;/td&gt;
&lt;td&gt;48.9&lt;/td&gt;
&lt;td&gt;86.5 %&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;23.1&lt;/td&gt;
&lt;td&gt;$2.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 5.5 &lt;code&gt;medium&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.1&lt;/td&gt;
&lt;td&gt;63.6&lt;/td&gt;
&lt;td&gt;93.8 %&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;$0.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 4.5 &lt;code&gt;medium&lt;/code&gt; ²&lt;/td&gt;
&lt;td&gt;59.6&lt;/td&gt;
&lt;td&gt;18.1&lt;/td&gt;
&lt;td&gt;53.7 %&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;$0.61&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;¹ In seven runs of 100 locators: the full list wrote nothing in 20 minutes.&lt;br&gt;
² In all three runs it wrote a Python converter instead of converting; this is the only run that ran its script and returned every answer.&lt;/p&gt;

&lt;p&gt;Each new generation scored higher on edge cases, made fewer wrong chains, and was faster and cheaper. Opus 4.6 took 47 minutes and $5.75 to do worse than Haiku 5.5 did in 6 minutes for $0.28.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdptidjp638dgrhpd6isu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdptidjp638dgrhpd6isu.png" alt="Quality against cost per run: plain request and rules prompt, three generations" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What the models get wrong
&lt;/h2&gt;

&lt;p&gt;The wrong chains are not random. The same five mistakes come back from run to run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Why it is wrong&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A line break inside a literal written as a raw line break&lt;/td&gt;
&lt;td&gt;WDA cannot parse the chain&lt;/td&gt;
&lt;td&gt;10 of 11 runs, every Sonnet and Opus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;(//Button)[last()]&lt;/code&gt; → &lt;code&gt;**/XCUIElementTypeButton[-1]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;WDA counts chain matches breadth-first, XPath top to bottom: another element&lt;/td&gt;
&lt;td&gt;3–5 per run, more in stronger models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[@label!="a"]&lt;/code&gt; → &lt;code&gt;[`label != "a"`]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;an element without a label passes in NSPredicate and fails in XPath&lt;/td&gt;
&lt;td&gt;Haiku, Sonnet &lt;code&gt;low&lt;/code&gt;–&lt;code&gt;high&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[.//*[@name="x"]]&lt;/code&gt; → &lt;code&gt;[$name == "x"$]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$…$&lt;/code&gt; also matches the element itself, not only its descendants&lt;/td&gt;
&lt;td&gt;Sonnet &lt;code&gt;medium&lt;/code&gt;–&lt;code&gt;max&lt;/code&gt;, every Opus run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[1]&lt;/code&gt; → &lt;code&gt;[0]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;class chain indexes start at 1&lt;/td&gt;
&lt;td&gt;Haiku &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The models also skip things they could convert: with the plain request, Opus at every level refused &lt;code&gt;matches()&lt;/code&gt;, &lt;code&gt;ends-with()&lt;/code&gt; and &lt;code&gt;lower-case()&lt;/code&gt; as "XPath 2.0 functions". WDA registers them as XPath extensions, so they are valid locators on the driver.&lt;/p&gt;

&lt;p&gt;The other direction exists too. In the first runs, some model found an exact chain for 30 locators that the converter at that time skipped or only approximated (unions as one &lt;code&gt;OR&lt;/code&gt; predicate, &lt;code&gt;not()&lt;/code&gt;, quotes and backticks inside literals, string functions). I added those forms to the converter after checking them with the emulator and, where they depend on how XCTest evaluates a predicate, on a simulator and an iPhone. So the LLM runs made the converter better, and after that no model was ahead of it on any locator.&lt;/p&gt;
&lt;h2&gt;
  
  
  A prompt with rules
&lt;/h2&gt;

&lt;p&gt;Most of the mistakes above are about knowing WDA, not about intelligence. So I wrote them down as general rules (structure, escaping, attributes, positions, what to skip) and ran the same three 5.5 runs again with them. The rules: &lt;a href="https://github.com/domovou69/xpath-to-class-chain/blob/main/audit/llm/prompt-v4.md" rel="noopener noreferrer"&gt;&lt;code&gt;audit/llm/prompt-v4.md&lt;/code&gt;&lt;/a&gt;, about 90 lines; there are no answers for particular locators in it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;R&lt;/th&gt;
&lt;th&gt;S&lt;/th&gt;
&lt;th&gt;vs conv.&lt;/th&gt;
&lt;th&gt;Wrong&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Converter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;96.0&lt;/td&gt;
&lt;td&gt;93.6&lt;/td&gt;
&lt;td&gt;100 %&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5 &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;95.1&lt;/td&gt;
&lt;td&gt;81.9&lt;/td&gt;
&lt;td&gt;96.7 %&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;7.1&lt;/td&gt;
&lt;td&gt;$2.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ rules&lt;/td&gt;
&lt;td&gt;95.0&lt;/td&gt;
&lt;td&gt;86.9 (+5.0)&lt;/td&gt;
&lt;td&gt;96.7 %&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt; (−4)&lt;/td&gt;
&lt;td&gt;5.1&lt;/td&gt;
&lt;td&gt;$1.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5 &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.8&lt;/td&gt;
&lt;td&gt;68.5&lt;/td&gt;
&lt;td&gt;95.9 %&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;$1.03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ rules&lt;/td&gt;
&lt;td&gt;95.0&lt;/td&gt;
&lt;td&gt;84.7 (+16.2)&lt;/td&gt;
&lt;td&gt;96.4 %&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt; (−7)&lt;/td&gt;
&lt;td&gt;8.9&lt;/td&gt;
&lt;td&gt;$1.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 5.5 &lt;code&gt;medium&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;94.1&lt;/td&gt;
&lt;td&gt;63.6&lt;/td&gt;
&lt;td&gt;93.8 %&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;$0.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ rules&lt;/td&gt;
&lt;td&gt;94.8&lt;/td&gt;
&lt;td&gt;72.0 (+8.4)&lt;/td&gt;
&lt;td&gt;95.7 %&lt;/td&gt;
&lt;td&gt;5 (+1)&lt;/td&gt;
&lt;td&gt;5.7&lt;/td&gt;
&lt;td&gt;$0.27&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Upper value: the plain request; lower: the same model with the rules, and the change in brackets. Column names as in the first table.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;R barely moved.&lt;/strong&gt; On real locators the models were already as good as they get.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S went up&lt;/strong&gt; by 5 (Opus), 16 (Sonnet) and 8 (Haiku), and the three models came closer to each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why Opus stays at 96.7 % of the converter.&lt;/strong&gt; The rules fixed 12 locators: 3 of its 4 wrong chains and 9 skips of things WDA supports (&lt;code&gt;matches()&lt;/code&gt;, &lt;code&gt;ends-with()&lt;/code&gt;, &lt;code&gt;lower-case()&lt;/code&gt;, &lt;code&gt;@x&lt;/code&gt;, &lt;code&gt;@index&lt;/code&gt;, &lt;code&gt;XCUIElementTypeAny&lt;/code&gt;). But the model also got more careful and skipped 12 others: 7 have no exact chain, so skipping them is right, and 5 the converter does convert (&lt;code&gt;self::&lt;/code&gt;, a union of two types, a partial type name, an absolute path with indexes, an index inside parentheses). 12 in, 12 out; the 4th wrong chain became a correct skip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opus and Sonnet made no wrong chains.&lt;/strong&gt; Haiku still made two kinds of mistake (one of them four times), so it needs a check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still no model is ahead of the converter&lt;/strong&gt; on any locator. With one run per model, differences of about 3 points in S are noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;/p&gt;
  How the rules prompt was built
  &lt;p&gt;It took three versions, each changing one rule. The first version had no wrong chains but skipped 76 convertible real locators because of one over-cautious rule about &lt;code&gt;@type&lt;/code&gt;; narrowing that rule fixed it. The third fixed &lt;code&gt;contains(@enabled,"false")&lt;/code&gt;, which has to become &lt;code&gt;enabled == false&lt;/code&gt; (&lt;code&gt;CONTAINS&lt;/code&gt; on a boolean raises an error on the device). The rules come from what the converter's own checks found (the emulator, a simulator and an iPhone), not from guesses.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would use
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The converter first.&lt;/strong&gt; It is free, offline, takes seconds, gives the same answer every time and made no wrong chain. Nothing a model did was better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A model only for what the converter skips&lt;/strong&gt;, and only the latest generation: Opus 5.5 or Sonnet 5.5 at &lt;code&gt;low&lt;/code&gt; or &lt;code&gt;medium&lt;/code&gt;, with &lt;a href="https://github.com/domovou69/xpath-to-class-chain/blob/main/audit/llm/prompt-v4.md" rel="noopener noreferrer"&gt;the rules&lt;/a&gt; in the prompt. Haiku 5.5 is six times cheaper, but even with the rules it made wrong chains. Most skips have no exact chain at all, so expect the model to explain why rather than to convert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check every chain a model writes&lt;/strong&gt; on its screen (or with the &lt;a href="https://github.com/domovou69/xpath-to-class-chain/tree/main/audit/harness" rel="noopener noreferrer"&gt;harness&lt;/a&gt;) before it replaces the XPath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not pay for &lt;code&gt;max&lt;/code&gt;.&lt;/strong&gt; It costs several times more, takes up to six times longer and gave at most a few points of S.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you use Claude Code, the repository has a &lt;a href="https://github.com/domovou69/xpath-to-class-chain/tree/main/.claude/skills/xpath-to-class-chain" rel="noopener noreferrer"&gt;skill&lt;/a&gt; that does exactly this: it runs the converter, then explains each skipped locator and suggests a chain only where the rules give one, marked as unverified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One run per model and effort level. Run-to-run variation was measured only in passing: about 3 points of S.&lt;/li&gt;
&lt;li&gt;Only Claude models. Other LLMs were not tested.&lt;/li&gt;
&lt;li&gt;The judge is the WDA emulator. The converter's chains were checked on a simulator and an iPhone; the forms only the models wrote (&lt;code&gt;MATCHES&lt;/code&gt;, &lt;code&gt;[c]&lt;/code&gt;, &lt;code&gt;$…$&lt;/code&gt; with &lt;code&gt;ancestor::&lt;/code&gt;) mostly were not.&lt;/li&gt;
&lt;li&gt;The edge cases are deliberately unusual. On a typical test suite the gap between a model and the converter is the R column: about 1 point.&lt;/li&gt;
&lt;li&gt;The whole list went in one request. Smaller batches may behave differently (Sonnet 4.6 only worked that way).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;Thanks to Anthropic: &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; helped me build the test setup and analyze the results, and the Claude models are also the subject of this test. All numbers come from real runs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://www.linkedin.com/in/mikhail-yesipchuk/" rel="noopener noreferrer"&gt;Mikhail Yesipchuk&lt;/a&gt;, SDET/Senior QA Automation Engineer. Which model do you use for test code, and where does it go wrong? Tell me on &lt;a href="https://www.linkedin.com/in/mikhail-yesipchuk/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or open an issue on &lt;a href="https://github.com/domovou69/xpath-to-class-chain/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>appium</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>XPath to iOS Class Chain: a converter you can trust, and when it is actually faster</title>
      <dc:creator>Mikhail Yesipchuk</dc:creator>
      <pubDate>Thu, 15 Oct 2026 16:29:42 +0000</pubDate>
      <link>https://dev.to/mikhail_yesipchuk/xpath-to-ios-class-chain-a-converter-you-can-trust-and-when-it-is-actually-faster-c6o</link>
      <guid>https://dev.to/mikhail_yesipchuk/xpath-to-ios-class-chain-a-converter-you-can-trust-and-when-it-is-actually-faster-c6o</guid>
      <description>&lt;p&gt;Converted 678 Appium XPath locators to iOS Class Chain, checked every result, and timed the two on a simulator and an iPhone. Here is what changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Class chain is ~25 % faster than XPath&lt;/strong&gt; for a typical iOS lookup: 70 ms saved at 100 elements, 676 ms at 2000 (iPhone).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It only matters on big screens.&lt;/strong&gt; On small ones the gain is milliseconds, and on a real project the whole test run was not faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~87 % of real XPath locators can be converted.&lt;/strong&gt; The converter does it with &lt;strong&gt;no wrong chains&lt;/strong&gt; and skips or flags the rest.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx xpath-to-class-chain ./tests          &lt;span class="c"&gt;# preview the diff&lt;/span&gt;
npx xpath-to-class-chain ./tests &lt;span class="nt"&gt;--write&lt;/span&gt;  &lt;span class="c"&gt;# apply it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What a class chain is&lt;/li&gt;
&lt;li&gt;Why converting by hand goes wrong&lt;/li&gt;
&lt;li&gt;What the tool does&lt;/li&gt;
&lt;li&gt;How I checked that it is right&lt;/li&gt;
&lt;li&gt;How much faster is it?&lt;/li&gt;
&lt;li&gt;Where the chain is slower&lt;/li&gt;
&lt;li&gt;What I took away&lt;/li&gt;
&lt;li&gt;Limits&lt;/li&gt;
&lt;li&gt;Try it&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;If you run Appium tests on iOS, you have probably heard that XPath is slow and that &lt;code&gt;-ios class chain&lt;/code&gt; is faster. I wanted two things I could not find anywhere:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A tool that converts existing XPath locators to class chain &lt;strong&gt;without silently changing which element they find&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A real number: how much faster is it, and does that depend on the size of the screen?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post covers both. The tool is open source: &lt;a href="https://github.com/domovou69/xpath-to-class-chain" rel="noopener noreferrer"&gt;xpath-to-class-chain&lt;/a&gt; on GitHub and &lt;a href="https://www.npmjs.com/package/xpath-to-class-chain" rel="noopener noreferrer"&gt;npm&lt;/a&gt;. In short: the chain was faster in &lt;strong&gt;all 240&lt;/strong&gt; timed lookups (22–29 %, simulator and iPhone), &lt;strong&gt;489 of 563&lt;/strong&gt; real locators have a class chain, and a few shapes (&lt;code&gt;visible&lt;/code&gt;, lookups of dozens of elements) get &lt;strong&gt;slower&lt;/strong&gt;, see below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a class chain is
&lt;/h2&gt;

&lt;p&gt;A class chain is WebDriverAgent's (WDA's) own locator language for iOS. Like XPath, it is a path of element types. Unlike XPath, each filter is an &lt;a href="https://developer.apple.com/documentation/foundation/nspredicate" rel="noopener noreferrer"&gt;NSPredicate&lt;/a&gt; in backticks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;XPath&lt;/th&gt;
&lt;th&gt;Class chain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;//XCUIElementTypeButton&lt;/code&gt; (any depth)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;**/XCUIElementTypeButton&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;//XCUIElementTypeCell/XCUIElementTypeButton&lt;/code&gt; (direct child)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;**/XCUIElementTypeCell/XCUIElementTypeButton&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[@name="Pay"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[`name == "Pay"`]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[contains(@label,"Total")]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[`label CONTAINS "Total"`]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[@name="a" and @label="b"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[`name == "a" AND label == "b"`]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;//*&lt;/code&gt; (any type)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;**/*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a test, it is just another locator strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WebdriverIO&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-ios class chain:**/XCUIElementTypeButton[`name == "Pay"`]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Java client&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppiumBy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;iOSClassChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"**/XCUIElementTypeButton[`name == \"Pay\"`]"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;click&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it is faster.&lt;/strong&gt; For XPath, WDA takes a snapshot of the whole accessibility tree, writes it out as XML, runs the XPath over it with libxml2 and maps the matches back to elements. A class chain skips the XML step: WDA turns it straight into XCTest queries. Both still walk the whole tree, so the gain is a percentage, not a fixed number of milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why converting by hand goes wrong
&lt;/h2&gt;

&lt;p&gt;The two languages look alike but behave differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Positions.&lt;/strong&gt; XPath &lt;code&gt;//Button[2]&lt;/code&gt; is the 2nd Button under &lt;em&gt;each&lt;/em&gt; parent; class chain &lt;code&gt;**/Button[2]&lt;/code&gt; is the 2nd of &lt;em&gt;all&lt;/em&gt; matches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order.&lt;/strong&gt; XPath &lt;code&gt;(//Button)[2]&lt;/code&gt; counts matches top to bottom; WDA counts them level by level (breadth-first). No class chain counts like XPath here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text.&lt;/strong&gt; &lt;code&gt;==&lt;/code&gt; compares exactly, but &lt;code&gt;CONTAINS&lt;/code&gt;, &lt;code&gt;MATCHES&lt;/code&gt; and &lt;code&gt;[c]&lt;/code&gt; compare normalized text (a fullwidth &lt;code&gt;Ｑ&lt;/code&gt; equals &lt;code&gt;Q&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors.&lt;/strong&gt; The Appium driver turns an invalid chain into "no such element", so a typo looks like a flaky test, not a syntax error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the reason for the whole project: a wrong chain does not fail loudly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool does
&lt;/h2&gt;

&lt;p&gt;It scans a folder (JS/TS, Java, Kotlin, Python, Ruby, C#, PHP, JSON…), converts the XPath locators it finds and prints a diff; &lt;code&gt;--write&lt;/code&gt; saves the files. It validates every chain, &lt;strong&gt;skips&lt;/strong&gt; what has no exact class chain (with a reason), &lt;strong&gt;warns&lt;/strong&gt; on approximations, and leaves alone strings passed to an explicit XPath strategy like &lt;code&gt;AppiumBy.xpath("…")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A preview on four shapes that are common in real test code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- //XCUIElementTypeStaticText[@name="Order total"]
+ -ios class chain:**/XCUIElementTypeStaticText[`name == "Order total"`]

- //*[contains(@type,"XCUIElementTypeButton") and contains(@label,"Add to cart")]
+ -ios class chain:**/XCUIElementTypeButton[`label CONTAINS "Add to cart"`]

- (//XCUIElementTypeButton[@name="product.addToCart"])[2]
+ -ios class chain:**/XCUIElementTypeButton[`name == "product.addToCart"`][2]
! grouped position: WDA counts class chain matches breadth-first, XPath in document order,
  so the element can differ when matches sit at different depths

[skip] skipped_unsupported_logic: //*[@name="Total:"]/following-sibling::*[1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two are exact, the third is the closest chain there is (check it on its screen), and the fourth stays XPath: a class chain has no sibling axis.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I checked that it is right
&lt;/h2&gt;

&lt;p&gt;Every verdict comes from running the XPath and the chain and comparing what they find:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A WDA emulator.&lt;/strong&gt; WDA's own class chain and XPath code (v16.14.1), compiled to run on a Mac. It tests each locator on hundreds of generated screens in seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;678 locators:&lt;/strong&gt; &lt;a href="https://github.com/domovou69/xpath-to-class-chain/blob/main/audit/corpus/ios-locators.json" rel="noopener noreferrer"&gt;563 real ones&lt;/a&gt; from production test suites (anonymized) and &lt;a href="https://github.com/domovou69/xpath-to-class-chain/blob/main/audit/harness/speccases.js" rel="noopener noreferrer"&gt;115 hand-made edge cases&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real devices.&lt;/strong&gt; 246 cases on an iOS 27 simulator and an iPhone 13 Pro Max under Appium 3.8.0 and WDA 16.14.1. Where a device disagreed with the emulator, the device won. That is how the breadth-first order was found.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Scoring:&lt;/strong&gt; an exact chain earns 1 point, an approximation 0.5, a skipped or wrong one 0. Locators that have no class chain at all are not counted; skipping them is the right answer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;R, real locators&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;96.0&lt;/strong&gt; of 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S, edge cases&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;93.6&lt;/strong&gt; of 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong chains&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why not 100?&lt;/strong&gt; 39 real locators, all positions like &lt;code&gt;(//Button)[2]&lt;/code&gt;, have only an approximation, worth 0.5. So 96.0 and 93.6 are the maximum this scoring allows, and the converter reaches it.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  How R and S are calculated
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;R = 100 × Σ points / N    N = 489 real locators that have a class chain
  = 100 × (450 + 39 × 0.5) / 489 = 96.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;S is the same score on the 83 edge cases that have a class chain, split into 9 groups (positions, string functions, escaping, logic…) and weighted by how much each group matters in real test code. Every case, group and weight is in &lt;a href="https://github.com/domovou69/xpath-to-class-chain/blob/main/audit/harness/speccases.js" rel="noopener noreferrer"&gt;&lt;code&gt;speccases.js&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Each locator with its chain and verdict is in &lt;a href="https://github.com/domovou69/xpath-to-class-chain/blob/main/audit/coverage-cases.json" rel="noopener noreferrer"&gt;&lt;code&gt;coverage-cases.json&lt;/code&gt;&lt;/a&gt;; the full method is in &lt;a href="https://github.com/domovou69/xpath-to-class-chain/blob/main/audit/coverage-report.md" rel="noopener noreferrer"&gt;the report&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much faster is it?
&lt;/h2&gt;

&lt;p&gt;On small screens (36–275 elements) the chain was about 20 % faster, but that is only 30–65 ms. So I measured what happens as the screen grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup:&lt;/strong&gt; a synthetic Checkout page grown to 100, 500, 1000 and 2000 elements; the 10 most common locator shapes from real tests; targets at the top, at depth 39 and at the end of the page. Each lookup is one cold &lt;code&gt;findElement&lt;/code&gt;, median of three passes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F23euof5noon2gud7c79q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F23euof5noon2gud7c79q.png" alt="Time per element lookup vs. elements in the tree, XPath and class chain, simulator and iPhone" width="799" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Elements&lt;/th&gt;
&lt;th&gt;Simulator, ms&lt;/th&gt;
&lt;th&gt;Saved&lt;/th&gt;
&lt;th&gt;iPhone, ms&lt;/th&gt;
&lt;th&gt;Saved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;~100&lt;/td&gt;
&lt;td&gt;115 → 90&lt;/td&gt;
&lt;td&gt;21.9 %&lt;/td&gt;
&lt;td&gt;295 → 225&lt;/td&gt;
&lt;td&gt;23.8 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~500&lt;/td&gt;
&lt;td&gt;453 → 344&lt;/td&gt;
&lt;td&gt;24.1 %&lt;/td&gt;
&lt;td&gt;760 → 582&lt;/td&gt;
&lt;td&gt;23.4 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~1000&lt;/td&gt;
&lt;td&gt;912 → 677&lt;/td&gt;
&lt;td&gt;25.8 %&lt;/td&gt;
&lt;td&gt;1400 → 997&lt;/td&gt;
&lt;td&gt;28.9 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~2000&lt;/td&gt;
&lt;td&gt;1750 → 1310&lt;/td&gt;
&lt;td&gt;25.4 %&lt;/td&gt;
&lt;td&gt;2720 → 2040&lt;/td&gt;
&lt;td&gt;24.9 %&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each cell: XPath → class chain, time per lookup.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The saving in percent does not depend on the screen size.&lt;/strong&gt; The saving in milliseconds grows with it, because both strategies process the whole tree on every lookup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The locator shape and the target's position do not matter.&lt;/strong&gt; A button at the top costs as much as one at depth 39.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;/p&gt;
  More findings: useFirstMatch, depth limit, simulator vs iPhone
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;[1]&lt;/code&gt; and &lt;code&gt;useFirstMatch&lt;/code&gt; do not help.&lt;/strong&gt; An explicit &lt;code&gt;[1]&lt;/code&gt; takes the same code path in WDA 16.14.1. &lt;code&gt;useFirstMatch&lt;/code&gt; (an Appium setting that makes WDA stop at the first match) was 2.7 % faster at 100 elements and 5.7 % slower at 2000, 15 % slower for deep targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing deeper than 50 levels is found&lt;/strong&gt; by either strategy (WDA's default &lt;code&gt;snapshotMaxDepth&lt;/code&gt;). The error is a plain "no such element".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real iPhone is 1.5–2.6× slower than the simulator&lt;/strong&gt;, mostly a fixed per-request cost over USB. The ratio between XPath and chain stays the same.&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the chain is slower
&lt;/h2&gt;

&lt;p&gt;On the small screens, four shapes were slower as a chain (simulator; the iPhone showed the same):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Locator&lt;/th&gt;
&lt;th&gt;XPath&lt;/th&gt;
&lt;th&gt;Chain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;//XCUIElementTypeButton[@visible='true']&lt;/code&gt;, 32 matches&lt;/td&gt;
&lt;td&gt;2.05 s&lt;/td&gt;
&lt;td&gt;12.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//XCUIElementTypeCell[@visible="true"]/XCUIElementTypeButton&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3.46 s&lt;/td&gt;
&lt;td&gt;5.14 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;//XCUIElementTypeButton[@name]&lt;/code&gt;, 36 matches&lt;/td&gt;
&lt;td&gt;1.15 s&lt;/td&gt;
&lt;td&gt;1.78 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/XCUIElementTypeApplication/XCUIElementTypeWindow/XCUIElementTypeButton&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.12 s&lt;/td&gt;
&lt;td&gt;0.75 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;visible&lt;/code&gt; is the main trap: WDA checks visibility for every candidate. These four cases outweighed all the gains, and over the whole small-screen run XPath was faster in total (53.7 s against 64.8 s). Without them, the chain was 3.8 % faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So on small screens XPath is fine.&lt;/strong&gt; The gain shows up on big trees: long feeds and lists, screens kept alive in a navigation stack or in tabs, off-screen drawers. An iPad with several windows on screen should behave the same way (not measured).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;Faster lookups do not automatically mean faster tests. On a real project with simple screens, run on Sauce Labs devices (iOS 16, 18, 26), the timed lookups got faster, but the total run time barely changed: a few milliseconds are lost next to app launch, network and waits.&lt;/p&gt;

&lt;p&gt;My rule of thumb: &lt;strong&gt;convert for speed where the screens are big, and convert lookups of one element by name, label, value or type. Check &lt;code&gt;visible&lt;/code&gt; predicates, list lookups and absolute paths first.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only lookups were measured in a controlled way, not whole tests. With implicit waits or retries, a missed lookup repeats the whole search, so the saving multiplies by the number of attempts.&lt;/li&gt;
&lt;li&gt;The big screen is synthetic; no production app was benchmarked.&lt;/li&gt;
&lt;li&gt;One WDA version (16.14.1). Its class chain lookup changed recently (appium/WebDriverAgent PR #1194), so other versions may give other numbers.&lt;/li&gt;
&lt;li&gt;One phone and one Mac (M1 Pro). Absolute times will differ on yours; the ratio should not.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;It is on &lt;a href="https://www.npmjs.com/package/xpath-to-class-chain" rel="noopener noreferrer"&gt;npm&lt;/a&gt;; you need Node.js 20 or newer, nothing else. Start with the screens that have the most elements: dump the page source of your heaviest screen and count them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx xpath-to-class-chain ./tests          &lt;span class="c"&gt;# preview&lt;/span&gt;
npx xpath-to-class-chain ./tests &lt;span class="nt"&gt;--write&lt;/span&gt;  &lt;span class="c"&gt;# save&lt;/span&gt;
npx xpath-to-class-chain ./tests &lt;span class="nt"&gt;--json&lt;/span&gt;   &lt;span class="c"&gt;# report with every skip and warning&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then review the warnings, run your suite, and check each grouped position and &lt;code&gt;visible&lt;/code&gt; locator on its screen. Everything behind the numbers in this post (the audit, the emulator, the device app) is in &lt;a href="https://github.com/domovou69/xpath-to-class-chain/tree/main/audit" rel="noopener noreferrer"&gt;&lt;code&gt;audit/&lt;/code&gt;&lt;/a&gt;; its README says where to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next: can an LLM do this instead?
&lt;/h2&gt;

&lt;p&gt;In part 2 I give the same 678 locators to Claude models (Opus, Sonnet and Haiku, several generations, every effort level) and score them with the same checks. Short version: no model beat the converter on real locators, and a prompt with the rules mattered more than the model or the effort level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;Thanks to Anthropic: &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; helped me build the converter, the test harness and the device benchmark, and analyze the results. All numbers come from real runs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://www.linkedin.com/in/mikhail-yesipchuk/" rel="noopener noreferrer"&gt;Mikhail Yesipchuk&lt;/a&gt;, SDET/Senior QA Automation Engineer. If you try the converter on your suite, I'd like to hear what it skipped and why: open an issue on &lt;a href="https://github.com/domovou69/xpath-to-class-chain/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or message me on &lt;a href="https://www.linkedin.com/in/mikhail-yesipchuk/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>appium</category>
      <category>testing</category>
      <category>xcuitest</category>
    </item>
  </channel>
</rss>
