Locators
A small grammar, deliberately biased toward how a person describes a control rather than where it sits in the DOM. Get these right and your spec survives a redesign.
The forms#
| Form | Matches |
|---|---|
role=button[name="Save"] | An ARIA role with an accessible name. The default and the most durable. |
label=Email | A form control by its <label>, aria-label or aria-labelledby. |
text="Total revenue" | Any element by its visible text. |
placeholder=Search… | An input by its placeholder. |
testid=row-3 | A data-testid, data-test-id or data-test attribute. |
css=.pipeline > .card:first-child | A raw CSS selector — the escape hatch. |
testid covers the three common spellings. data-testid, data-test-id and data-test all resolve. Cypress-style data-cy still needs css=[data-cy="row-3"].Exact, substring, or regex#
The quoting is the matching rule, and it is easy to miss:
| Written as | Means |
|---|---|
text="Save changes" | Exact — quoted, whitespace-normalised, whole string. |
text=Save | Substring — bare, case-insensitive. Matches “Save changes” too. |
text=/^Save/i | Regex — delimited by slashes, with flags. |
The same applies to a role’s name: role=button[name="Save"] is exact, role=button[name=Save] is a substring match.
When more than one element matches#
role=button[name="Delete"] >> nth=2 # zero-basedPrefer narrowing before counting: a nearby unique container plus a CSS chain usually reads better in six months than an index into a list whose order may change.
Iframes#
frame=#checkout >> role=button[name="Pay"]Chain frame=SELECTOR >> to descend into a frame. Same-origin frames are pierced one level deep. Cross-origin iframes cannot be explored, and therefore cannot be addressed — Playhead is honest about that rather than half-working.
Shadow DOM#
Open shadow roots are pierced by the same engines capture uses, so a component-library app is as addressable as a plain one. Closed shadow roots are invisible to explore and to capture, which means some apps genuinely cannot be filmed.
aria-hidden subtrees — so a decorative icon inside a link never becomes part of its name ("Contacts", not "☰Contacts"). A locator that explore prints but capture cannot resolve is a bug in Playhead, not in your spec. Please report it.Choosing well#
- Reach for
role=first. It is what a screen reader sees, so it is stable across visual redesigns and doubles as a light accessibility audit — if nothing has an accessible name, that is worth knowing. label=for form fields. More durable than a placeholder, which designers change freely.text=for content you are asserting on rather than a control you are operating.testid=when the DOM is hostile — generated class names, no accessible names, canvas toolbars.css=last. It works, and it is the first thing to break.
Do not write these by hand#
playhead explore prints every addressable element on the page as a ready-to-paste locator, ranked for stability, and playhead validate checks every locator in a spec against the live DOM with suggestions for the ones that miss. Authoring workflow →