Addressing an element
A locator is a string, and it is the whole of how this project addresses an element. The same
string means the same thing to a step in a case file, to an assertion, to the tool that checks
the file before it exists, and to inspect printing a tree — so there is one grammar to learn
rather than one per verb.
Every table here is read out of the parser on every build, and the suite parses each form in the first one — so what this page says is true of the version you are looking at rather than of the day somebody wrote it down.
| Written as | What it addresses |
|---|---|
#saveButton | the automation id |
Button | the control type |
Button#saveButton | both |
Button[name="Save as..."] | the name |
MenuItem[nameStarts="Pessoal "] | the name, where the rest of it is decoration |
Pane[class=Chrome_WidgetWin_1] | the window class |
Button[pattern=Invoke] | it must carry that pattern |
ComboBox|Slider|Edit | any one of several control types |
Text[name="Statistics"][order=left] | the leftmost of the ones that match |
MenuItem[order=top][index=2] | the second from the top |
Text[name="{settings.nav.about}"] | what the project's strings call it |
Group[name="{}"] | the member, in a case that repeats |
Window#main > Pane > Button#save | a descendant of, at any depth |
The control type, and the automation id
Section titled “The control type, and the automation id”A step may say what kind of control it is, what its automation id is, or both. Neither is required on its own; a step that says nothing at all is refused, because it addresses every element there is.
Button a control type#saveButton an automation idButton#saveButton both#"Item 1" an id that is not an identifier, so it is quotedThe id is the field your application controls, and a locator should prefer it: it is the one
thing about an element that no translation and no restyling changes. Quote it where it is not
an identifier — Windows gives a window’s own system menu the id Item 1, and inspect writes
ids back in the spelling the grammar reads.
Control types are spelled as UI Automation spells them, and the list is read off UI Automation
itself rather than kept in this repository — so it cannot drift from the thing it describes,
and this page cannot publish it. A word that is no control type is refused at parse time, with
the three nearest spellings. The same is true of pattern.
The predicates
Section titled “The predicates”Everything else is [key=value], and these are the keys.
| Key | What it matches |
|---|---|
name | The name, which is what a person sees and therefore what a language changes. |
nameStarts | What the name must begin with, where the rest of it is decoration the case cannot know. |
class | The window class, which is what tells one framework's chrome from another's. |
pattern | The pattern the element must carry, which is what makes a step actionable. |
order | The order matches are put in before Index picks one. |
index | Which match, counting from one. |
A value is bare or quoted. A bare one runs to the ] and is trimmed, so it may hold spaces —
[name=Save as] is the name Save as. Quote it where it carries a ] or a quotation mark of
its own. Inside quotes, \n, \r and \t are the characters they name and a backslash before
anything else leaves it as itself, which matters for a notification-area icon: its name is a
tooltip, and a real one runs to several lines.
order is how matches are put in order before index counts them, and it takes one of four
words:
left— Left to right, then top to bottom.right— Right to left, then top to bottom.top— Top to bottom, then left to right.bottom— Bottom to top, then left to right.
index counts from one. A step that matches more than one element and says neither
order nor index is refused rather than answered with whichever the tree happened to walk
first. The refusal lists what it matched, each written as the step that would address it, so
the choice gets made in the file rather than by the tree.
Any one of several control types
Section titled “Any one of several control types”| at the type position means any one of these, and the predicates after it apply to the
whole union:
ComboBox|Slider|Edit[pattern=Value]It is there because a rule under test governs a family of controls as often as it governs one. A settings panel’s rows may be a combo box, a slider and a text box, and what the rule is about is that each of them reads back what was set — excluded by what they are rather than by a list of ids somebody keeps current. Written as one step per type, most of the steps match nothing on any given panel, each of those is a hole, and the run is a page of holes.
A type named twice in one union is refused. That is a step written twice rather than a wider set, and the reader of one is looking for a difference between the halves that is not there.
A descendant of, at any depth
Section titled “A descendant of, at any depth”Window#main > Pane > Button#save> is a descendant of, not a direct child, and that is a decision rather than a shorthand:
UI Automation wraps controls in panes that differ between frameworks, between versions of one
framework, and between a maximised window and a restored one. A direct-child locator is the one
that breaks on somebody else’s machine. Whitespace around the operator is optional.
A brace is a hole the run fills
Section titled “A brace is a hole the run fills”Text[name="{settings.nav.about}"] what your project's strings call itGroup[name="{}"] the member, in a case that repeatsA locator naming an element in words is a hardcoded set at its smallest: it goes stale the day
somebody edits the strings file, and it is wrong in every language the application ships but
one from the moment it is written. {a.key} is read out of the languageFiles your project
declares, in the language the fixture says the window is in, so a case addressing an element by
the words on it stops being a case about one language. {} is the member of the set a
repeating case walks.
The grammar itself does not resolve either, and that is deliberate — what a key declares is a property of your project, and a grammar that resolved it could not be read without it. A locator with a brace in it parses as the literal text; the run substitutes and re-parses before the first act, and the trace records the substituted locator, because the words the run actually looked for are what a red is about.
A key none of your languageFiles declares is refused before the first act, naming the key and
the file. A brace that would leave the locator unparseable is refused at declaration.
What it refuses
Section titled “What it refuses”Half of what a grammar is worth is in what it will not parse. A refusal carries the position as well as the reason, because one saying only bad locator sends you back to count characters — and the text came out of a case file, where the next thing you will do is find the column.
Button[name=Save ^ this predicate is not closedThese are the ways a locator can fail to parse. Each is its own arm of LocatorFault, so a
caller collecting refusals can tell them apart:
| Refusal | What it is about |
|---|---|
StepNotSeparated | Two steps with something between them that is not the descendant operator. |
UnknownControlType | A word in the control-type position that UI Automation has no such type for. |
EmptyAutomationId | A # introducing an automation id, with no id after it. |
PredicateMalformed | A predicate that does not read [key=value]. |
PredicateNotClosed | A predicate whose closing bracket never arrives. |
QuoteNotClosed | A quoted value whose closing quote never arrives. |
UnknownKey | A key the grammar does not have. |
KeyClaimedTwice | One key claimed twice in one step, which is two claims. |
UnknownPattern | A pattern name UI Automation has no such pattern for. |
UnknownOrder | An order that is none of left, right, top or bottom. |
IndexNotANumber | An index that is not a whole number. |
IndexBelowOne | An index below one, which addresses nothing. |
StepConstrainsNothing | A step that constrains nothing, and so addresses everything. |
Locator.Parse throws a LocatorSyntaxException carrying the arm, the position and the
sentence. Locator.TryParse answers without throwing, for a caller collecting refusals rather
than stopping at the first.
Reading one back before the file exists
Section titled “Reading one back before the file exists”A locator is parsed when the case is loaded, not when the step runs — one that does not parse is wrong on every machine, and the reader of a red about one is opening the wrong repository. So the refusal names the step and what is wrong with it, before anything is launched.
If you drive your application from Claude Code, winwright_check gets you that answer before
the file exists. inspect is the other direction: it prints a tree with a locator per element,
written in this grammar and quoted so that a line it printed can be pasted into a case.
Where to go next
Section titled “Where to go next”- Your first case — the file these locators go in, and the four calls that run it.
- What this is — the verdicts, and what a hole is.
- The README — every verb a step may name, every field a case may carry, and the reasoning behind each refusal.