From 97750ccf9afc3237540d7782c5de89eca09e8eec Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 25 Apr 2022 20:06:18 +0100 Subject: [PATCH] feat: locator.that (#13731) Filters existing locator by options, currently `has` and `hasText`. --- docs/src/api/class-locator.md | 7 + docs/src/locators.md | 129 +++++++++++++++++- .../playwright-core/src/client/locator.ts | 4 + packages/playwright-core/types/types.d.ts | 21 +++ tests/config/experimental.d.ts | 21 +++ tests/page/locator-query.spec.ts | 17 +++ 6 files changed, 198 insertions(+), 1 deletion(-) diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index c536147442..69c7a1a60b 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -770,6 +770,13 @@ Returns the `node.textContent`. ### option: Locator.textContent.timeout = %%-input-timeout-%% +## method: Locator.that +- returns: <[Locator]> + +This method narrows existing locator according to the options, for example filters by text. + +### option: Locator.that.-inline- = %%-locator-options-list-%% + ## async method: Locator.type Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. diff --git a/docs/src/locators.md b/docs/src/locators.md index f63754256a..45886dab8b 100644 --- a/docs/src/locators.md +++ b/docs/src/locators.md @@ -67,10 +67,69 @@ await locator.HoverAsync(); await locator.ClickAsync(); ``` +## Creating Locators + +Use [`method: Page.locator`] method to create a locator. This method takes a selector that describes how to find an element in the page. Playwright supports many different selectors like [Text](./selectors.md#text-selector), [CSS](./selectors.md#css-selector), [XPath](./selectors.md#xpath-selectors) and many more. Learn more about available selectors and how to pick one in this [in-depth guide](./selectors.md). + +```js +// Find by text. +await page.locator('text=Sign up').click(); + +// Find by CSS. +await page.locator('button.sign-up').click(); + +// Find by test id. +await page.locator('data-testid=sign-up').click(); +``` + +```python async +# Find by text. +await page.locator("text=Sign up").click() + +# Find by CSS. +await page.locator("button.sign-up").click() + +# Find by test id. +await page.locator("data-testid=sign-up").click() +``` + +```python sync +# Find by text. +page.locator("text=Sign up").click() + +# Find by CSS. +page.locator("button.sign-up").click() + +# Find by test id. +page.locator("data-testid=sign-up").click() +``` + +```java +// Find by text. +page.locator("text=Sign up").click(); + +// Find by CSS. +page.locator("button.sign-up").click(); + +// Find by test id. +page.locator("data-testid=sign-up").click(); +``` + +```csharp +// Find by text. +await page.Locator("text=Sign up").ClickAsync(); + +// Find by CSS. +await page.Locator("button.sign-up").ClickAsync(); + +// Find by test id. +await page.Locator("data-testid=sign-up").ClickAsync(); +``` + ## Strictness Locators are strict. This means that all operations on locators that imply -some target DOM element will throw an exception if more than one element matches +some target DOM element will throw an exception if more than one element matches given selector. ```js @@ -217,6 +276,74 @@ for (let i = 0; i < count; ++i) var texts = await rows.EvaluateAllAsync("list => list.map(element => element.textContent)"); ``` +## Filtering Locators + +When creating a locator, you can pass additional options to filter it. + +Filtering by text will search for a particular string somewhere inside the element, possibly in a descendant element, case-insensitively. You can also pass a regular expression. + +```js +await page.locator('button', { hasText: 'Sign up' }).click(); +``` +```java +page.locator("button", new Page.LocatorOptions().setHasText("Sign up")).click(); +``` +```python async +await page.locator("button", has_text="Sign up").click() +``` +```python sync +page.locator("button", has_text="Sign up").click() +``` +```csharp +await page.Locator("button", new PageLocatorOptions { HasText = "Sign up" }).ClickAsync(); +``` + +Locators also support an option to only select elements that have a descendant matching another locator. Note that inner locator is matched starting from the outer one, not from the document root. + +```js +page.locator('article', { has: page.locator('button.subscribe') }) +``` +```java +page.locator("article", new Page.LocatorOptions().setHas(page.locator("button.subscribe"))) +``` +```python async +page.locator("article", has=page.locator("button.subscribe")) +``` +```python sync +page.locator("article", has=page.locator("button.subscribe")) +``` +```csharp +page.Locator("article", new PageLocatorOptions { Has = page.Locator("button.subscribe") }) +``` + +You can also filter an existing locator with [`method: Locator.that`] method. + +```js +const buttonLocator = page.locator('button'); +// ... +await buttonLocator.that({ hasText: 'Sign up' }).click(); +``` +```java +Locator buttonLocator = page.locator("button"); +// ... +buttonLocator.that(new Locator.ThatOptions().setHasText("Sign up")).click(); +``` +```python async +button_locator = page.locator("button") +# ... +await button_locator.that(has_text="Sign up").click() +``` +```python sync +button_locator = page.locator("button") +# ... +button_locator.that(has_text="Sign up").click() +``` +```csharp +var buttonLocator = page.Locator("button"); +// ... +await buttonLocator.That(new LocatorThatOptions { HasText = "Sign up" }).ClickAsync(); +``` + ## Locator vs ElementHandle :::caution diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 68e51252b6..7f5bb57c11 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -130,6 +130,10 @@ export class Locator implements api.Locator { return new FrameLocator(this._frame, this._selector + ' >> ' + selector); } + that(options?: { hasText?: string | RegExp, has?: Locator }): Locator { + return new Locator(this._frame, this._selector, options); + } + async elementHandle(options?: TimeoutOptions): Promise> { return await this._frame.waitForSelector(this._selector, { strict: true, state: 'attached', ...options })!; } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index eb662cf007..db7f6ecf8c 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9799,6 +9799,27 @@ export interface Locator { timeout?: number; }): Promise; + /** + * This method narrows existing locator according to the options, for example filters by text. + * @param options + */ + that(options?: { + /** + * Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one. + * For example, `article` that has `text=Playwright` matches `
Playwright
`. + * + * Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s. + */ + has?: Locator; + + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a + * [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches + * `
Playwright
`. + */ + hasText?: string|RegExp; + }): Locator; + /** * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. * diff --git a/tests/config/experimental.d.ts b/tests/config/experimental.d.ts index 16faefe788..f4dd2d1827 100644 --- a/tests/config/experimental.d.ts +++ b/tests/config/experimental.d.ts @@ -9808,6 +9808,27 @@ export interface Locator { timeout?: number; }): Promise; + /** + * This method narrows existing locator according to the options, for example filters by text. + * @param options + */ + that(options?: { + /** + * Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one. + * For example, `article` that has `text=Playwright` matches `
Playwright
`. + * + * Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s. + */ + has?: Locator; + + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a + * [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches + * `
Playwright
`. + */ + hasText?: string|RegExp; + }): Locator; + /** * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. * diff --git a/tests/page/locator-query.spec.ts b/tests/page/locator-query.spec.ts index 1908070015..cc6fd9cb1e 100644 --- a/tests/page/locator-query.spec.ts +++ b/tests/page/locator-query.spec.ts @@ -124,6 +124,23 @@ it('should support has:locator', async ({ page, trace }) => { })).toHaveCount(1); }); +it('should support locator.that', async ({ page, trace }) => { + it.skip(trace === 'on'); + + await page.setContent(`
hello
world
`); + await expect(page.locator(`div`).that({ hasText: 'hello' })).toHaveCount(1); + await expect(page.locator(`div`, { hasText: 'hello' }).that({ hasText: 'hello' })).toHaveCount(1); + await expect(page.locator(`div`, { hasText: 'hello' }).that({ hasText: 'world' })).toHaveCount(0); + await expect(page.locator(`section`, { hasText: 'hello' }).that({ hasText: 'world' })).toHaveCount(1); + await expect(page.locator(`div`).that({ hasText: 'hello' }).locator('span')).toHaveCount(1); + await expect(page.locator(`div`).that({ has: page.locator('span', { hasText: 'world' }) })).toHaveCount(1); + await expect(page.locator(`div`).that({ has: page.locator('span') })).toHaveCount(2); + await expect(page.locator(`div`).that({ + has: page.locator('span'), + hasText: 'world', + })).toHaveCount(1); +}); + it('should enforce same frame for has:locator', async ({ page, server }) => { await page.goto(server.PREFIX + '/frames/two-frames.html'); const child = page.frames()[1];