Skip to content

Locators

In OrbitTest, the default way to target an element is the text a user would use to find it. No selector syntax at all:

await orbit.click("Login");
await orbit.type("Email", "user@example.com");
await orbit.hover("Menu");

For type(), OrbitTest resolves the field by its label, placeholder, name attribute, or accessible text, in other words, by all the clues a user has. This keeps tests aligned with behavior: a refactor that changes class names cannot break them, but a real change to the user-facing label will, and should.

Sometimes text is ambiguous: three “Edit” buttons on one screen, or an icon button with no text at all. That is when you reach for an explicit locator:

LocatorExampleBest for
Roleorbit.getByRole(“button”, “Login”)Accessible elements; survives markup changes; also tests accessibility.
Attributeorbit.getByAttribute(“data-testid”, “submit”)Stable hooks the team controls, like data-testid.
CSSorbit.css(“#login-btn”)When you know the markup and need precision.
XPathorbit.xpath(“//button[text()=‘Login’]“)Structural queries CSS cannot express.
Object literal{ css: “#login” } or { role: “button”, name: “Login” }Building locators dynamically in helpers.
await orbit.click(orbit.getByRole("button", "Login"));
await orbit.type(orbit.getByAttribute("name", "email"), "user@example.com");
await orbit.click(orbit.css("#login"));
await orbit.waitFor(orbit.xpath("//h1[contains(text(), 'Dashboard')]"));

Explicit locators in action.

When plain text is not enough, work down this list and stop at the first one that fits:

  1. Role + name (getByRole("button", "Submit")): closest to user intent, and doubles as a basic accessibility check.
  2. A test ID (getByAttribute("data-testid", "submit-form")): completely stable, because the team owns the attribute. Worth adding to the product code when a screen is automation-heavy.
  3. A semantic attribute that already exists: name, aria-label, title.
  4. CSS, scoped as tightly as possible.
  5. XPath, as the last resort.

The locators to avoid are the ones tied to layout: div > div:nth-child(3) > span, generated class names like .css-1x2y3z, or anything that encodes how deep an element happens to sit in the DOM today. These fail during harmless refactors and train your team to ignore red builds.

If a text target matches several elements, make the locator more specific rather than hoping the first match is right. Role narrowing usually resolves it:

// Ambiguous: "Delete" appears on a button and in a heading
await orbit.click("Delete");
// Specific: only the button
await orbit.click(orbit.getByRole("button", "Delete"));

When you need all the matches, not one, use orbit.all(); that workflow has its own page: Working with Elements.

Match the visible text as the user sees it. If you are unsure what the page really renders, read it back with orbit.text() or orbit.visibleText() and assert on that.

Should I ask developers to add data-testid attributes?

Section titled “Should I ask developers to add data-testid attributes?”

For screens with heavy automation, yes, it is a small change that buys total selector stability. But try role and label locators first; if those work, you need nothing from the markup.

Do explicit locators work everywhere text does?

Section titled “Do explicit locators work everywhere text does?”

Yes. Anything that accepts a text target (click, type, waitFor, exists, text readers) also accepts any locator object.

  • Working with Elements — Clicking, typing, reading text with three text readers, waiting for state, and iterating collections with orbit.all().
  • Frames & Shadow DOM — Scope tests into iframes and open or closed shadow roots with orbit.frame() and orbit.shadow(), including nested paths.
  • Test Basics — How to structure readable end-to-end tests: arrange-act-assert, the orbit object, per-test options, and strong assertions.
  • Visual Automation — Test canvas, WebGL, and pixel-rendered UIs with coordinate actions, snapshots, pixel assertions, and color search.