# Stable selectors

> For the complete documentation index, see [llms.txt](/llms.txt)

Tests break when they target markup that changes. unotest steers every locator
toward **meaning** over **structure**, so tests survive refactors.

## The priority

1. **`getByTestId(id)`** — an explicit `data-testid`. Most stable. Prefer it.
2. **`getByRole(role, { name })`** — semantic role + accessible name. Stable when
   the name is unique.
3. **`getByLabel(text)`** — form controls by their label.
4. **`getByText(text)`** — unique visible text.
5. **`locator(css)`** — raw CSS. Last resort.

```js
// good — resilient
click(getByRole("button", { name: "Save changes" }));
fill(getByLabel("Email"), TEST_USER_EMAIL);

// avoid — brittle
click(locator(".btn.btn-primary.css-1a2b3c"));
```

## Refine, don't index blindly

Narrow with `filter()` before reaching for position:

```js
click(getByRole("row").filter({ hasText: "Uma Quinn" }).first());
```

`nth()` / index-only refinement is fragile — the linter flags it.

## The linter enforces it

The [linter](/reference/linter/) warns on deep CSS, XPath, hashed class names
(Tailwind JIT, CSS Modules), and unexplained `pause()`. Run it anytime:

```sh
npx @unotest/web lint
```
