feat: locator.that (#13731)
Filters existing locator by options, currently `has` and `hasText`.
This commit is contained in:
parent
d71060321d
commit
97750ccf9a
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -67,10 +67,69 @@ 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
|
||||||
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.
|
given selector.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 })!;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
packages/playwright-core/types/types.d.ts
vendored
21
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
21
tests/config/experimental.d.ts
vendored
21
tests/config/experimental.d.ts
vendored
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue