{"title":"Debug Appium Selector Failures That Differ on Android and iOS","excerpt":"A selector that taps the wrong row on Android can throw NoSuchElement on iOS for the same product list. This guide dumps the page source from the failing step on both platforms and separates four causes: rows a LazyColumn or lazy SwiftUI stack never created, duplicate matches, identifiers that never reach the accessibility tree (including the UiAutomator2 id prefix and the iOS label fallback), and stale handles after scrolling. Each gets a discriminating check and a WebdriverIO fix built on Compose testTag and SwiftUI accessibilityIdentifier.","canonicalUrl":"https://automationtester.in/blog/automation-tutorials/debug-appium-selectors-android-ios","category":{"name":"Automation Tutorials","slug":"automation-tutorials"},"tags":["appium","mobile-testing","android","ios","locators","webdriverio","jetpack-compose"],"author":{"name":"Shashank Rawlani","url":"https://shashank.rawlani.com","linkedin":"https://www.linkedin.com/in/shashankrawlani/"},"datePublished":"2026-10-14T13:30:00.000Z","dateModified":"2026-10-14T11:09:52.936Z","coverImage":{"url":"https://ik.imagekit.io/automationtester/automationtester-in/blog/covers/v3/debug-appium-selectors-android-ios.webp","alt":"Dark technical illustration over a faint dashed vertical grid. In the centre, a tall dashed green rounded boundary frames six stacked row panels of identical shape, each with grey text bars on the left and a pill-shaped button on the right; above and below the boundary, dashed grey outlines of further rows fade off the top and bottom edges. From the left, an orange pill feeds an orange ringed dot, and thin orange curves fan out from it to the buttons of four different rows at once, while a dashed orange curve bends down past the boundary toward a ghost row and ends at a crossed-out orange ring. The fourth row alone is outlined in green, with a ringed green dot on its left edge and a green button. A green curve leaves that row to the right into a larger green panel with a ringed dot on its left edge, text bars, and a small dashed green box around a single bright green button, then continues as a straight green line to an arrowhead at the right edge."},"contentHtml":"<div class=\"callout callout-info\"><strong>Quick answer:</strong> Dump the page source on both platforms at the moment the step fails, then match the symptom to one of four causes: the row has not been created yet because the list is virtualized, several nodes answer to your selector, the identifier exists in app code but never reaches the accessibility tree, or you are holding an element reference from before the list moved. Each has a check that tells it apart from the others. The durable fix for a list of look-alike rows is a per-row identifier (Compose <code class=\"language-text\">testTag</code> exposed as a resource id, SwiftUI <code class=\"language-text\">accessibilityIdentifier</code>), a lookup scoped inside that row, and a fresh lookup after every scroll.</div>\n\n<p>The test is three lines long. Find the product, tap its \"Add to cart\" button, assert the badge says one. On the Android emulator the badge says one, but the item in the cart is the wrong product. On the iOS simulator the same test fails at the first line with <code class=\"language-text\">NoSuchElementError</code>, even though Appium Inspector shows the product sitting right there on screen. Nobody changed the selector. The two platforms are simply answering different questions with it.</p>\n\n<p>Mobile selectors fail differently from web ones because the tree you query is not the UI you see. It is an accessibility snapshot produced by UiAutomator on Android and by XCTest on iOS, each with its own rules about what gets included, what an identifier is called, and when a node stops existing.</p>\n\n<div class=\"callout callout-warning\"><strong>Versions checked on 2026-09-30:</strong> Appium 3.8.0 (the <code class=\"language-text\">latest</code> tag on npm; 4.0.0 is still in beta), UiAutomator2 driver 8.7.0, XCUITest driver 12.13.3, WebdriverIO 9.32.0. UiAutomator2 driver 5.0.0 and later, and XCUITest driver 10.0.0 and later, run only on Appium 3. Setting defaults below are quoted from the driver documentation at those versions, and several of them (<code class=\"language-text\">mapTestTagToResourceId</code> in particular) do not exist in older drivers.</div>\n\n<h2 id=\"what-each-appium-error-is-reporting\">What each error is actually reporting</h2>\n\n<p>Three signatures cover almost every case, and one of them is not an error at all.</p>\n\n<pre class=\"language-text\"><code class=\"language-text\">no such element: An element could not be located on the page using the given search parameters.\n\nstale element reference: The element '&lt;locator&gt;' does not exist in DOM anymore\nstale element reference: The element identified by \"6A00...\" is either not present or it has expired from the internal cache. Try to find it again</code></pre>\n\n<p>The first line is the W3C WebDriver wording, which Appium's base driver reuses as its default message. It means the lookup ran against the current snapshot and matched nothing. That is a statement about the snapshot, not about the screen.</p>\n\n<p>The two stale messages come from the device-side servers. The first is from the UiAutomator2 server's element cache on Android, the second from WebDriverAgent on iOS. Both mean the handle you are using points at a node that the platform has since discarded, which in a scrolling list is routine: rows are recycled or recomposed as they leave the viewport.</p>\n\n<p>The wrong-element tap produces no error. The WebDriver specification's Find Element steps end with \"return the first element of result\", so a selector that matches six rows succeeds and hands you the first one in tree order. Which row is first depends on how far the list has scrolled, so the test passes on a fresh launch and taps the wrong product after any earlier step moved the list.</p>\n\n<h2 id=\"a-product-list-rendered-twice\">The same product list, rendered twice</h2>\n\n<p>Here is the kind of screen that produces all three symptoms. Each row shows a product name and an \"Add to cart\" button, and the list is long enough to scroll. The Android version uses a <code class=\"language-text\">LazyColumn</code>:</p>\n\n<pre class=\"language-kotlin\"><code class=\"language-kotlin\">@Composable\nfun ProductList(products: List&lt;Product&gt;, onAdd: (Product) -&gt; Unit) {\n    LazyColumn {\n        items(products, key = { it.id }) { product -&gt;\n            Row(Modifier.fillMaxWidth().padding(16.dp)) {\n                Text(product.name, Modifier.weight(1f))\n                Button(onClick = { onAdd(product) }) {\n                    Text(stringResource(R.string.add_to_cart))\n                }\n            }\n        }\n    }\n}</code></pre>\n\n<p>The iOS version uses a SwiftUI <code class=\"language-text\">List</code>:</p>\n\n<pre class=\"language-swift\"><code class=\"language-swift\">struct ProductList: View {\n    let products: [Product]\n    let onAdd: (Product) -&gt; Void\n\n    var body: some View {\n        List(products) { product in\n            HStack {\n                Text(product.name)\n                Spacer()\n                Button(\"Add to cart\") { onAdd(product) }\n            }\n        }\n    }\n}</code></pre>\n\n<p>Neither version gives a test anything stable to hold. Every button has the same visible text, which is also the only thing the accessibility tree knows it by, and that text will change the day the app ships in German.</p>\n\n<h2 id=\"dump-the-hierarchy-from-the-failing-step\">Dump the hierarchy from inside the failing step</h2>\n\n<p>Appium Inspector is useful for exploring, but it is a poor witness for a failure. Its documentation says the source XML \"is only updated when a refresh is requested\", and that interacting with the app outside the Inspector \"will not cause the source XML to refresh\". What you see there is the tree as of your last click, not the tree your test queried.</p>\n\n<p>Capture the source from the test itself, at the step that fails, and count matches for the selector you used while you are there:</p>\n\n<pre class=\"language-javascript\"><code class=\"language-javascript\">import { writeFile } from 'node:fs/promises';\n\nexport async function snapshotFor(selector, label) {\n  const platform = driver.isAndroid ? 'android' : 'ios';\n  const source = await driver.getPageSource();\n  await writeFile(`artifacts/${label}-${platform}.xml`, source);\n\n  const matches = await $$(selector);\n  console.log(`${platform} ${selector}: ${matches.length} match(es)`);\n  return matches.length;\n}\n\n// in the failing test, before the lookup that fails\nawait snapshotFor('//*[@text=\"Add to cart\" or @label=\"Add to cart\"]', 'add-to-cart');</code></pre>\n\n<p>Run it on both platforms and keep both files. The two XML documents use different vocabularies: Android nodes carry <code class=\"language-text\">resource-id</code>, <code class=\"language-text\">content-desc</code> and <code class=\"language-text\">text</code>, while iOS nodes carry <code class=\"language-text\">name</code>, <code class=\"language-text\">label</code> and <code class=\"language-text\">value</code> on element types such as <code class=\"language-text\">XCUIElementTypeButton</code>.</p>\n\n<p>One trap before you trust either file: both drivers cap the snapshot depth. UiAutomator2's <code class=\"language-text\">snapshotMaxDepth</code> defaults to 70, and the driver warns that \"a part of the elements source tree might be lost if the value is too low\". XCUITest's defaults to 50 and accepts only 1 to 62, because deeper values \"are not supported by XCTest\". A deeply nested Compose or SwiftUI screen can put your row below the cut-off, so if a node you can see on screen is missing from the dump, raise the setting once and dump again before concluding anything else.</p>\n\n<h2 id=\"four-causes-and-the-check-for-each\">Four causes, told apart by what the dump shows</h2>\n\n<h3 id=\"the-row-has-not-been-created\">The row has not been created yet</h3>\n\n<p>Android's Compose documentation is explicit that <code class=\"language-text\">LazyColumn</code> and <code class=\"language-text\">LazyRow</code> \"only compose and lay out items which are visible in the component's viewport\". Product 1042, forty rows down, is not hidden. It does not exist, and no selector can find it.</p>\n\n<p>On iOS, Apple documents the same behaviour for <code class=\"language-text\">LazyVStack</code>, which \"doesn't create items until it needs to render them onscreen\". The <code class=\"language-text\">List</code> page makes no such statement either way, so treat the dump as the authority: scroll the row out of view and see whether its cell is still in the source.</p>\n\n<p>The check that separates \"not created\" from \"created but filtered out\" on Android is the <code class=\"language-text\">allowInvisibleElements</code> setting, which is <code class=\"language-text\">false</code> by default and controls whether nodes whose <code class=\"language-text\">displayed</code> attribute is false appear in the source at all:</p>\n\n<pre class=\"language-javascript\"><code class=\"language-javascript\">await driver.updateSettings({ allowInvisibleElements: true });\nconst withHidden = await $$('id=product-1042');\nawait driver.updateSettings({ allowInvisibleElements: false });\n\n// 0 here as well: the row was never composed. Scroll to it.\n// 1 here but 0 normally: it exists off screen. Scroll it into view before acting.</code></pre>\n\n<p>The fix is to scroll by identifier and look the row up again afterwards. On Android, <code class=\"language-text\">mobile: scrollGesture</code> returns <code class=\"language-text\">true</code> while the container can still scroll in that direction, which gives you a loop with a natural end:</p>\n\n<pre class=\"language-javascript\"><code class=\"language-javascript\">async function scrollToRowAndroid(rowId, maxScrolls = 20) {\n  const list = await $('id=products');\n  for (let i = 0; i &lt; maxScrolls; i++) {\n    const rows = await $$(`id=${rowId}`);\n    if (rows.length === 1) return rows[0];\n    if (rows.length &gt; 1) throw new Error(`${rowId} matched ${rows.length} nodes`);\n    const canScrollMore = await driver.execute('mobile: scrollGesture', {\n      elementId: list.elementId, direction: 'down', percent: 0.8,\n    });\n    if (!canScrollMore) break;\n  }\n  throw new Error(`${rowId} not found after scrolling the list to its end`);\n}</code></pre>\n\n<p>You will see older answers do this in one line with <code class=\"language-text\">new UiScrollable(...).scrollIntoView(...)</code> through the <code class=\"language-text\">-android uiautomator</code> strategy. It works today, but Google's UI Automator page says <code class=\"language-text\">UiScrollable</code> and <code class=\"language-text\">UiSelector</code> \"are being deprecated in a future release\", and the UiAutomator2 driver warns that their removal \"will render all <code class=\"language-text\">-android uiautomator</code>-based locators invalid\". The gesture loop does not depend on them.</p>\n\n<p>On iOS, <code class=\"language-text\">mobile: scroll</code> accepts a <code class=\"language-text\">predicateString</code> for the child to scroll to, provided you pass the container as <code class=\"language-text\">elementId</code>:</p>\n\n<pre class=\"language-javascript\"><code class=\"language-javascript\">async function scrollToRowIos(rowId) {\n  const list = await $('~products');\n  await driver.execute('mobile: scroll', {\n    elementId: list.elementId,\n    predicateString: `name == '${rowId}'`,\n  });\n  return $(`~${rowId}`);\n}</code></pre>\n\n<p>The XCUITest documentation is candid about its limits: the extension \"relies on several undocumented XCTest features\", and it may fail when reaching the child needs \"tens of\" scroll gestures. For a very long catalogue, filter or search the list down first instead of scrolling through it.</p>\n\n<h3 id=\"several-nodes-answer-the-selector\">Several nodes answer the same selector</h3>\n\n<p>This is the wrong-product tap. The dump shows six or seven nodes with <code class=\"language-text\">text=\"Add to cart\"</code> on Android, or <code class=\"language-text\">label=\"Add to cart\"</code> on iOS, and the match count from the snippet above is greater than one. Positional XPath such as <code class=\"language-text\">(//android.widget.Button)[3]</code> is the same bug with a number attached: position three is a different product after every scroll.</p>\n\n<p>The fix belongs in the app. Give each row an identifier derived from the data, give the button a fixed identifier, and on Android expose test tags to UiAutomator once, high in the tree:</p>\n\n<pre class=\"language-kotlin\"><code class=\"language-kotlin\">Scaffold(modifier = Modifier.semantics { testTagsAsResourceId = true }) { padding -&gt;\n    LazyColumn(Modifier.padding(padding).testTag(\"products\")) {\n        items(products, key = { it.id }) { product -&gt;\n            Row(Modifier.fillMaxWidth().testTag(\"product-${product.id}\")) {\n                Text(product.name, Modifier.weight(1f))\n                Button(onClick = { onAdd(product) }, Modifier.testTag(\"add-to-cart\")) {\n                    Text(stringResource(R.string.add_to_cart))\n                }\n            }\n        }\n    }\n}</code></pre>\n\n<pre class=\"language-swift\"><code class=\"language-swift\">List(products) { product in\n    HStack {\n        Text(product.name)\n        Spacer()\n        Button(\"Add to cart\") { onAdd(product) }\n            .accessibilityIdentifier(\"add-to-cart\")\n    }\n    .accessibilityElement(children: .contain)\n    .accessibilityIdentifier(\"product-\\(product.id)\")\n}\n.accessibilityIdentifier(\"products\")</code></pre>\n\n<p>The <code class=\"language-text\">.contain</code> behaviour matters for the lookup that follows. Apple describes it as making the view \"an accessibility container\" whose child elements become its children, so the row gets its own identifier and the button stays a separate element inside it. Open the dump after the change and confirm the nesting rather than assuming it.</p>\n\n<p>Now the button identifier is deliberately not unique across the screen. It only has to be unique inside a row, and the test finds the row first:</p>\n\n<pre class=\"language-javascript\"><code class=\"language-javascript\">// Wrong: first \"Add to cart\" in tree order, whichever row that is right now\nawait $('//*[@text=\"Add to cart\"]').click();\n\n// Right: find the row, then search only inside it\nconst rowSelector = driver.isAndroid ? 'id=product-1042' : '~product-1042';\nconst buttonSelector = driver.isAndroid ? 'id=add-to-cart' : '~add-to-cart';\n\nconst row = driver.isAndroid\n  ? await scrollToRowAndroid('product-1042')\n  : await scrollToRowIos('product-1042');\n\nconst buttons = await row.$$(buttonSelector);\nif (buttons.length !== 1) throw new Error(`expected one button in the row, got ${buttons.length}`);\nawait buttons[0].click();</code></pre>\n\n<h3 id=\"identifier-set-but-not-exposed\">The identifier is in the code but not in the tree</h3>\n\n<p>This presents as <code class=\"language-text\">NoSuchElementError</code> for an identifier that is plainly in the source code. The dump says which of three things happened.</p>\n\n<p><strong>Android, no <code class=\"language-text\">resource-id</code> on the row at all.</strong> Compose does not expose test tags to UiAutomator by default. Its testing documentation says composables are reachable \"only by their convenient descriptors (displayed text, content description, etc.)\" until <code class=\"language-text\">testTagsAsResourceId</code> is enabled for the subtree, a property available from Compose 1.2.0-alpha08. If the app team cannot ship that change yet, UiAutomator2 driver 8.4.0 added a <code class=\"language-text\">mapTestTagToResourceId</code> setting that performs the same mapping from the test side, applied to attributes, page source and id lookups.</p>\n\n<p><strong>Android, <code class=\"language-text\">resource-id=\"product-1042\"</code> is in the dump but <code class=\"language-text\">id=product-1042</code> still fails.</strong> The driver rewrote your selector. Its <code class=\"language-text\">id</code> strategy adds the package prefix \"automatically if unset\", so the lookup actually searched for <code class=\"language-text\">com.example.shop:id/product-1042</code>. Compose's documentation warns about the same mismatch from the other side: a package-qualified resource name \"is different from <code class=\"language-text\">Modifier.testTag</code>\". Turn the rewriting off:</p>\n\n<pre class=\"language-javascript\"><code class=\"language-javascript\">// Either: stop the driver prefixing bare ids with the app package\nawait driver.updateSettings({ disableIdLocatorAutocompletion: true });\n\n// Or, on UiAutomator2 driver 8.4.0+, map testTag to resource-id without an app change.\n// Bare tags are matched as-is under this setting, with no package prefix.\nawait driver.updateSettings({ mapTestTagToResourceId: true });</code></pre>\n\n<p><strong>iOS, the row's <code class=\"language-text\">name</code> equals its visible text.</strong> The XCUITest driver's <code class=\"language-text\">accessibility id</code>, <code class=\"language-text\">id</code> and <code class=\"language-text\">name</code> strategies are all aliases for the same <code class=\"language-text\">name</code> attribute, and that attribute \"corresponds to the element's XCTest identifier or label property, depending on which one is available first\". When no identifier reaches the element, the name silently falls back to the label. So <code class=\"language-text\">~Add to cart</code> works on iOS without any identifier, which is exactly how a suite ends up full of label lookups that break under localization. If the dump shows <code class=\"language-text\">name=\"Add to cart\"</code> where you expected <code class=\"language-text\">name=\"add-to-cart\"</code>, the modifier did not land on the element you think it did.</p>\n\n<p>That fallback is also why sharing <code class=\"language-text\">~</code> across platforms is a poor bargain. On Android the accessibility id strategy maps to UiAutomator's <code class=\"language-text\">By.desc</code>, an exact match on content description. Content description is what TalkBack reads aloud, and Android's own guidance is to \"use a localized string, as it is visible to the user\". Putting <code class=\"language-text\">product-1042</code> there to satisfy a test makes the screen reader announce it. Apple's identifier has the opposite property: the XCUITest driver notes it is \"supposed to stay constant under different locales, and does not affect accessibility services such as VoiceOver\". The two platforms have a stable, silent identifier each, and they are reached by different strategies.</p>\n\n<h3 id=\"the-element-went-stale-after-scrolling\">The element was found, then the list moved</h3>\n\n<p>Here the dump looks fine and a fresh lookup succeeds, but the reference the test was holding does not. The selector is innocent; the test kept a handle too long.</p>\n\n<p>The Android server's behaviour is worth knowing in detail. When a cached element goes stale, the UiAutomator2 server tries to restore it by re-running the locator that found it. It refuses when that locator matched more than one node, with a source comment that reads \"we cannot restore a single element from a locator that matches multiple items\", and it throws \"is not linked to the same object in DOM anymore\" when the re-run finds a different node. So an element picked out of an ambiguous <code class=\"language-text\">$$</code> result cannot be recovered, and an element found by a unique identifier sometimes can. WebDriverAgent on iOS makes no such attempt and tells you to \"try to find it again\".</p>\n\n<p>The rule that follows is simple: never carry an element handle across a scroll, a navigation, or a list refresh. Keep selectors in variables and elements in local scope, and look the row up again after anything that could have recycled it. The <code class=\"language-text\">scrollToRow</code> helpers above return a fresh element for exactly this reason.</p>\n\n<p>A related timing symptom is a tap that lands on the neighbouring row while the list is still settling. Both drivers wait for the app to go idle before acting, and both default that wait to ten seconds (<code class=\"language-text\">waitForIdleTimeout</code>, in milliseconds on Android and seconds on iOS). Setting it to zero is the common way to \"speed up\" a slow suite. The UiAutomator2 documentation says plainly what it costs: with no wait, \"clicks and other actions might happen at wrong places of the application UI\". If lookups are slow because an animation never stops, disable animations in the test build rather than disabling the wait.</p>\n\n<h2 id=\"xpath-on-visible-text-fails-twice\">XPath on visible text fails on both counts</h2>\n\n<p>The selector at the start of this article, an XPath on the button text, combines the two weakest choices. It depends on copy that changes with locale, and it runs against the page-source XML rather than the platform's native query engine. The XCUITest driver ranks XPath lowest of its strategies and says such locators \"can be much slower compared to all other ones - sometimes up to 10x slower\". On Android, the UiAutomator2 driver evaluates XPath against \"the same XML tree that is generated by page source API\", which is why it inherits every depth limit and visibility filter described above.</p>\n\n<p>XPath still has a place for elements that can only be told apart by their neighbours. If you have to keep one, count its matches with the snapshot helper on both platforms before committing it. On iOS, a class chain query such as <code class=\"language-text\">**/XCUIElementTypeCell[`name == 'product-1042'`]/**/XCUIElementTypeButton</code> expresses the same parent-then-child relationship through XCTest's own predicate engine.</p>\n\n<h2 id=\"capture-both-hierarchies-before-the-next-fix\">Capture both hierarchies before the next fix</h2>\n\n<p>Pick the flakiest list interaction in your suite and run the snapshot helper at its lookup on one Android device and one iOS simulator. Keep the two XML files and the match counts with the bug. From those, write down which of the four causes applies on each platform. Do not assume it is the same one: a duplicate match on Android and a label fallback on iOS can sit behind a single failing step.</p>\n\n<p>Then take the per-row identifier request to the app team with the dumps attached. Ask for three things: a data-derived identifier on each row, a fixed identifier on each control inside a row, and <code class=\"language-text\">testTagsAsResourceId</code> enabled once near the root of the Compose tree. Until that ships, the <code class=\"language-text\">mapTestTagToResourceId</code> setting covers Android if the tags already exist in code.</p>\n\n<p>Finally, make the suite refuse ambiguity. Route every row interaction through a helper that asserts exactly one match for the row and exactly one for the control inside it, so the next duplicate fails loudly at the lookup instead of passing by tapping the wrong product.</p>\n\n<h2 id=\"appium-selector-questions-from-mobile-teams\">Questions mobile teams ask about these failures</h2>\n\n<h3 id=\"faq-inspector-shows-it\">Inspector shows the element, so why does the test say it does not exist?</h3>\n\n<p>Inspector shows the tree from its last refresh, possibly on a different screen state, and it does not refresh when the app changes outside its own actions. Compare against a source dump taken by the test at the failing step. If the node is on screen but absent from both, raise <code class=\"language-text\">snapshotMaxDepth</code> before investigating further.</p>\n\n<h3 id=\"faq-one-selector-both-platforms\">Can one selector work on Android and iOS?</h3>\n\n<p>Only if you put the same string into Android's content description, which TalkBack reads to users. Keeping the identifier strings identical across platforms and switching the strategy (<code class=\"language-text\">id=</code> on Android with exposed test tags, <code class=\"language-text\">~</code> on iOS) gets you shared test code without shipping identifiers to screen-reader users.</p>\n\n<h3 id=\"faq-react-native\">What changes for a React Native app?</h3>\n\n<p>The mapping moves. The XCUITest driver documents that the iOS <code class=\"language-text\">name</code> attribute corresponds to React Native's <code class=\"language-text\">testID</code>, and the UiAutomator2 driver documents that on Android the content description reflects <code class=\"language-text\">accessibilityLabel</code>. Dump the source on both platforms before assuming either maps to the same strategy.</p>\n\n<h3 id=\"faq-recyclerview\">Does this apply to RecyclerView rather than Compose?</h3>\n\n<p>The diagnosis does. Android's RecyclerView guide says that when an item scrolls off the screen the view is not destroyed but reused for new items scrolling on, so the dump and the <code class=\"language-text\">allowInvisibleElements</code> check are still how you tell \"not created\" from \"filtered\". The identifier fix is simpler there, because View resource ids follow the <code class=\"language-text\">&lt;packageName&gt;:id/</code> convention the driver's prefixing assumes, so neither setting above is needed. What you cannot do is set a per-row resource id from data, so per-row identity usually comes from content description or a tag the team exposes deliberately.</p>\n\n<h2 id=\"appium-selector-primary-references\">Primary references</h2>\n\n<ul>\n<li><a href=\"https://github.com/appium/appium-uiautomator2-driver/blob/master/README.md\">Appium UiAutomator2 Driver README</a>. The location strategies and what they map to (<code class=\"language-text\">id</code> to <code class=\"language-text\">By.res</code> with the package prefix \"added automatically if unset\", accessibility id to <code class=\"language-text\">By.desc</code>, XPath against the page-source XML), the deprecation warning for <code class=\"language-text\">-android uiautomator</code>, and the settings <code class=\"language-text\">allowInvisibleElements</code>, <code class=\"language-text\">snapshotMaxDepth</code> (default 70), <code class=\"language-text\">waitForIdleTimeout</code> (default 10000 ms, and the cost of setting it to zero), <code class=\"language-text\">disableIdLocatorAutocompletion</code> and <code class=\"language-text\">mapTestTagToResourceId</code> (since driver 8.4.0). Also that driver 5.0.0+ runs only on Appium 3. Driver 8.7.0.</li>\n<li><a href=\"https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md\">UiAutomator2 Driver — Automating Mobile Gestures</a>. The <code class=\"language-text\">mobile: scrollGesture</code> arguments and its boolean return, \"true if the object can still scroll in the given direction\".</li>\n<li><a href=\"https://github.com/appium/appium/releases/tag/appium%403.8.0\">Appium 3.8.0 release</a>. That 3.8.0, published 2026-09-24, is the current stable Appium, with 4.0.0 only at beta.</li>\n<li><a href=\"https://github.com/appium/appium-xcuitest-driver/blob/master/README.md\">Appium XCUITest Driver README</a>. That driver 10.0.0 and later runs only on Appium 3. Driver 12.13.3.</li>\n<li><a href=\"https://appium.github.io/appium-xcuitest-driver/latest/reference/locator-strategies/\">XCUITest Driver — Locator Strategies</a>. That <code class=\"language-text\">id</code> and <code class=\"language-text\">accessibility id</code> are aliases for the <code class=\"language-text\">name</code> strategy, when to use predicate strings and class chains, and that XPath \"can be much slower compared to all other ones - sometimes up to 10x slower\".</li>\n<li><a href=\"https://appium.github.io/appium-xcuitest-driver/latest/reference/element-attributes/\">XCUITest Driver — Element Attributes</a>. That <code class=\"language-text\">name</code> is the XCTest identifier or label \"depending on which one is available first\", the recommendation to prefer <code class=\"language-text\">accessibilityIdentifier</code> because it stays constant across locales and does not affect VoiceOver, and the React Native <code class=\"language-text\">testID</code> mapping.</li>\n<li><a href=\"https://appium.github.io/appium-xcuitest-driver/latest/reference/settings/\">XCUITest Driver — Settings</a>. <code class=\"language-text\">snapshotMaxDepth</code> default 50 and range 1..62, and <code class=\"language-text\">waitForIdleTimeout</code> default 10 seconds.</li>\n<li><a href=\"https://appium.github.io/appium-xcuitest-driver/latest/reference/execute-methods/\">XCUITest Driver — Execute Methods</a>. The <code class=\"language-text\">mobile: scroll</code> arguments <code class=\"language-text\">elementId</code>, <code class=\"language-text\">name</code> and <code class=\"language-text\">predicateString</code>, and its stated reliance on undocumented XCTest features.</li>\n<li><a href=\"https://appium.github.io/appium-inspector/2026.9/session-inspector/source/\">Appium Inspector — Source</a>. That the source XML only updates when a refresh is requested and not when the app changes outside the Inspector.</li>\n<li><a href=\"https://www.w3.org/TR/webdriver2/\">W3C WebDriver</a>. The Find Element steps that \"return the first element of result\", and the error texts for <code class=\"language-text\">no such element</code> and <code class=\"language-text\">stale element reference</code>.</li>\n<li><a href=\"https://github.com/appium/appium-uiautomator2-server/blob/v10.6.6/app/src/main/java/io/appium/uiautomator2/model/ElementsCache.java\">appium-uiautomator2-server ElementsCache.java at v10.6.6</a>. The stale-element restore logic: re-running the original locator, refusing when it matched multiple items, and the exact stale messages.</li>\n<li><a href=\"https://github.com/appium/WebDriverAgent/blob/v16.13.5/WebDriverAgentLib/Routing/FBElementCache.m\">WebDriverAgent FBElementCache.m at v16.13.5</a>. The iOS stale-element message, \"either not present or it has expired from the internal cache\".</li>\n<li><a href=\"https://developer.android.com/develop/ui/compose/testing/interoperability\">Android Developers — Interoperability with UiAutomator</a>. That composables are reachable only by text and content description until <code class=\"language-text\">testTagsAsResourceId</code> is enabled, the Compose 1.2.0-alpha08 minimum, enabling it once high in the hierarchy, and the warning that a package-qualified resource name differs from <code class=\"language-text\">Modifier.testTag</code>.</li>\n<li><a href=\"https://developer.android.com/develop/ui/compose/lists\">Android Developers — Lists and grids</a>. That <code class=\"language-text\">LazyColumn</code> and <code class=\"language-text\">LazyRow</code> \"only compose and lay out items which are visible\", and stable item keys.</li>\n<li><a href=\"https://developer.android.com/develop/ui/compose/accessibility/api-defaults\">Android Developers — Compose accessibility API defaults</a>. That <code class=\"language-text\">contentDescription</code> should be a localized string because it is presented to the user.</li>\n<li><a href=\"https://developer.android.com/training/testing/other-components/ui-automator-legacy\">Android Developers — UI Automator (legacy APIs)</a>. That <code class=\"language-text\">UiObject</code>, <code class=\"language-text\">UiScrollable</code> and <code class=\"language-text\">UiSelector</code> are being deprecated in a future release.</li>\n<li><a href=\"https://developer.apple.com/documentation/swiftui/view/accessibilityidentifier(_:)\">Apple — accessibilityIdentifier(_:)</a>. That the identifier is for testing and \"isn't visible to the user\".</li>\n<li><a href=\"https://developer.apple.com/documentation/swiftui/accessibilitychildbehavior/contain\">Apple — AccessibilityChildBehavior.contain</a>. That child accessibility elements become children of a new container element.</li>\n<li><a href=\"https://developer.apple.com/documentation/swiftui/lazyvstack\">Apple — LazyVStack</a>. That the stack \"doesn't create items until it needs to render them onscreen\".</li>\n<li><a href=\"https://developer.android.com/develop/ui/views/layout/recyclerview\">Android Developers — Create dynamic lists with RecyclerView</a>. That an item scrolling off screen has its view reused for new items rather than destroyed.</li>\n<li><a href=\"https://webdriver.io/docs/selectors\">WebdriverIO — Selectors</a>. The <code class=\"language-text\">~</code>, <code class=\"language-text\">id=</code>, <code class=\"language-text\">android=</code>, <code class=\"language-text\">-ios predicate string:</code> and <code class=\"language-text\">-ios class chain:</code> selector syntaxes used in the snippets.</li>\n</ul>\n"}