Skip to content

Locators

Locators identify UI elements on the device. They are exposed as Playwright-style getBy* methods on Device and ElementHandle. Each method returns an ElementHandle — a lazy reference that resolves when an action or assertion runs against it.

See the Selectors Guide for a deeper discussion of when to use each one.

device.getByText(text: string, options?: { exact?: boolean }): ElementHandle

Section titled “device.getByText(text: string, options?: { exact?: boolean }): ElementHandle”

Locate an element by its visible text. Substring match by default, like Playwright. Pass { exact: true } for an exact match.

device.getByText("Welcome") // substring
device.getByText("Sign In", { exact: true }) // exact

Because the default is a substring match, getByText("Sign in") also matches longer text like "Sign in to continue". When that happens, acting on the locator throws a strict mode violation — add { exact: true } or use getByRole(role, { name }) to pin a single element.

device.getByRole(role: string, options?): ElementHandle

Section titled “device.getByRole(role: string, options?): ElementHandle”

Locate an element by its accessibility role, optionally filtered by accessible name or state.

device.getByRole("button", { name: "Submit" })
device.getByRole("textfield", { name: "Email" })
device.getByRole("checkbox")
device.getByRole("switch", { name: "Dark Mode", checked: true })
device.getByRole("button", { name: "Submit", disabled: true })
device.getByRole("tab", { name: "Settings", selected: true })
device.getByRole("button", { name: "Details", expanded: true })

Options:

OptionTypeDescription
namestringFilter by accessible name
checkedbooleanFilter by checked state (checkbox, switch, radio)
disabledbooleanFilter by disabled state
selectedbooleanFilter by selected state (tab, option)
expandedbooleanFilter by expanded state (accordion, dropdown)

device.getByDescription(text: string): ElementHandle

Section titled “device.getByDescription(text: string): ElementHandle”

Locate an element by its accessibility description (Android contentDescription, iOS accessibilityLabel).

device.getByDescription("Close menu")
device.getByDescription("Profile photo")

device.getByPlaceholder(text: string): ElementHandle

Section titled “device.getByPlaceholder(text: string): ElementHandle”

Locate an input by its placeholder / hint text.

device.getByPlaceholder("Enter your email")
device.getByPlaceholder("Search")

device.getByTestId(testId: string): ElementHandle

Section titled “device.getByTestId(testId: string): ElementHandle”

Locate an element by its dedicated test identifier.

device.getByTestId("submit-button")

device.getByLabel(text: string): ElementHandle

Section titled “device.getByLabel(text: string): ElementHandle”

Locate an input element by its associated label text. Finds form controls (text fields, checkboxes, switches, etc.) whose accessible name matches the label.

  • Android: matches inputs whose contentDescription equals the text, or inputs linked via labelFor/labeledBy.
  • iOS: matches input elements (text fields, switches, sliders, etc.) whose accessibilityLabel equals the text.
device.getByLabel("Email") // finds the email text field
device.getByLabel("Dark Mode") // finds the Dark Mode switch
device.getByLabel("Volume") // finds the Volume slider

device.locator(options: LocatorOptions): ElementHandle

Section titled “device.locator(options: LocatorOptions): ElementHandle”

Escape hatch for native, non-accessible queries. Exactly one of id, xpath, or className must be set.

device.locator({ id: "com.myapp:id/email_input" })
device.locator({ className: "com.myapp.widget.ColorPicker" })
// XPath is Android-only. Always include a comment explaining why.
device.locator({ xpath: "//android.widget.Button[@text='OK']" })

LocatorOptions:

OptionTypeDescription
idstringNative resource id (Android R.id.foo or iOS accessibilityIdentifier).
xpathstringXPath expression. Android-only.
classNamestringNative widget class name.

The getBy* methods and locator() are also available on every ElementHandle. Calling them on a parent locator scopes the search to its descendants. See ElementHandle Scoping.

iOS wrapper suppression. When traversing the iOS accessibility tree, Tapsmith drops a matching XCUIElementTypeOther container if a descendant also matches the same selector and shares the wrapper’s accessibilityIdentifier (or, when the wrapper’s identifier is empty, its accessibilityLabel). This collapses the redundant wrappers React Native (and SwiftUI in some configurations) emit around interactive elements, so a getByText("Submit") resolves to the actual control rather than the surrounding container. The visible text of the wrapper and descendant is not compared — identifier/label match alone is enough. If your native iOS app deliberately exposes an .other container with the same identifier as a child you also want addressable, the outer wrapper will be silently suppressed in favour of the child — give the wrapper a unique accessibilityIdentifier (or use device.locator({ id: ... })) to address it directly.

Like Playwright, Tapsmith locators are strict: a locator used for an action, single-element query, or assertion must resolve to exactly one element. When it resolves to more than one, the operation throws a StrictModeViolationError immediately (it does not keep auto-waiting) listing every match with a suggested unambiguous selector:

strict mode violation: getByText("Sign in") resolved to 2 elements:
1) text "Sign in to continue to DreamSpinner" [44,210][436,260] aka device.getByText("Sign in to continue to DreamSpinner", { exact: true })
2) button "Sign in" [44,640][436,712] aka device.getByRole("button", { name: "Sign in" })
Hint: use { exact: true }, getByRole(role, { name }), getByTestId(), or .first()/.nth()/.last() to target a single element.

This is the safety net for the substring default of getByText — without it, an ambiguous locator would silently act on the first match in document order, which is rarely the element you meant.

OperationStrict?
Actions (tap, type, scroll, dragTo, setChecked, …)Yes
Single-element queries (find, getText, isVisible, boundingBox, scrollIntoView, …)Yes
waitFor({ state: "visible" | "attached" })Yes
Positive assertions (toBeVisible, toHaveText, toBeChecked, …)Yes
waitFor({ state: "hidden" | "detached" })No — absence is evaluated over all matches
toBeHidden, not.toBeVisible, not.toExistNo — absence is evaluated over all matches
count(), all(), exists(), toHaveCountNo — inherently multi-element
Locators narrowed with .first() / .last() / .nth(n)Exempt — they target one match by definition

To handle a violation programmatically, import the error class or the cross-realm-safe guard:

import { StrictModeViolationError, isStrictModeViolation } from "tapsmith";
try {
await device.getByText("Delete").tap();
} catch (err) {
if (isStrictModeViolation(err)) {
console.log(err.elements.length); // the matched ElementInfo objects
}
throw err;
}

Accessibility-tree duplicates don’t count. Some platforms expose the same visual element twice — iOS in particular renders a React Native <Text testID="..."> as a parent StaticText carrying the attributes plus an inner child with identical text and pixel-identical bounds. Matches with identical text and identical bounds are collapsed to one element (the attribute-carrying first occurrence) before the strict check, since acting on either taps the same point. This collapsing also applies to count(), all(), and .nth() indexing, so positional chains stay consistent with what you see on screen. Distinct elements that merely share text at different positions still violate.

Transient duplicates: if a screen briefly shows two elements matching the locator mid-transition, the violation throws at that moment (Playwright behaves the same way). Prefer selectors that are unique at all times, or .first() when duplication is expected.

WebView locators (webview.getBy*) do not enforce strict mode yet; they act on the first DOM match.