Troupeby AliveDocsSign in

Testing

Locators

A locator names an element on the screen by what a person or a screen reader sees: its role, its text, its label. This page covers the five ways to find an element, how text matches, how to narrow a locator, and how to reach system UI.

Finding an element

Locators start from the screen fixture, which is the foreground app. They read the device's accessibility tree, so they work the same on a native, React Native or Expo app.

MethodMatchesOn iOSOn Android
getByRole(role, options?)The element's role, and with name its accessible nameThe element type and traitsThe view class and accessibility flags
getByText(text, options?)The element's own textA static text's label, a text input's valuetext
getByLabel(text, options?)The accessibility labelaccessibilityLabelcontentDescription
getByPlaceholder(text, options?)The placeholder of a text inputplaceholderValueThe hint text
getByTestId(testId)The test idaccessibilityIdentifierresource-id, without the package prefix
TypeScript
await screen.getByRole('button', { name: 'Sign in' }).tap()
await screen.getByLabel('Email').fill('ada@ex.co')
await screen.getByPlaceholder('Password').fill('pw')
await expect(screen.getByText('Welcome back')).toBeVisible()
await expect(screen.getByTestId('count')).toHaveText('Count: 1')

React Native's testID becomes the test id on both platforms. Jetpack Compose needs testTagsAsResourceId for a test tag to reach resource-id.

A locator is lazy and holds no element. It is sent to the device with each action or assertion and resolved there, so it always finds the element as the screen is now.

How text matches

Every text argument is a string or a regular expression.

  • A string matches as a case-insensitive substring. getByText('welcome') finds "Welcome back".
  • exact: true makes it a case-sensitive match of the whole string.
  • Whitespace is normalized on both sides first: runs of spaces and line breaks count as one space.
  • A regular expression is tested against the normalized text. Only the flags i, m and s are allowed, and lookbehind, backreferences and named groups are not, because the expression runs on the device. Anything else is refused with VALIDATION.
  • getByTestId is the exception: a string test id matches exactly.

A substring match can find more than you meant. In the example app a stepper's button is named "Quantity: 1, Increment", which also contains "Increment", so the counter's button is matched exactly:

TypeScript
await screen.getByRole('button', { name: 'Increment', exact: true }).tap()

Roles

getByRole takes one of these roles: alert, alertdialog, application, button, checkbox, combobox, dialog, document, generic, group, heading, img, keyboard, link, list, listitem, menu, menuitem, progressbar, radio, radiogroup, searchbox, slider, spinbutton, status, switch, tab, tablist, text, textbox and toolbar. They are ARIA's names, with keyboard and text added. Each platform maps its native elements to them:

RoleiOSAndroid
buttonA button or key, or any element with the button traitButton, ImageButton, FloatingActionButton
headingThe header traitisHeading()
textboxA text field, secure text field or text viewEditText, or any editable view
textStatic textA TextView that is not editable
switchA switch or toggleSwitch, SwitchCompat, SwitchMaterial, ToggleButton
list, listitemA table, collection view or outline; a cellRecyclerView, ListView, GridView; an item of one
tab, tablistA tab, or a button inside a tab bar; the tab barA tab view; TabLayout, TabWidget
alertdialogAn alertAn AlertDialog window
imgAn image or iconImageView
linkA link, or the link traitA ClickableSpan, or a role description of Link

getByRole options narrow by state as well as by name:

OptionTypeMatches elements that
namestring or RegExphave this accessible name
exactbooleanmatch a string name exactly
checkedboolean or 'mixed'are checked, unchecked or mixed
disabled, expanded, selected, pressedbooleanare in that state
levelnumberare a heading of that level
includeHiddenbooleanmay have no area on screen; without it such elements are skipped

Narrowing a locator

Locators chain. Each step searches inside the elements the step before it found.

TypeScript
const row = screen.getByRole('listitem').filter({ hasText: 'Oat milk' })
await row.getByRole('button', { name: 'Remove' }).tap()
MethodKeeps
locator.getByRole(…) and the other fourMatching descendants of each element
filter({ hasText }), filter({ hasNotText })Elements whose text, with their descendants' text, does or does not match
filter({ has }), filter({ hasNot })Elements that do or do not contain a match of another locator
filter({ visible: true })Visible elements only
nth(index), first(), last()One element by position, from 0; a negative index counts from the end
and(other)Elements that match both locators
or(other)Elements that match either locator
describe(text)The same elements, with a label that traces and reports show

One element, or the action fails

Actions and assertions are strict: the locator must match exactly one element. When it matches more, the call fails at once with LOCATOR_STRICT_VIOLATION, which lists the matches. It is not retried. Narrow the locator, or pick one element with first(), last() or nth().

When it matches nothing, an action waits for the element until its timeout and then fails with LOCATOR_NOT_FOUND. Counting is not strict: count(), all() and toHaveCount() accept any number of matches.

Matching ignores visibility, so an element that is in the tree but off-screen or hidden still matches. That is what lets toBeHidden() and waitFor({ state: 'hidden' }) work. Actions wait for the element to become visible before they act.

System UI and other apps

screen looks only at the foreground app. Other scopes reach further:

TypeScript
// System UI: permission prompts, system alerts, the status bar.
await screen.system.getByRole('button', { name: 'Allow' }).tap()

// The app and the system UI together.
const anywhere = screen.locator({ root: 'all' })
await expect(anywhere.getByText('Delete item?')).toBeVisible()
ScopeLooks at
screenThe foreground app
screen.systemSystem UI and system alerts
screen.locator({ root: 'all' })The app's tree and the system's, in that order
screen.locator({ bundleId })One running app by its bundle id, instead of the foreground one

A system button's name depends on the prompt and the device's language. For permission prompts, granting the permission ahead with the permissions option avoids the prompt altogether. The system scope needs the ui.systemScope capability, which iOS Simulator and Android Emulator declare and Mac Catalyst does not. An alert your own app shows is part of the app: find it with screen.

Reading an element

These methods read the element once and return a value. Prefer an assertion when you want to check something, because an assertion retries.

MethodReturns
count()How many elements match
all()One locator per match
isVisible(), isHidden()Whether the element is visible now, without waiting
isEnabled(), isDisabled(), isChecked(), isFocused()The element's state, after waiting for it to exist
textContent()The element's own text, or its accessible name when it has no text
inputValue()A text input's current value
accessibleName()The accessible name
boundingBox()The frame in points: { x, y, width, height }
node()The whole accessibility node, or undefined when nothing matches
ariaSnapshot()The element's subtree as text
screenshot()A picture of the element as bytes: PNG, or JPEG with { format: 'jpeg' }

screen.ariaSnapshot() returns the whole screen as text, one line per element. It is the quickest way to see what locators can find:

Output
- application "Shop":
  - toolbar "Cart":
    - button "Back"
    - heading "Cart" [level=1]
  - textbox "Email" [testId="email"]: "a@b.co"
  - switch "Remember me" [checked]
  - text: "Total: 12.00"

Next

esc

    Type to search every page, the protocol reference and the cloud API.