Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions docs/element-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
permalink: /element-selection
title: Element Selection
---

# Element Selection

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.

But what happens when it's not?

## Picking a Specific Element

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:

```js
import step from 'codeceptjs/steps'

// click the 2nd link
I.click('a', step.opts({ elementIndex: 2 }))

// click the last link
I.click('a', step.opts({ elementIndex: 'last' }))

// fill the last matching input
I.fillField('.email-input', 'test@example.com', step.opts({ elementIndex: -1 }))
```

The `elementIndex` option accepts:

* **Positive numbers** (1-based) — `1` is first, `2` is second, `3` is third
* **Negative numbers** — `-1` is last, `-2` is second-to-last
* **`'first'`** and **`'last'`** as readable aliases

This works with any action that targets a single element: `click`, `doubleClick`, `rightClick`, `fillField`, `appendField`, `clearField`, `checkOption`, `selectOption`, `attachFile`, and others.

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.

When multiple elements exist but the index is out of range, CodeceptJS throws a clear error:

```
elementIndex 100 exceeds the number of elements found (3) for "a"
```

You can combine `elementIndex` with other step options:

```js
I.click('a', step.opts({ elementIndex: 2 }).timeout(5).retry(3))
```

## Strict Mode

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:

```js
// codecept.conf.js
helpers: {
Playwright: {
url: 'http://localhost',
browser: 'chromium',
strict: true,
}
}
```

Now any ambiguous locator will fail immediately:

```js
I.click('a') // MultipleElementsFound: Multiple elements (3) found for "a" in strict mode
```

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.

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:

```js
// Multiple elements (3) found for "a" in strict mode. Call fetchDetails() for full information.
// After fetchDetails():
// /html/body/div/a[1] <a id="first-link">First</a>
// /html/body/div/a[2] <a id="second-link">Second</a>
// /html/body/div/a[3] <a id="third-link">Third</a>
// Use a more specific locator or grabWebElements() to work with multiple elements
```

Strict mode is supported in **Playwright**, **Puppeteer**, and **WebDriver** helpers.

### Per-Step Strict Mode with `exact`

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:

```js
import step from 'codeceptjs/steps'

I.click('a', step.opts({ exact: true }))
// throws MultipleElementsFound if more than one link matches
```

`strictMode: true` is an alias if you prefer a more descriptive name:

```js
I.click('a', step.opts({ strictMode: true }))
```

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`:

```js
// strict: true in config, but this step allows multiple matches
I.click('a', step.opts({ exact: false }))
```

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:

```js
// strict: true in config, but this works without error
I.click('a', step.opts({ elementIndex: 2 }))
```

## Summary

| Situation | Approach |
|-----------|----------|
| You want to catch ambiguous locators early | Enable `strict: true` in helper config |
| You need a specific element from a known list | Use `step.opts({ elementIndex: N })` |
| You want to iterate over all matching elements | Use [`eachElement`](/els) from the `els` module |
| You need full control over element inspection | Use [`grabWebElements`](/WebElement) to get all matches |
2 changes: 1 addition & 1 deletion lib/element/WebElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ class WebElement {
parts.unshift(`${tagName}${pathIndex}`)
current = current.parentElement
}
return '/' + parts.join('/')
return '//' + parts.join('/')
}

switch (this.helperType) {
Expand Down
2 changes: 1 addition & 1 deletion lib/els.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isAsyncFunction, humanizeFunction } from './utils.js'

function element(purpose, locator, fn) {
let stepConfig
if (arguments[arguments.length - 1] instanceof StepConfig) {
if (StepConfig.isStepConfig(arguments[arguments.length - 1])) {
stepConfig = arguments[arguments.length - 1]
}

Expand Down
Loading