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-%%
## 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.

View file

@ -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

View file

@ -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<ElementHandle<SVGElement | HTMLElement>> {
return await this._frame.waitForSelector(this._selector, { strict: true, state: 'attached', ...options })!;
}

View file

@ -9799,6 +9799,27 @@ export interface Locator {
timeout?: number;
}): 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.
*

View file

@ -9808,6 +9808,27 @@ export interface Locator {
timeout?: number;
}): 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.
*

View file

@ -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(`<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 }) => {
await page.goto(server.PREFIX + '/frames/two-frames.html');
const child = page.frames()[1];