<?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 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>
