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.
The pipeline
Section titled “The pipeline”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:
- a field whose label is exactly the target;
- a field whose placeholder is exactly the target;
- a text box, combo box or number field whose accessible name is exactly the target;
- 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.
Element types in detail
Section titled “Element types in detail”elementType | What it matches | Fallback 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 |
text | Any element whose text matches | Headings (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.
Why this matters for test stability
Section titled “Why this matters for test stability”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.
See also
Section titled “See also”- Step actions reference — every action and how it uses
target - Test YAML schema — the full set of fields a step can have
- How Marriska works — the broader pipeline this fits inside