Skip to main content

Which location strategy to use with mobile apps

Locator Strategy for native mobile apps

IDs and accessibility locators are still king.

NSPredicate (iOS) and UIAutomator (Android) are great—mainly better than XPath—but they do not beat a good accessibility id or resource-id. And CSS selectors don’t exist for native apps (only for WebViews).

Locator Strategy Ranking

Rank

iOS (native)

Android (native)

Why

1

Accessibility IDname/label (via MobileBy.AccessibilityId

or @iOSXCUITFindBy(accessibility = "Mehr anzeigen"))

Accessibility IDcontent-desc (via MobileBy.AccessibilityId)

or

@AndroidFindBy(accessibility = "Letzte Aktivitäten, Überschrift"))

Fast, readable, stable when set intentionally; improves a11y.

2

Stable identifiers (rare on iOS)

resource-id (via new UiSelector().resourceId(...))

or

@AndroidFindBy(id = "load_more")

id is just shorthand for matching the native Android resource-id attribute. Appium automatically maps idresource-id

If you pass only "load_more", Appium internally prepends the app’s package name:
"com.example.app:id/load_more"

If you pass the full value yourself, it still works:

@AndroidFindBy(id = "com.example.app:id/load_more")

Most stable app-side handle (Android).

3

NSPredicate / Class Chain

UIAutomator (new UiSelector()...)

Native engine, flexible, much faster and less brittle than XPath.

XPath 

XPath

Slow, brittle, DOM/structure‑dependent. Avoid unless you must.

CSS selectors: Not supported in native context on iOS/Android. Use CSS only inside WebViews (Chromium/WebKit). In WebViews, CSS is usually preferable to XPath.

When Each Makes Sense

  • Accessibility ID (best default):

    • iOS maps to name/label, Android maps to content-desc.

    • Great for buttons, tabs, and anything user-labeled.

    • Most readable tests, cross‑platform friendly.

  • resource-id (Android):

    • Super stable, ideal for inputs, icons without visible text, repeated items.

    • Prefer this over text queries when available.

  • NSPredicate (iOS) / UIAutomator (Android):

    • Use when you can’t get a stable a11y id or resource-id.

    • Powerful text ops (contains, begins/ends, regex), state filters (enabled/visible), class checks.

    • Also handy for case/diacritic handling on iOS ([c], [cd]).

  • XPath:

    • Only if nothing else is feasible (legacy screens, dynamic trees).

    • Keep short and attribute-based if you must use it.

iOS (prefer AccessibilityId; fall back to NSPredicate)

// Best (if set): AccessibilityId driver.findElement(MobileBy.AccessibilityId("Back")).click(); // Next best: NSPredicate (fast, flexible) driver.findElement(MobileBy.iOSNsPredicateString("label ==[cd] 'zurück'")).click(); // Contains, case-insensitive: driver.findElement(MobileBy.iOSNsPredicateString("name CONTAINS[c] 'login'")).click();

Android (prefer accessibility or resource-id; fall back to UIAutomator)

// Best (if set): AccessibilityId (content-desc) driver.findElement(MobileBy.AccessibilityId("Back")).click(); // Equally great: resource-id driver.findElement(MobileBy.AndroidUIAutomator("new UiSelector().resourceId('com.app:idsubmit')")).click();

 

PageFactory Example

Use‑case

iOS (PageFactory)

Android (PageFactory)

Accessibility first choice

@iOSXCUITFindBy(accessibility = "Back")

@AndroidFindBy(accessibility = "Back")

Stable ID

(rare on iOS)

@AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"com.app:id/submit\")")

Native fast text match

@iOSXCUITFindBy(iOSNsPredicate = "label ==[cd] 'zurück'")

@AndroidFindBy(uiAutomator = "new UiSelector().textMatches(\"(?i).*login.*\")")

Avoid if possible

@iOSXCUITFindBy(xpath = "...")

@AndroidFindBy(xpath = "...")

  • Whenever you have accessibility ids (and resource-ids on Android), use those first.

  • Use NSPredicate/UIAutomator when you lack stable IDs but still want speed and robustness. It is better alternative to XPath.

  • CSS is only for WebViews, where it usually beats XPath.

WebViews (Hybrid Apps)

  • Use CSS selectors (fast, readable) via the standard Selenium driver once you’ve switched to the WebView context.

  • Ranking in WebViews: CSS > XPath (IDs still best if present).

Examples (WebView only):

// After switching to WEBVIEW context: driver.findElement(By.id("email")).sendKeys("x@x.com"); driver.findElement(By.cssSelector("button.primary")).click();

 

NSPredicate / UIAutomator deep dive

Scenario

iOS (NSPredicate) — why it helps

Android (UIAutomator) — why it helps

You only have visible text (no a11y id / resource‑id)

Powerful string ops (==[c], ==[cd], CONTAINS, BEGINSWITH, MATCHES), runs natively in XCTest

Text selectors: .text(...), .textContains(...), .textMatches(...) (regex), runs natively in UIAutomator

 

You need case-insensitive (and/or diacritic-insensitive) matching

Add [c] (case) or [cd] (case+diacritic) to operators

Use regex (?i) in .textMatches(...) for case-insensitivity (no diacritic flag; use alternation if needed)

 

You must combine conditions (text + state)

AND/OR, numeric booleans (enabled == 1, visible == 1)

Chain selectors: .enabled(true).clickable(true) etc.

 

You want to avoid XPath for performance/stability

Native engine, typically faster and less brittle than XPath

Same advantage: native engine, faster and less brittle than XPath

 

You only know the class/type + partial label

Filter by text + type == 'XCUIElementTypeButton'

Combine .className("android.widget.Button") + .textMatches(...)

 

Dynamic UIs where exact text varies slightly

CONTAINS[c], BEGINSWITH[c], regex

.textContains(...) or regex with .textMatches(...)

Rule of thumb: Prefer AccessibilityId / resource‑id first. If unavailable, use NSPredicate (iOS) or UIAutomator (Android). Keep XPath as a last resort.

RAW + PageFactory examples

Exact match (case/diacritic considerations)

 

iOS (NSPredicate)

Android (UIAutomator)

RAW

driver.findElement(MobileBy.iOSNsPredicateString("label ==[cd] 'zurück'")).click();

driver.findElement(MobileBy.AndroidUIAutomator("new UiSelector().text(\"Zurück\")")).click();

PageFactory

@iOSXCUITFindBy(iOSNsPredicate = "label ==[cd] 'zurück'") WebElement back;

@AndroidFindBy(uiAutomator = "new UiSelector().text(\"Zurück\")") WebElement back;

Contains / partial text (case-insensitive)

 

iOS (NSPredicate)

Android (UIAutomator)

RAW

MobileBy.iOSNsPredicateString("name CONTAINS[c] 'login'")

MobileBy.AndroidUIAutomator("new UiSelector().textMatches(\"(?i).*login.*\")") (or .textContains("login") if case-sensitive is OK)

PageFactory

@iOSXCUITFindBy(iOSNsPredicate = "name CONTAINS[c] 'login'") WebElement loginBtn;

@AndroidFindBy(uiAutomator = "new UiSelector().textMatches(\"(?i).*login.*\")") WebElement loginBtn;

Starts/Ends with

 

iOS (NSPredicate)

Android (UIAutomator)

RAW

iOSNsPredicateString("label BEGINSWITH[c] 'sign'")

AndroidUIAutomator("new UiSelector().textMatches(\"(?i)^sign.*\")")

PageFactory

@iOSXCUITFindBy(iOSNsPredicate = "label BEGINSWITH[c] 'sign'") WebElement sign;

@AndroidFindBy(uiAutomator = "new UiSelector().textMatches(\"(?i)^sign.*\")") WebElement sign;

Regex (digits only, badges, ids embedded in text)

 

iOS (NSPredicate)

Android (UIAutomator)

RAW

iOSNsPredicateString("label MATCHES '^[0-9]+$'")

AndroidUIAutomator("new UiSelector().textMatches(\"^[0-9]+$\")")

PageFactory

@iOSXCUITFindBy(iOSNsPredicate = "label MATCHES '^[0-9]+$'") WebElement badge;

@AndroidFindBy(uiAutomator = "new UiSelector().textMatches(\"^[0-9]+$\")") WebElement badge;

Class/type + partial text

 

iOS (NSPredicate)

Android (UIAutomator)

RAW

iOSNsPredicateString("type == 'XCUIElementTypeButton' AND label CONTAINS[c] 'continue'")

AndroidUIAutomator("new UiSelector().className(\"android.widget.Button\").textMatches(\"(?i).*continue.*\")")

PageFactory

@iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeButton' AND label CONTAINS[c] 'continue'") WebElement cont;

@AndroidFindBy(uiAutomator = "new UiSelector().className(\"android.widget.Button\").textMatches(\"(?i).*continue.*\")") WebElement cont;

Handling diacritics (iOS builtin vs Android workaround)

 

iOS (NSPredicate)

Android (UIAutomator)

RAW

iOSNsPredicateString("label ==[cd] 'zuruck'") (matches “zurück” too)

`AndroidUIAutomator("new UiSelector().textMatches("(?i)zurück

PageFactory

@iOSXCUITFindBy(iOSNsPredicate = "label ==[cd] 'zuruck'") WebElement back;

`@AndroidFindBy(uiAutomator = "new UiSelector().textMatches("(?i)zurück

Unified Page Object for iOS + Android




Comments

Popular posts from this blog

Real Time JMeter Result Using Backend Listener

Since JMeter 2.13 Backend Listener has been available to create real time graph of JMeter Test. Following tutorial explain the entire process in detail. At the end of this tutorial you would be able to create JMeter Live Test Result dashboard similar to following - This tutorial borrows information from many sources and my own experiments with JMeter live reporting dashboard. I have added source of information wherever applicable But before we can build such a snazzy JMeter Live Reporting dashboard we need to understand two more components - influxDB (a time series database) and Grafana Dashboard This is a big tutorial, so take deep breath :-) and follow on. Once you complete set up specified in this tutorial then you can watch JMeter Training Video Tutorial to watch this in action. What is Time Series Database? A time series is a sequence of data points , typically consisting of successive measurements made over a time interval . Examples of time ...

Verify email confirmation using Selenium

Note: If you are new to java and selenium then start with selenium java training videos .     Email confirmation seems to be integral part of any registration process. I came across an application which lets you provide your email address. You can follow the sign up link in you mail and then complete the registration process. Lets consider we provide GMail address for it. Now if were to use only Selenium then we would have to follow following steps - Launch GMail using Selenium; Some how search for new mail in the list of available mails; Some how click on it; Parse the mail message; Get the registration link; Follow up with registration process What do you think of an approach in which you can

Selenium Tutorial: Ant Build for Selenium Java project

Ant is a build tool which could be used to have your tests running either from command line or from Hudson CI tool. There is detailed documentation available for ant here but probably you need to know only a little part of it for you selenium tests. The essentials which are needed to know are: Project Target (ant execution point and collection of tasks) Tasks (could be as simple as compilation) And there would usually be following targets for Selenium tools - setClassPath - so that ant knows where you jar files are loadTestNG - so that you could use testng task in ant and use it to execute testng tests from ant init - created the build file clean - delete the build file compile - compiles the selenium tests run - executes the selenium tests Here is my project set up for ant -