Skip to content

Locator resolution

Every target you write in a test ("Sign in button", "email field", "user avatar") is plain English. At run time, the locator engine turns that into a Playwright selector that picks one specific DOM element. This page explains how.

A target is matched the way a person reads it: by the name an element shows or announces, as whole words. A target is never matched inside a longer word — "To" does not match a Toggle light or dark mode button, "Name" does not match a Username field, "Date" does not match Last update. Case, spacing and punctuation are forgiven ("Email" matches a field labelled Email:). Scripts written without spaces between words — Chinese, Japanese, Thai — are the exception and match inside a sentence. This is the same rule verifyText uses.

A name is what a person or a screen reader would use: the element’s text, its <label>, aria-label, title, placeholder or alt. An icon-only button with aria-label="Close" is reachable as "Close".

Typing, clearing and choosing (type, clear_text, select_option) always look for a field — never a button or a heading that shares the words — in this order, and the first that finds one wins:

  1. a field whose label is exactly the target;
  2. a field whose placeholder is exactly the target;
  3. a text box, combo box or number field whose accessible name is exactly the target;
  4. a field one of whose names reads as the target, by the whole-word rule.

Only visible fields that can take the input count. A target with a trailing noun — "Email field" — is tried again as "Email".

Clicking looks inside an open dialog, menu or popover before the rest of the page, then for a control whose name is the target — its role’s name, or an aria-label or title that is exactly the target, so an icon-only control is found before a paragraph that mentions the word — then for text and names that read as the target by the whole-word rule. The first visible, enabled element that nothing covers wins.

If nothing reads as the target, the step fails — it does not settle for an element whose name merely contains the words.

elementTypeWhat it matchesFallback if no role hit
button<button>, role="button"Clickable <div> / <a> containing the text
link<a>, role="link"Any anchor-like element
input<input>, <textarea>, role="textbox", role="searchbox"Visible inputs near a matching label
checkbox<input type="checkbox">, role="checkbox"Compound locator: label + nearby checkbox
radio<input type="radio">, role="radio"Same pattern as checkbox
select<select>, role="combobox"Native and ARIA-combobox patterns
textAny element whose text matchesHeadings (h1-h6), paragraphs, labels

The exact resolution lives in backend/app/services/execution/locator_builder.py and contextual_locator.py — those are the source of truth if you ever need to dig deeper.

Traditional tests pin to CSS selectors (.btn-primary.large) or data-test-ids ([data-testid="submit"]). Both break the moment someone touches the DOM or the markup conventions.

Marriska’s locators pin to what the user sees: a button labeled “Sign in” stays “Sign in” across CSS refactors, design-system swaps, and even framework migrations. As long as the visible label hasn’t changed, the test still finds the element.

The cost: when labels change, tests do break. That’s a feature — “Submit” → “Pay now” is a behavioural change worth re-confirming the test for.