Skip to content

Commit 38458ea

Browse files
xegersheremet-va
andauthored
feat(browser): implement locator.nth() (#7137)
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
1 parent 1d45895 commit 38458ea

File tree

3 files changed

+80
-0
lines changed

3 files changed

+80
-0
lines changed

docs/guide/browser/locators.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,69 @@ It is recommended to use this only after the other locators don't work for your
387387

388388
- [testing-library's `ByTestId`](https://testing-library.com/docs/queries/bytestid/)
389389

390+
## nth
391+
392+
```ts
393+
function nth(index: number): Locator
394+
```
395+
396+
This method returns a new locator that matches only a specific index within a multi-element query result. Unlike `elements()[n]`, the `nth` locator will be retried until the element is present.
397+
398+
```html
399+
<div aria-label="one"><input/><input/><input/></div>
400+
<div aria-label="two"><input/></div>
401+
```
402+
403+
```tsx
404+
page.getByRole('textbox').nth(0) // ✅
405+
page.getByRole('textbox').nth(4) // ❌
406+
```
407+
408+
::: tip
409+
Before resorting to `nth`, you may find it useful to use chained locators to narrow down your search.
410+
Sometimes there is no better way to distinguish than by element position; although this can lead to flake, it's better than nothing.
411+
:::
412+
413+
```tsx
414+
page.getByLabel('two').getByRole('input') // ✅ better alternative to page.getByRole('textbox').nth(3)
415+
page.getByLabel('one').getByRole('input') // ❌ too ambiguous
416+
page.getByLabel('one').getByRole('input').nth(1) // ✅ pragmatic compromise
417+
```
418+
419+
## first
420+
421+
```ts
422+
function first(): Locator
423+
```
424+
425+
This method returns a new locator that matches only the first index of a multi-element query result.
426+
It is sugar for `nth(0)`.
427+
428+
```html
429+
<input/> <input/> <input/>
430+
```
431+
432+
```tsx
433+
page.getByRole('textbox').first() // ✅
434+
```
435+
436+
## last
437+
438+
```ts
439+
function last(): Locator
440+
```
441+
442+
This method returns a new locator that matches only the last index of a multi-element query result.
443+
It is sugar for `nth(-1)`.
444+
445+
```html
446+
<input/> <input/> <input/>
447+
```
448+
449+
```tsx
450+
page.getByRole('textbox').last() // ✅
451+
```
452+
390453
## Methods
391454

392455
All methods are asynchronous and must be awaited. Since Vitest 3, tests will fail if a method is not awaited.

packages/browser/src/client/tester/locators/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,18 @@ export abstract class Locator {
185185
return this.elements().map(element => this.elementLocator(element))
186186
}
187187

188+
public nth(index: number): Locator {
189+
return this.locator(`nth=${index}`)
190+
}
191+
192+
public first(): Locator {
193+
return this.nth(0)
194+
}
195+
196+
public last(): Locator {
197+
return this.nth(-1)
198+
}
199+
188200
public toString(): string {
189201
return this.selector
190202
}

test/browser/fixtures/locators/blog.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,10 @@ test('renders blog posts', async () => {
2222

2323
expect(screen.getByRole('listitem').all()).toHaveLength(3)
2424

25+
expect(screen.getByRole('listitem').nth(0).element()).toHaveTextContent(/molestiae ut ut quas/)
26+
await expect.element(screen.getByRole('listitem').nth(666)).not.toBeInTheDocument()
27+
expect(screen.getByRole('listitem').first().element()).toHaveTextContent(/molestiae ut ut quas/)
28+
expect(screen.getByRole('listitem').last().element()).toHaveTextContent(/eum et est/)
29+
2530
expect(screen.getByPlaceholder('non-existing').query()).not.toBeInTheDocument()
2631
})

0 commit comments

Comments
 (0)