From 6c3db9ac222efbf4bd380046ac8c341437664b9b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 23 Sep 2026 10:33:04 -0700 Subject: [PATCH] feat(locator): add locator.within() Fixes https://github.com/microsoft/playwright/issues/42842 --- docs/src/api/class-locator.md | 41 +++++++++++++++++++ packages/playwright-client/types/types.d.ts | 25 +++++++++++ .../playwright-core/src/client/locator.ts | 4 ++ packages/playwright-core/types/types.d.ts | 25 +++++++++++ tests/page/locator-query.spec.ts | 18 ++++++++ 5 files changed, 113 insertions(+) diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index cf6ac55c65b55..22f5dfbee7a2e 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -3056,3 +3056,44 @@ Optional argument to pass to [`param: expression`]. ### option: Locator.waitForFunction.signal = %%-input-signal-%% * since: v1.62 + +## method: Locator.within +* since: v1.64 +- returns: <[Locator]> + +Returns a locator that matches this locator's elements inside each element matched by [`param: locator`]. This is the same as calling [`method: Locator.locator`] on [`param: locator`] with this locator as an argument, but reads in the natural order. + +Note that relative locators, such as [`method: Locator.nth`] or [`method: Locator.first`], are resolved separately inside each matched parent. In the example below, `page.getByRole('cell').nth(2)` picks the third cell of every row, not the third cell in the whole table. + +**Usage** + +```js +const thirdColumn = page.getByRole('cell').nth(2).within(page.getByRole('row')); +await expect(thirdColumn).toHaveText(['Apple', 'Banana', 'Cherry']); +``` + +```python async +third_column = page.get_by_role("cell").nth(2).within(page.get_by_role("row")) +await expect(third_column).to_have_text(["Apple", "Banana", "Cherry"]) +``` + +```python sync +third_column = page.get_by_role("cell").nth(2).within(page.get_by_role("row")) +expect(third_column).to_have_text(["Apple", "Banana", "Cherry"]) +``` + +```java +Locator thirdColumn = page.getByRole(AriaRole.CELL).nth(2).within(page.getByRole(AriaRole.ROW)); +assertThat(thirdColumn).hasText(new String[] {"Apple", "Banana", "Cherry"}); +``` + +```csharp +var thirdColumn = page.GetByRole(AriaRole.Cell).Nth(2).Within(page.GetByRole(AriaRole.Row)); +await Expect(thirdColumn).ToHaveTextAsync(new[] { "Apple", "Banana", "Cherry" }); +``` + +### param: Locator.within.locator +* since: v1.64 +- `locator` <[Locator]> + +Locator matching the parent elements to search within. diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 9c1e61ba92e21..20b22f0e4e42b 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -17368,6 +17368,31 @@ export interface Locator { */ timeout?: number; }): Promise; + + /** + * Returns a locator that matches this locator's elements inside each element matched by + * [`locator`](https://playwright.dev/docs/api/class-locator#locator-within-option-locator). This is the same as + * calling + * [locator.locator(selectorOrLocator[, options])](https://playwright.dev/docs/api/class-locator#locator-locator) on + * [`locator`](https://playwright.dev/docs/api/class-locator#locator-within-option-locator) with this locator as an + * argument, but reads in the natural order. + * + * Note that relative locators, such as + * [locator.nth(index)](https://playwright.dev/docs/api/class-locator#locator-nth) or + * [locator.first()](https://playwright.dev/docs/api/class-locator#locator-first), are resolved separately inside each + * matched parent. In the example below, `page.getByRole('cell').nth(2)` picks the third cell of every row, not the + * third cell in the whole table. + * + * **Usage** + * + * ```js + * const thirdColumn = page.getByRole('cell').nth(2).within(page.getByRole('row')); + * await expect(thirdColumn).toHaveText(['Apple', 'Banana', 'Cherry']); + * ``` + * + * @param locator Locator matching the parent elements to search within. + */ + within(locator: Locator): Locator; } /** diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 6ff220960ef2d..c282a1484eb15 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -180,6 +180,10 @@ export class Locator implements api.Locator { return new Locator(this._frame, this._selector + ' >> internal:chain=' + JSON.stringify(selectorOrLocator._selector), options); } + within(locator: Locator): Locator { + return locator.locator(this); + } + getByTestId(testId: string | RegExp): Locator { return this.locator(getByTestIdSelector(testIdAttributeName(), testId)); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 9c1e61ba92e21..20b22f0e4e42b 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17368,6 +17368,31 @@ export interface Locator { */ timeout?: number; }): Promise; + + /** + * Returns a locator that matches this locator's elements inside each element matched by + * [`locator`](https://playwright.dev/docs/api/class-locator#locator-within-option-locator). This is the same as + * calling + * [locator.locator(selectorOrLocator[, options])](https://playwright.dev/docs/api/class-locator#locator-locator) on + * [`locator`](https://playwright.dev/docs/api/class-locator#locator-within-option-locator) with this locator as an + * argument, but reads in the natural order. + * + * Note that relative locators, such as + * [locator.nth(index)](https://playwright.dev/docs/api/class-locator#locator-nth) or + * [locator.first()](https://playwright.dev/docs/api/class-locator#locator-first), are resolved separately inside each + * matched parent. In the example below, `page.getByRole('cell').nth(2)` picks the third cell of every row, not the + * third cell in the whole table. + * + * **Usage** + * + * ```js + * const thirdColumn = page.getByRole('cell').nth(2).within(page.getByRole('row')); + * await expect(thirdColumn).toHaveText(['Apple', 'Banana', 'Cherry']); + * ``` + * + * @param locator Locator matching the parent elements to search within. + */ + within(locator: Locator): Locator; } /** diff --git a/tests/page/locator-query.spec.ts b/tests/page/locator-query.spec.ts index 9fc227adcddaa..9430fd0570cdf 100644 --- a/tests/page/locator-query.spec.ts +++ b/tests/page/locator-query.spec.ts @@ -240,6 +240,24 @@ it('should support locator.locator with and/or', async ({ page }) => { await expect(page.locator('button').and(page.getByRole('button'))).toHaveText(['three', 'five']); }); +it('should support locator.within', async ({ page }) => { + await page.setContent(` + + + + +
a1a2a3
b1b2b3
c1c2c3
+ outside + `); + + await expect(page.getByRole('cell').within(page.getByRole('row'))).toHaveText(['a1', 'a2', 'a3', 'b1', 'b2', 'b3', 'c1', 'c2', 'c3']); + await expect(page.getByRole('cell').nth(1).within(page.getByRole('row'))).toHaveText(['a2', 'b2', 'c2']); + await expect(page.getByRole('cell').last().within(page.getByRole('row'))).toHaveText(['a3', 'b3', 'c3']); + await expect(page.getByRole('cell').nth(1).within(page.getByRole('row').nth(2))).toHaveText(['c2']); + await expect(page.locator('span').within(page.getByRole('row'))).toHaveCount(0); + await expect(page.getByRole('cell').nth(1).within(page.getByRole('row')).nth(1)).toHaveText('b2'); +}); + it('should allow some, but not all nested frameLocators', async ({ page }) => { await page.setContent(`hello`); await expect(page.frameLocator('iframe').locator('span').or(page.frameLocator('iframe').locator('article'))).toHaveText('world');