Skip to content

Commit 45f3a7c

Browse files
DavertMikclaude
andcommitted
docs(locators): elevate semantic+context as the default pattern
Reframe semantic locators (`I.click('Save', '.header')`) as the recommended default for stable scenarios, not a prototyping shortcut. Combined with a context, they read like prose, survive ARIA/CSS refactors, and disambiguate duplicate labels — so they're more precise than ARIA or CSS used alone. - Intro: lead with the "semantic + context" recipe. - Locator-types table: split semantic into "with context" (default) and "no context" (unique label / prototyping); document the combined pattern as a first-class type. - Semantic section: front-load the "pair with a context" guidance and drop the "switch to strict locators once stable" line. - Context section: explain why scoping every action is the default, not the special case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ccbccbf commit 45f3a7c

1 file changed

Lines changed: 46 additions & 15 deletions

File tree

docs/locators.md

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,29 @@ Locators tell CodeceptJS which element on the page a step acts on. Every action
1010
CodeceptJS accepts locators in two forms:
1111

1212
- **Strict locator** — an object whose single key names the strategy: `{ css: 'button' }`, `{ role: 'button', name: 'Submit' }`, `{ xpath: '//td[1]' }`, `{ id: 'email' }`. The strategy is explicit, so the helper runs exactly one query.
13-
- **Fuzzy locator** — a plain string. CodeceptJS guesses the strategy from shape (`#foo` → id, `//td` → xpath, `.row` → css) and falls back to semantic matching (labels, button text, placeholders). Convenient, but slower and sometimes ambiguous.
13+
- **Semantic locator** — a plain string like `'Sign In'` or `'Email'`. CodeceptJS matches it against labels, button text, placeholders, and `aria-*` attributes the way a user would read the page.
1414

15-
Prefer strict locators in stable test suites. Reach for fuzzy strings when prototyping.
15+
Both are idiomatic. The strongest pattern in CodeceptJS — readable, resilient, and unambiguous — is a **semantic locator scoped to a context**:
16+
17+
```js
18+
I.click('Save', '.header')
19+
I.fillField('Search', 'Item 1', '.topbar')
20+
I.click({ role: 'button', name: 'Submit' }, '#login-form')
21+
```
22+
23+
The context narrows the search to one region of the page, and the semantic string says what the user actually clicks. This is **more precise than ARIA or CSS alone** because it combines structural scope with human-readable intent.
1624

1725
Supported strategies: `css`, `xpath`, `id`, `name`, `role`, `frame`, `shadow`, `pw`. Shadow DOM and React selectors have their own pages — see [Shadow DOM](/shadow) and [React](/react). Playwright-specific locators (`_react`, `_vue`, `data-testid`) use the `pw` strategy: `{ pw: '_react=Button[name="Save"]' }`.
1826

1927
## Locator types at a glance
2028

2129
| Type | Example | Strengths | Weaknesses | Reach for it when |
2230
|------|---------|-----------|------------|-------------------|
31+
| **Semantic + context** | `I.click('Save', '.header')` | Reads like prose; survives CSS and ARIA refactors; the context disambiguates duplicates | Needs a stable region to scope into | **Default for stable suites.** Anywhere a label, button text, or placeholder identifies the element |
2332
| **ARIA role** | `{ role: 'button', name: 'Save' }` | Survives markup changes; matches how users and screen readers identify elements; exposes accessibility gaps | Needs correct ARIA roles and accessible names; slower than CSS | The app follows accessibility guidelines and you want tests that mirror user intent |
33+
| **Semantic (no context)** | `'Sign In'`, `'Email'` | No locator to maintain; reads like prose | Ambiguous when the same label appears more than once on the page | A label is unique on the page, or you are prototyping |
2434
| **CSS** | `{ css: '.btn-save' }` or `.btn-save` | Fast; familiar to every web developer; composes with class, attribute, and pseudo-selectors | Couples tests to styling; breaks on CSS refactors; cannot match by visible text | A stable class, id, or data-attribute exists on the target |
2535
| **XPath** | `{ xpath: '//table//tr[2]/td[last()]' }` | Walks the tree in any direction (`ancestor`, `following-sibling`); matches visible text | Verbose; slow; harder to read than CSS | You need text matching or axis navigation that CSS cannot express |
26-
| **Semantic (fuzzy)** | `'Sign In'`, `'Email'` | No locator to maintain; reads like prose | Several lookups per call; ambiguous when labels repeat | Writing a quick scenario or prototyping |
2736
| **ID / name** | `#email`, `{ name: 'user[email]' }` | Shortest possible locator; unambiguous | Requires an `id` or `name` attribute to exist | Forms and elements with stable ids |
2837
| **Accessibility id** | `~login-button` | Works in both web (`aria-label`) and mobile | Mobile apps need to expose the id | Cross-platform web and mobile tests |
2938
| **Custom (`$foo`)** | `$register_button` | Encodes team convention (`data-qa`, `data-test`) in two characters | Needs the [customLocator plugin](/plugins#customlocator) | Your team uses dedicated test attributes |
@@ -97,26 +106,41 @@ Long XPath expressions become unreadable fast. The [`locate()` builder](#combini
97106

98107
## Semantic locators
99108

100-
When you pass a plain string to a form or click action, CodeceptJS tries several strategies in order: links, buttons, labels, placeholders, `aria-label`.
109+
A plain string is a semantic locator. CodeceptJS reads it the way a user would: as a button label, a link, a field name, a placeholder, or an `aria-label`.
101110

102111
```js
103-
I.click('Sign In') // matches <a>, <button>, or <input type="submit">
104-
I.fillField('Email', 'u@t.com') // matches label, placeholder, or name
112+
I.click('Sign In') // matches <a>, <button>, or <input type="submit">
113+
I.fillField('Email', 'u@t.com') // matches label, placeholder, name, or aria-label
105114
I.checkOption('I accept the terms')
106115
```
107116

108-
The order `fillField` actually uses is:
117+
### Pair semantic locators with a context
109118

110-
1. Is the locator an ARIA role locator (`{ role: 'textbox', name: 'Email' }`)? If so, resolve through the accessibility tree and stop.
111-
2. Is it a strict locator (`{ css: ... }`, `{ xpath: ... }`, `{ id: ... }`, …)? Run it directly and stop.
112-
3. Otherwise treat the string as fuzzy and try, in order:
113-
1. A field whose `name`, `id`+`label[for]`, or `placeholder` **equals** the string — or a `<label>` with that exact text wrapping an input.
119+
The same label often appears in more than one place — a "Save" button in the toolbar, the modal, and the inline editor. **Pass a context as the last argument** and the lookup is unambiguous, fast, and still readable:
120+
121+
```js
122+
I.click('Save', '.toolbar')
123+
I.fillField('Search', 'Item 1', '.topbar')
124+
I.click('Edit', { css: 'tr.acme' })
125+
I.see('Welcome', '.header')
126+
```
127+
128+
The context can be any locator (CSS, XPath, ARIA, [`locate()` chain](#locate-builder-compose-css-and-xpath)). The action runs only inside it, so duplicate labels elsewhere on the page no longer cause flaky matches. This is the recommended default for stable scenarios — production-grade, not a prototyping shortcut.
129+
130+
### How matching works
131+
132+
For `fillField` and similar actions, CodeceptJS resolves the locator in this order:
133+
134+
1. ARIA role locator (`{ role: 'textbox', name: 'Email' }`) — resolved through the accessibility tree.
135+
2. Strict locator (`{ css: ... }`, `{ xpath: ... }`, `{ id: ... }`, …) — run directly.
136+
3. Plain string treated as semantic, tried in order:
137+
1. Field whose `name`, `id`+`label[for]`, or `placeholder` **equals** the string — or a `<label>` with that exact text wrapping an input.
114138
2. The same match with **contains**, extended to `aria-label`, `aria-labelledby`, and `title`.
115139
3. An input with that `name` attribute.
116-
4. Finally, the string as a CSS selector.
140+
4. The string as a CSS selector.
117141
4. Nothing matched? Throw `ElementNotFound`.
118142

119-
So fuzzy `fillField` already covers labels, placeholders, names, ids referenced by labels, and the ARIA attributes (`aria-label`, `aria-labelledby`, `title`) — no extra syntax needed. Each lookup runs several queries, though, so switch to strict locators (`{ role: ... }` or `{ css: ... }`) once a scenario is stable.
143+
A semantic lookup runs several queries, but each query is cheap and the second argument (context) prunes the search space dramatically.
120144

121145
## ID locators
122146

@@ -164,15 +188,22 @@ Two mechanisms narrow a locator to a region of the page:
164188

165189
### Context: scope any locator to a region
166190

167-
Every action that targets an element accepts a context locator as its last argument. The action searches only inside the context.
191+
Every action that targets an element accepts a context locator as its last argument. The action searches only inside the context. **Use it by default** — even a one-line scenario reads better and survives more refactors when the lookup is scoped:
168192

169193
```js
170194
I.click('Login', '#login-form')
171195
I.fillField('Email', 'u@t.com', '.modal')
172196
I.seeElement({ role: 'button', name: 'Delete' }, '.toolbar')
173197
```
174198

175-
Context is the right tool when the locator types differ on each side — for example, an ARIA button inside a CSS-selected container.
199+
Why scope every action:
200+
201+
- Duplicate labels stop being a problem ("Save" in the toolbar vs. the modal).
202+
- The semantic locator stays semantic — no need to rewrite as `[data-testid="save-toolbar"]` to disambiguate.
203+
- The lookup is faster: each strategy queries only inside the context, not the whole DOM.
204+
- Tests read like a sentence about the page: "click Save in the header".
205+
206+
The two sides can be any combination — semantic+CSS, ARIA+CSS, semantic+`locate()`. Mix freely.
176207

177208
**Example: a dropdown inside a top bar**
178209

0 commit comments

Comments
 (0)