Locators
getByRole() lets a single test target the same element on both Android and iOS,
even though each platform names its native classes differently. Mobilewright does
this by normalizing the native type reported by the device and mapping it to a
semantic role.
How a native type becomes a role
When you call screen.getByRole('textfield'), the query engine:
- Takes the raw native type from the UI dump
(Android:
android.widget.EditText, iOS:XCUIElementTypeTextField). - Normalizes it — strips the Android package prefix (
android.widget.,androidx.*, …) and the iOSXCUIElementTypeprefix — leaving a bare name (edittext,textfield). - Matches that bare name against the role's class list.
getByType() skips normalization and matches the raw native class instead, so
use it (or getByLabel() / getByTestId()) for classes that have no role mapping.
Android class → role
Android reports the base framework class (e.g. android.widget.EditText, even when
the app uses AppCompatEditText).
| Native class | getByRole() |
|---|---|
android.widget.Button | button |
android.widget.ImageButton | button |
android.widget.EditText | textfield |
android.widget.TextView | text |
android.widget.ImageView | image |
android.widget.Switch | switch |
android.widget.ToggleButton | switch |
android.widget.CheckBox | checkbox |
android.widget.RadioButton | radio |
android.widget.SeekBar | slider |
android.widget.ProgressBar | progressbar |
android.widget.Spinner | combobox |
android.widget.ListView | list |
androidx.recyclerview.widget.RecyclerView | list |
android.widget.ScrollView | list |
android.widget.LinearLayout | listitem |
android.widget.RelativeLayout | listitem |
android.widget.Toolbar | header |
AndroidX / Material Components
Some dumps surface the AndroidX or Material Components subclass name directly instead of the collapsed framework class above — these are also recognized:
| Native class | getByRole() |
|---|---|
androidx.appcompat.widget.AppCompatButton | button |
com.google.android.material.button.MaterialButton | button |
com.google.android.material.floatingactionbutton.FloatingActionButton | button |
androidx.appcompat.widget.AppCompatRadioButton | radio |
androidx.appcompat.widget.AppCompatSpinner | combobox |
androidx.appcompat.widget.AppCompatEditText | textfield |
com.google.android.material.textfield.TextInputEditText | textfield |
androidx.appcompat.widget.AppCompatTextView | text |
com.google.android.material.textview.MaterialTextView | text |
androidx.appcompat.widget.AppCompatImageView | image |
com.google.android.material.imageview.ShapeableImageView | image |
React Native (Android)
| Native class | getByRole() |
|---|---|
…textinput.ReactEditText | textfield |
…text.ReactTextView | text |
…image.ReactImageView | image |
…scroll.ReactScrollView | list |
…view.ReactViewGroup | button — only when clickable="true" or accessible="true" |
Jetpack Compose (Android)
Compose reports most of its widgets as a plain android.view.View. It only reports
android.widget.Button for a button with no children, so a Button { Text("Save") }
arrives as a clickable View with its caption on a child TextView.
| Compose node | getByRole() |
|---|---|
android.view.View | button — only when clickable |
android.view.View | alert — only when its pane title is Dialog, as on a Material3 AlertDialog |
A Compose TextField / OutlinedTextField is a real android.widget.EditText
(textfield), but its floating label = { Text("Email") } is a child TextView.
A text field without a label of its own is labelled by that child, so both of these
find it:
await screen.getByLabel('Email').fill('glorpax@milliways.space');
await expect(screen.getByRole('textfield', { name: 'Email' })).toHaveValue('glorpax@milliways.space');
iOS class → role
| Native class | getByRole() |
|---|---|
XCUIElementTypeButton | button |
XCUIElementTypeTextField | textfield |
XCUIElementTypeSecureTextField | textfield |
XCUIElementTypeSearchField | textfield |
XCUIElementTypeStaticText | text |
XCUIElementTypeTextView | text — see note below |
XCUIElementTypeImage | image |
XCUIElementTypeSwitch | switch |
XCUIElementTypeSlider | slider |
XCUIElementTypeProgressIndicator | progressbar |
XCUIElementTypeActivityIndicator | progressbar |
XCUIElementTypeAlert | alert |
XCUIElementTypeSheet | alert |
XCUIElementTypePicker | combobox |
XCUIElementTypePickerWheel | combobox |
XCUIElementTypeTable | list |
XCUIElementTypeCollectionView | list |
XCUIElementTypeScrollView | list |
XCUIElementTypeCell | listitem |
XCUIElementTypeOther | listitem |
XCUIElementTypeTab | tab |
XCUIElementTypeTabBar | tab |
XCUIElementTypeLink | link |
XCUIElementTypeNavigationBar | header |
Accessible names
getByRole('button', { name }) matches the element's own label, then its own text.
Roles that take their name from their content — button, link, tab, header,
switch, checkbox and radio, as in ARIA — then fall back to the text of their
children. That is how a Compose button is named by its caption:
await screen.getByRole('button', { name: 'Save' }).tap();
Other roles are never named by their children, so a list isn't named by its items.
The one exception is a textfield, which is named by its floating label (see above).
Notes and known gaps
-
Classes with no role. Anything not listed above has no role mapping —
getByRole()won't find it. Target it withgetByType('<raw.native.Class>'),getByLabel(), orgetByTestId(). Examples: AndroidRadioGroup,CheckedTextView; iOSDatePicker. -
iOS source filtering. mobilecli keeps the iOS dump small, so not every element reaches the query engine.
Button,TextField,SecureTextField,SearchField,TextView,Switch,WebView,Slider,PickerandPickerWheelare always included. Every other class in the iOS table is included only when it has a label, name oraccessibilityIdentifier— sogetByRole('list')finds a labeledTablebut not an anonymous one.XCUIElementTypeOtheris included only when it carries anaccessibilityIdentifier. Requires mobilecli 1.0.13 or later; earlier versions only surfaceButton,TextField,SecureTextField,SearchField,TextView,Switch,WebView,StaticText,ImageandIcon. -
TextViewis platform-ambiguous. On Android,TextViewis a static label (text). On iOS,XCUIElementTypeTextViewis an editable multiline input (UITextView/ SwiftUITextEditor), which is closer totextfield. Both normalize to the sametextviewtoken, so they currently share thetextrole. Aligning an editable iOS text view withtextfield(matching the web/ARIA model, where<textarea>and<input>are bothtextbox) is tracked separately.