feat: locator.that (#13731)

Filters existing locator by options, currently `has` and `hasText`.
This commit is contained in:
Dmitry Gozman 2022-04-25 20:06:18 +01:00 committed by GitHub
parent d71060321d
commit 97750ccf9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 198 additions and 1 deletions

View file

@ -770,6 +770,13 @@ Returns the `node.textContent`.
### option: Locator.textContent.timeout = %%-input-timeout-%% ### 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 ## async method: Locator.type
Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.

View file

@ -67,6 +67,65 @@ await locator.HoverAsync();
await locator.ClickAsync(); 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 ## Strictness
Locators are strict. This means that all operations on locators that imply Locators are strict. This means that all operations on locators that imply
@ -217,6 +276,74 @@ for (let i = 0; i < count; ++i)
var texts = await rows.EvaluateAllAsync("list => list.map(element => element.textContent)"); 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 ## Locator vs ElementHandle
:::caution :::caution

View file

@ -130,6 +130,10 @@ export class Locator implements api.Locator {
return new FrameLocator(this._frame, this._selector + ' >> ' + selector); 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<ElementHandle<SVGElement | HTMLElement>> { async elementHandle(options?: TimeoutOptions): Promise<ElementHandle<SVGElement | HTMLElement>> {
return await this._frame.waitForSelector(this._selector, { strict: true, state: 'attached', ...options })!; return await this._frame.waitForSelector(this._selector, { strict: true, state: 'attached', ...options })!;
} }

View file

@ -9799,6 +9799,27 @@ export interface Locator {
timeout?: number; timeout?: number;
}): Promise<null|string>; }): Promise<null|string>;
/**
* 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 `<article><div>Playwright</div></article>`.
*
* 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
* `<article><div>Playwright</div></article>`.
*/
hasText?: string|RegExp;
}): Locator;
/** /**
* Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.
* *

View file

@ -9808,6 +9808,27 @@ export interface Locator {
timeout?: number; timeout?: number;
}): Promise<null|string>; }): Promise<null|string>;
/**
* 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 `<article><div>Playwright</div></article>`.
*
* 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
* `<article><div>Playwright</div></article>`.
*/
hasText?: string|RegExp;
}): Locator;
/** /**
* Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.
* *

View file

@ -124,6 +124,23 @@ it('should support has:locator', async ({ page, trace }) => {
})).toHaveCount(1); })).toHaveCount(1);
}); });
it('should support locator.that', async ({ page, trace }) => {
it.skip(trace === 'on');
await page.setContent(`<section><div><span>hello</span></div><div><span>world</span></div></section>`);
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 }) => { it('should enforce same frame for has:locator', async ({ page, server }) => {
await page.goto(server.PREFIX + '/frames/two-frames.html'); await page.goto(server.PREFIX + '/frames/two-frames.html');
const child = page.frames()[1]; const child = page.frames()[1];