This documentation is available as Markdown. For the complete index, see llms.txt. Skip to content

Stable selectors

For the complete documentation index, see 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.
// 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:

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

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

The linter enforces it

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

Terminal window
npx @unotest/web lint