Skip to content

Commit aed9f0e

Browse files
DavertMikDavertMikclaude
authored
feat: add elementIndex step option for targeting specific elements (#5499)
* feat: add elementIndex step option for targeting specific elements When multiple elements match a locator, users can now specify which one to interact with using step.opts({ elementIndex }). Supports positive (1-based), negative (-1 = last), and 'first'/'last' aliases. Silently ignored when only one element matches. Overrides strict mode when set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add TypeScript types for step options and codeceptjs/steps module Add StepOptions typedef with elementIndex and ignoreCase in JSDoc. Add declare module for 'codeceptjs/steps' for IDE autocompletion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use absolute XPath with // prefix in MultipleElementsFound error toAbsoluteXPath() now returns //html/... instead of /html/... to match standard absolute XPath notation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: rename strict.md to element-selection.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use duck-typing for StepConfig detection instead of instanceof instanceof fails when StepConfig is loaded from different module paths (e.g., symlinked packages). Add __isStepConfig marker and static isStepConfig() method for reliable detection across module boundaries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add exact and strictMode step options for per-step strict mode Enable strict mode on individual steps without changing helper config: step.opts({ exact: true }) // Playwright-compatible naming step.opts({ strictMode: true }) // alias Throws MultipleElementsFound when multiple elements match, even with strict: false in helper config. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: exact: false cancels strict mode per-step When helper has strict: true, step.opts({ exact: false }) overrides it for that step, allowing multiple element matches without error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: role locators now use getByRole() when wrapped in Locator object handleRoleLocator used isRoleLocatorObject() which rejected Locator-wrapped role objects (checking !locator.type). This caused findClickable to fall through to a CSS [role="button"] selector, losing text/exact filters. Now uses new Locator(locator).isRole() to detect role locators regardless of whether they arrive as raw objects or Locator instances, ensuring Playwright's native getByRole() API is always used. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use Array.from() for WebDriver element collections in selectElement WebDriver returns element collections that aren't plain arrays, so .map() may not work correctly. Matches the pattern used in WebDriver's own assertOnlyOneElement. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add exact/strictMode per-step options to element selection guide Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: DavertMik <davert@testomat.io> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b6aed85 commit aed9f0e

File tree

11 files changed

+422
-123
lines changed

11 files changed

+422
-123
lines changed

docs/element-selection.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
permalink: /element-selection
3+
title: Element Selection
4+
---
5+
6+
# Element Selection
7+
8+
When you write `I.click('a')` and there are multiple links on a page, CodeceptJS clicks the **first** one it finds. Most of the time this is exactly what you need — your locators are specific enough that there's only one match, or the first match happens to be the right one.
9+
10+
But what happens when it's not?
11+
12+
## Picking a Specific Element
13+
14+
Say you have a list of items and you want to click the second one. You could write a more specific CSS selector, but sometimes the simplest approach is to tell CodeceptJS which element you want by position:
15+
16+
```js
17+
import step from 'codeceptjs/steps'
18+
19+
// click the 2nd link
20+
I.click('a', step.opts({ elementIndex: 2 }))
21+
22+
// click the last link
23+
I.click('a', step.opts({ elementIndex: 'last' }))
24+
25+
// fill the last matching input
26+
I.fillField('.email-input', 'test@example.com', step.opts({ elementIndex: -1 }))
27+
```
28+
29+
The `elementIndex` option accepts:
30+
31+
* **Positive numbers** (1-based) — `1` is first, `2` is second, `3` is third
32+
* **Negative numbers**`-1` is last, `-2` is second-to-last
33+
* **`'first'`** and **`'last'`** as readable aliases
34+
35+
This works with any action that targets a single element: `click`, `doubleClick`, `rightClick`, `fillField`, `appendField`, `clearField`, `checkOption`, `selectOption`, `attachFile`, and others.
36+
37+
If only one element matches the locator, `elementIndex` is silently ignored — you always get that single element regardless of the index value. This is convenient when the number of matches depends on page state: you won't get an error if the list happens to have just one item.
38+
39+
When multiple elements exist but the index is out of range, CodeceptJS throws a clear error:
40+
41+
```
42+
elementIndex 100 exceeds the number of elements found (3) for "a"
43+
```
44+
45+
You can combine `elementIndex` with other step options:
46+
47+
```js
48+
I.click('a', step.opts({ elementIndex: 2 }).timeout(5).retry(3))
49+
```
50+
51+
## Strict Mode
52+
53+
If you'd rather not silently click the first of many matches, enable `strict: true` in your helper configuration. This makes CodeceptJS throw an error whenever a locator matches more than one element, forcing you to write precise locators:
54+
55+
```js
56+
// codecept.conf.js
57+
helpers: {
58+
Playwright: {
59+
url: 'http://localhost',
60+
browser: 'chromium',
61+
strict: true,
62+
}
63+
}
64+
```
65+
66+
Now any ambiguous locator will fail immediately:
67+
68+
```js
69+
I.click('a') // MultipleElementsFound: Multiple elements (3) found for "a" in strict mode
70+
```
71+
72+
This is useful on projects where you want to catch accidental matches early — clicking the wrong button because of a vague locator is a common source of flaky tests.
73+
74+
When a test fails in strict mode, the error includes a `fetchDetails()` method that lists the matched elements with their XPath and simplified HTML, so you can see exactly what was found and write a better locator:
75+
76+
```js
77+
// Multiple elements (3) found for "a" in strict mode. Call fetchDetails() for full information.
78+
// After fetchDetails():
79+
// /html/body/div/a[1] <a id="first-link">First</a>
80+
// /html/body/div/a[2] <a id="second-link">Second</a>
81+
// /html/body/div/a[3] <a id="third-link">Third</a>
82+
// Use a more specific locator or grabWebElements() to work with multiple elements
83+
```
84+
85+
Strict mode is supported in **Playwright**, **Puppeteer**, and **WebDriver** helpers.
86+
87+
### Per-Step Strict Mode with `exact`
88+
89+
You don't have to enable strict mode globally. Use `exact: true` to enforce it on a single step — handy when most of your tests are fine with default behavior but a particular action needs to be precise:
90+
91+
```js
92+
import step from 'codeceptjs/steps'
93+
94+
I.click('a', step.opts({ exact: true }))
95+
// throws MultipleElementsFound if more than one link matches
96+
```
97+
98+
`strictMode: true` is an alias if you prefer a more descriptive name:
99+
100+
```js
101+
I.click('a', step.opts({ strictMode: true }))
102+
```
103+
104+
It works the other way too. If your helper has `strict: true` globally but you need to relax it for one step, use `exact: false`:
105+
106+
```js
107+
// strict: true in config, but this step allows multiple matches
108+
I.click('a', step.opts({ exact: false }))
109+
```
110+
111+
And when you know there are multiple matches and want a specific one, `elementIndex` also overrides the strict check — no error is thrown because you've explicitly chosen which element to use:
112+
113+
```js
114+
// strict: true in config, but this works without error
115+
I.click('a', step.opts({ elementIndex: 2 }))
116+
```
117+
118+
## Summary
119+
120+
| Situation | Approach |
121+
|-----------|----------|
122+
| You want to catch ambiguous locators early | Enable `strict: true` in helper config |
123+
| You need a specific element from a known list | Use `step.opts({ elementIndex: N })` |
124+
| You want to iterate over all matching elements | Use [`eachElement`](/els) from the `els` module |
125+
| You need full control over element inspection | Use [`grabWebElements`](/WebElement) to get all matches |

lib/element/WebElement.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ class WebElement {
352352
parts.unshift(`${tagName}${pathIndex}`)
353353
current = current.parentElement
354354
}
355-
return '/' + parts.join('/')
355+
return '//' + parts.join('/')
356356
}
357357

358358
switch (this.helperType) {

lib/els.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import WebElement from './element/WebElement.js'
1010

1111
function element(purpose, locator, fn) {
1212
let stepConfig
13-
if (arguments[arguments.length - 1] instanceof StepConfig) {
13+
if (StepConfig.isStepConfig(arguments[arguments.length - 1])) {
1414
stepConfig = arguments[arguments.length - 1]
1515
}
1616

0 commit comments

Comments
 (0)