diff --git a/docs/guide/browser/locators.md b/docs/guide/browser/locators.md index f30b05ccdd80..b3775b1fc38e 100644 --- a/docs/guide/browser/locators.md +++ b/docs/guide/browser/locators.md @@ -387,6 +387,69 @@ It is recommended to use this only after the other locators don't work for your - [testing-library's `ByTestId`](https://testing-library.com/docs/queries/bytestid/) +## nth + +```ts +function nth(index: number): Locator +``` + +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. + +```html +
+
+``` + +```tsx +page.getByRole('textbox').nth(0) // ✅ +page.getByRole('textbox').nth(4) // ❌ +``` + +::: tip +Before resorting to `nth`, you may find it useful to use chained locators to narrow down your search. +Sometimes there is no better way to distinguish than by element position; although this can lead to flake, it's better than nothing. +::: + +```tsx +page.getByLabel('two').getByRole('input') // ✅ better alternative to page.getByRole('textbox').nth(3) +page.getByLabel('one').getByRole('input') // ❌ too ambiguous +page.getByLabel('one').getByRole('input').nth(1) // ✅ pragmatic compromise +``` + +## first + +```ts +function first(): Locator +``` + +This method returns a new locator that matches only the first index of a multi-element query result. +It is sugar for `nth(0)`. + +```html + +``` + +```tsx +page.getByRole('textbox').first() // ✅ +``` + +## last + +```ts +function last(): Locator +``` + +This method returns a new locator that matches only the last index of a multi-element query result. +It is sugar for `nth(-1)`. + +```html + +``` + +```tsx +page.getByRole('textbox').last() // ✅ +``` + ## Methods All methods are asynchronous and must be awaited. Since Vitest 3, tests will fail if a method is not awaited. diff --git a/packages/browser/src/client/tester/locators/index.ts b/packages/browser/src/client/tester/locators/index.ts index 0ae190d02e17..e699eda87a20 100644 --- a/packages/browser/src/client/tester/locators/index.ts +++ b/packages/browser/src/client/tester/locators/index.ts @@ -185,6 +185,18 @@ export abstract class Locator { return this.elements().map(element => this.elementLocator(element)) } + public nth(index: number): Locator { + return this.locator(`nth=${index}`) + } + + public first(): Locator { + return this.nth(0) + } + + public last(): Locator { + return this.nth(-1) + } + public toString(): string { return this.selector } diff --git a/test/browser/fixtures/locators/blog.test.tsx b/test/browser/fixtures/locators/blog.test.tsx index 991c0eacbf0e..65faef58c9b7 100644 --- a/test/browser/fixtures/locators/blog.test.tsx +++ b/test/browser/fixtures/locators/blog.test.tsx @@ -22,5 +22,10 @@ test('renders blog posts', async () => { expect(screen.getByRole('listitem').all()).toHaveLength(3) + expect(screen.getByRole('listitem').nth(0).element()).toHaveTextContent(/molestiae ut ut quas/) + await expect.element(screen.getByRole('listitem').nth(666)).not.toBeInTheDocument() + expect(screen.getByRole('listitem').first().element()).toHaveTextContent(/molestiae ut ut quas/) + expect(screen.getByRole('listitem').last().element()).toHaveTextContent(/eum et est/) + expect(screen.getByPlaceholder('non-existing').query()).not.toBeInTheDocument() })