# Locators

> Find elements by test id, role, label, text, placeholder or CSS, and narrow a match down.

Source: https://rehearsal.dev/retest/essentials/locators

A locator is a recipe, not an element. Retest finds the element again for every action and every check. A page that redraws itself does not leave a test holding a stale element.

## Find an element

| Locator | Finds |
| --- | --- |
| `getByTestId(id)` | Elements whose `data-testid` equals `id` exactly |
| `getByRole(role, { name, exact })` | Elements with this ARIA role and, when given, this accessible name |
| `getByLabel(text, { exact })` | Form controls whose accessible name matches: text boxes, search boxes, combo boxes, list boxes, checkboxes, radios, switches, sliders and spin buttons |
| `getByText(text, { exact })` | The innermost elements whose text matches |
| `getByPlaceholder(text, { exact })` | Elements whose `placeholder` attribute matches |
| `locator(selector)` | Elements a CSS selector matches, as `querySelectorAll` matches it |

```ts
await page.getByTestId('task-title').fill('Release checklist')
await page.getByRole('button', { name: 'Save' }).click()
await page.getByLabel('Password').fill(secret('password'))
await expect(page.getByText('Signed in as alice')).toBeVisible()
```

## How text matches

- Both sides are trimmed, and each run of spaces or line breaks reads as one space.
- `exact` is true by default: the whole string, case and all. `name: 'Save'` never matches "Save draft" or "save".
- `exact: false` matches any part, in any case. `name: 'save', exact: false` matches "Save", "save" and "Save draft".
- A regular expression is searched for anywhere in the text, with its own flags, as in `getByText(/^Saved \d+ tasks$/)`. It takes no `exact`.

## Where names come from

`getByRole` and `getByLabel` read the accessibility tree the browser computes. A name comes from `aria-label`, `aria-labelledby`, a `<label>`, a `title`, a placeholder or the content, as the browser decides.

- Elements the browser leaves out of that tree are not found: `aria-hidden`, `display: none`, `hidden`, `visibility: hidden` and `inert`.
- `getByText` reads the text in the page and skips `script`, `style`, `template` and `noscript`. It finds hidden elements too, and reports them as not visible.
- Chrome names a table row or a list item only from `aria-label` or `aria-labelledby`. Find a row by its place, as in `getByRole('row').nth(1)`, or by a test id.

## Narrow a match

Every finder is also a method of a locator. It looks inside the elements that locator found, never at those elements themselves.

```ts
page.getByTestId('inbox').getByRole('button', { name: 'Delete' })
page.locator('li').getByRole('button')
page.getByRole('listitem').nth(1).getByRole('button')
```

- `first()`, `last()` and `nth(index)` keep one match, by its place in the document. `nth` counts from 0, and from the end when negative, so `nth(-1)` is the last.
- A locator chooses once: `first().nth(1)` is refused. Choose, then look inside.
- An index past the matches keeps nothing, and an action waits as it does for no match.

## One match per action

An action needs exactly one element. When nothing matches, it waits until its time runs out, then fails `not_found`. When several match, it fails at once as `ambiguous` and acts on none of them.

A failure names the step that kept nothing, such as "getByRole('listitem') matched 4 elements, and nth(9) keeps none of them."

## CSS

- `locator(selector)` takes CSS only. XPath and Playwright's selector engines, such as `text=Save` or `div >> span`, are refused before anything is sent.
- A selector the browser cannot read, such as one with `:has-text()`, fails at once with the browser's reason.
- Inside a locator, a selector matches as that element's own `querySelectorAll` does.

## Not supported yet

Locators search the top-level document only. They do not look into shadow DOM or frames. There is no `filter()`, `and()`, `or()`, `getByAltText()` or `getByTitle()` yet, and no XPath.

> [!NOTE]
> Native apps take a smaller set of locators. [Mobile apps](https://rehearsal.dev/retest/platforms/mobile.md) lists them.
