feat: Locator.filter(locator) (#21975)
Produces a locator that matches both locators. Implemented through `internal:and` selector. Fixes #19551.
This commit is contained in:
parent
47e5c02a21
commit
525097d465
|
|
@ -888,7 +888,7 @@ Value to set for the `<input>`, `<textarea>` or `[contenteditable]` element.
|
||||||
### option: Locator.fill.timeout = %%-input-timeout-js-%%
|
### option: Locator.fill.timeout = %%-input-timeout-js-%%
|
||||||
* since: v1.14
|
* since: v1.14
|
||||||
|
|
||||||
## method: Locator.filter
|
## method: Locator.filter#1
|
||||||
* since: v1.22
|
* since: v1.22
|
||||||
- returns: <[Locator]>
|
- returns: <[Locator]>
|
||||||
|
|
||||||
|
|
@ -946,9 +946,47 @@ await rowLocator
|
||||||
.ScreenshotAsync();
|
.ScreenshotAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
### option: Locator.filter.-inline- = %%-locator-options-list-v1.14-%%
|
### option: Locator.filter#1.-inline- = %%-locator-options-list-v1.14-%%
|
||||||
* since: v1.22
|
* since: v1.22
|
||||||
|
|
||||||
|
|
||||||
|
## method: Locator.filter#2
|
||||||
|
* since: v1.33
|
||||||
|
- returns: <[Locator]>
|
||||||
|
|
||||||
|
Creates a locator that matches both this locator and the argument locator.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
The following example finds a button with a specific title.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const button = page.getByRole('button').filter(page.getByTitle('Subscribe'));
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
Locator button = page.getByRole(AriaRole.BUTTON).filter(page.getByTitle("Subscribe"));
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
button = page.get_by_role("button").filter(page.getByTitle("Subscribe"))
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
button = page.get_by_role("button").filter(page.getByTitle("Subscribe"))
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var button = page.GetByRole(AriaRole.Button).Filter(page.GetByTitle("Subscribe"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### param: Locator.filter#2.locator
|
||||||
|
* since: v1.33
|
||||||
|
- `locator` <[Locator]>
|
||||||
|
|
||||||
|
Additional locator to match.
|
||||||
|
|
||||||
|
|
||||||
## method: Locator.first
|
## method: Locator.first
|
||||||
* since: v1.14
|
* since: v1.14
|
||||||
- returns: <[Locator]>
|
- returns: <[Locator]>
|
||||||
|
|
|
||||||
|
|
@ -1233,7 +1233,7 @@ Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-sele
|
||||||
|
|
||||||
## template-locator-locator
|
## template-locator-locator
|
||||||
|
|
||||||
The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options, similar to [`method: Locator.filter`] method.
|
The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options, similar to [`method: Locator.filter#1`] method.
|
||||||
|
|
||||||
[Learn more about locators](../locators.md).
|
[Learn more about locators](../locators.md).
|
||||||
|
|
||||||
|
|
@ -1293,7 +1293,7 @@ use: {
|
||||||
|
|
||||||
Allows locating elements that contain given text.
|
Allows locating elements that contain given text.
|
||||||
|
|
||||||
See also [`method: Locator.filter`] that allows to match by another criteria, like an accessible role, and then filter by the text content.
|
See also [`method: Locator.filter#1`] that allows to match by another criteria, like an accessible role, and then filter by the text content.
|
||||||
|
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
|
||||||
|
|
@ -806,7 +806,7 @@ Consider the following DOM structure where we want to click on the buy button of
|
||||||
|
|
||||||
### Filter by text
|
### Filter by text
|
||||||
|
|
||||||
Locators can be filtered by text with the [`method: Locator.filter`] method. It will search for a particular string somewhere inside the element, possibly in a descendant element, case-insensitively. You can also pass a regular expression.
|
Locators can be filtered by text with the [`method: Locator.filter#1`] method. It 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
|
```js
|
||||||
await page
|
await page
|
||||||
|
|
@ -883,7 +883,7 @@ await page
|
||||||
.ClickAsync();
|
.ClickAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Filter by another locator
|
### Filter by child/descendant
|
||||||
|
|
||||||
Locators support an option to only select elements that have a descendant matching another locator. You can therefore filter by any other locator such as a [`method: Locator.getByRole`], [`method: Locator.getByTestId`], [`method: Locator.getByText`] etc.
|
Locators support an option to only select elements that have a descendant matching another locator. You can therefore filter by any other locator such as a [`method: Locator.getByRole`], [`method: Locator.getByTestId`], [`method: Locator.getByText`] etc.
|
||||||
|
|
||||||
|
|
@ -985,6 +985,30 @@ await Expect(page
|
||||||
|
|
||||||
Note that the inner locator is matched starting from the outer one, not from the document root.
|
Note that the inner locator is matched starting from the outer one, not from the document root.
|
||||||
|
|
||||||
|
### Filter by matching an additional locator
|
||||||
|
|
||||||
|
Method [`method: Locator.filter#2`] narrows down an existing locator by matching an additional locator. For example, you can combine [`method: Page.getByRole`] and [`method: Page.getByTitle`] to match by both role and title.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const button = page.getByRole('button').filter(page.getByTitle('Subscribe'));
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
Locator button = page.getByRole(AriaRole.BUTTON).filter(page.getByTitle("Subscribe"));
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
button = page.get_by_role("button").filter(page.getByTitle("Subscribe"))
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
button = page.get_by_role("button").filter(page.getByTitle("Subscribe"))
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var button = page.GetByRole(AriaRole.Button).Filter(page.GetByTitle("Subscribe"));
|
||||||
|
```
|
||||||
|
|
||||||
## Chaining Locators
|
## Chaining Locators
|
||||||
|
|
||||||
You can chain methods that create a locator, like [`method: Page.getByText`] or [`method: Locator.getByRole`], to narrow down the search to a particular part of the page.
|
You can chain methods that create a locator, like [`method: Page.getByText`] or [`method: Locator.getByRole`], to narrow down the search to a particular part of the page.
|
||||||
|
|
@ -1198,7 +1222,7 @@ await page.GetByText("orange").ClickAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Filter by text
|
#### Filter by text
|
||||||
Use the [`method: Locator.filter`] to locate a specific item in a list.
|
Use the [`method: Locator.filter#1`] to locate a specific item in a list.
|
||||||
|
|
||||||
For example, consider the following DOM structure:
|
For example, consider the following DOM structure:
|
||||||
|
|
||||||
|
|
@ -1303,7 +1327,7 @@ However, use this method with caution. Often times, the page might change, and t
|
||||||
|
|
||||||
### Chaining filters
|
### Chaining filters
|
||||||
|
|
||||||
When you have elements with various similarities, you can use the [`method: Locator.filter`] method to select the right one. You can also chain multiple filters to narrow down the selection.
|
When you have elements with various similarities, you can use the [`method: Locator.filter#1`] method to select the right one. You can also chain multiple filters to narrow down the selection.
|
||||||
|
|
||||||
For example, consider the following DOM structure:
|
For example, consider the following DOM structure:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -967,7 +967,7 @@ If a selector needs to include `>>` in the body, it should be escaped inside a s
|
||||||
### Intermediate matches
|
### Intermediate matches
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
We recommend [filtering by another locator](./locators.md#filter-by-another-locator) to locate elements that contain other elements.
|
We recommend [filtering by another locator](./locators.md#filter-by-childdescendant) to locate elements that contain other elements.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
By default, chained selectors resolve to an element queried by the last selector. A selector can be prefixed with `*` to capture elements that are queried by an intermediate selector.
|
By default, chained selectors resolve to an element queried by the last selector. A selector can be prefixed with `*` to capture elements that are queried by an intermediate selector.
|
||||||
|
|
|
||||||
|
|
@ -452,7 +452,7 @@ Note that the new methods [`method: Page.routeFromHAR`] and [`method: BrowserCon
|
||||||
|
|
||||||
Read more in [our documentation](./locators.md#locate-by-role).
|
Read more in [our documentation](./locators.md#locate-by-role).
|
||||||
|
|
||||||
- New [`method: Locator.filter`] API to filter an existing locator
|
- New [`method: Locator.filter#1`] API to filter an existing locator
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var buttons = page.Locator("role=button");
|
var buttons = page.Locator("role=button");
|
||||||
|
|
|
||||||
|
|
@ -385,7 +385,7 @@ Note that the new methods [`method: Page.routeFromHAR`] and [`method: BrowserCon
|
||||||
|
|
||||||
Read more in [our documentation](./locators.md#locate-by-role).
|
Read more in [our documentation](./locators.md#locate-by-role).
|
||||||
|
|
||||||
- New [`method: Locator.filter`] API to filter an existing locator
|
- New [`method: Locator.filter#1`] API to filter an existing locator
|
||||||
|
|
||||||
```java
|
```java
|
||||||
Locator buttonsLocator = page.locator("role=button");
|
Locator buttonsLocator = page.locator("role=button");
|
||||||
|
|
|
||||||
|
|
@ -786,7 +786,7 @@ WebServer is now considered "ready" if request to the specified port has any of
|
||||||
|
|
||||||
Read more in [our documentation](./locators.md#locate-by-role).
|
Read more in [our documentation](./locators.md#locate-by-role).
|
||||||
|
|
||||||
- New [`method: Locator.filter`] API to filter an existing locator
|
- New [`method: Locator.filter#1`] API to filter an existing locator
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const buttons = page.locator('role=button');
|
const buttons = page.locator('role=button');
|
||||||
|
|
|
||||||
|
|
@ -440,7 +440,7 @@ Note that the new methods [`method: Page.routeFromHAR`] and [`method: BrowserCon
|
||||||
|
|
||||||
Read more in [our documentation](./locators.md#locate-by-role).
|
Read more in [our documentation](./locators.md#locate-by-role).
|
||||||
|
|
||||||
- New [`method: Locator.filter`] API to filter an existing locator
|
- New [`method: Locator.filter#1`] API to filter an existing locator
|
||||||
|
|
||||||
```py
|
```py
|
||||||
buttons = page.locator("role=button")
|
buttons = page.locator("role=button")
|
||||||
|
|
|
||||||
|
|
@ -168,8 +168,15 @@ export class Locator implements api.Locator {
|
||||||
return new FrameLocator(this._frame, this._selector + ' >> ' + selector);
|
return new FrameLocator(this._frame, this._selector + ' >> ' + selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
filter(options?: LocatorOptions): Locator {
|
filter(options?: LocatorOptions): Locator;
|
||||||
return new Locator(this._frame, this._selector, options);
|
filter(locator: Locator): Locator;
|
||||||
|
filter(optionsOrLocator?: LocatorOptions | Locator): Locator {
|
||||||
|
if (optionsOrLocator instanceof Locator) {
|
||||||
|
if (optionsOrLocator._frame !== this._frame)
|
||||||
|
throw new Error(`Locators must belong to the same frame.`);
|
||||||
|
return new Locator(this._frame, this._selector + ` >> internal:and=` + JSON.stringify(optionsOrLocator._selector));
|
||||||
|
}
|
||||||
|
return new Locator(this._frame, this._selector, optionsOrLocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
async elementHandle(options?: TimeoutOptions): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
async elementHandle(options?: TimeoutOptions): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,11 @@ class Locator {
|
||||||
self.getByText = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTextSelector(text, options));
|
self.getByText = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTextSelector(text, options));
|
||||||
self.getByTitle = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTitleSelector(text, options));
|
self.getByTitle = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTitleSelector(text, options));
|
||||||
self.getByRole = (role: string, options: ByRoleOptions = {}): Locator => self.locator(getByRoleSelector(role, options));
|
self.getByRole = (role: string, options: ByRoleOptions = {}): Locator => self.locator(getByRoleSelector(role, options));
|
||||||
self.filter = (options?: { hasText?: string | RegExp, has?: Locator }): Locator => new Locator(injectedScript, selector, options);
|
self.filter = (optionsOrLocator?: { hasText?: string | RegExp, has?: Locator } | Locator): Locator => {
|
||||||
|
if (optionsOrLocator instanceof Locator)
|
||||||
|
return new Locator(injectedScript, selectorBase + ` >> internal:and=` + JSON.stringify((optionsOrLocator as any)[selectorSymbol]));
|
||||||
|
return new Locator(injectedScript, selector, optionsOrLocator);
|
||||||
|
};
|
||||||
self.first = (): Locator => self.locator('nth=0');
|
self.first = (): Locator => self.locator('nth=0');
|
||||||
self.last = (): Locator => self.locator('nth=-1');
|
self.last = (): Locator => self.locator('nth=-1');
|
||||||
self.nth = (index: number): Locator => self.locator(`nth=${index}`);
|
self.nth = (index: number): Locator => self.locator(`nth=${index}`);
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ export class InjectedScript {
|
||||||
this._engines.set('internal:control', this._createControlEngine());
|
this._engines.set('internal:control', this._createControlEngine());
|
||||||
this._engines.set('internal:has', this._createHasEngine());
|
this._engines.set('internal:has', this._createHasEngine());
|
||||||
this._engines.set('internal:or', { queryAll: () => [] });
|
this._engines.set('internal:or', { queryAll: () => [] });
|
||||||
|
this._engines.set('internal:and', { queryAll: () => [] });
|
||||||
this._engines.set('internal:label', this._createInternalLabelEngine());
|
this._engines.set('internal:label', this._createInternalLabelEngine());
|
||||||
this._engines.set('internal:text', this._createTextEngine(true, true));
|
this._engines.set('internal:text', this._createTextEngine(true, true));
|
||||||
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
|
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
|
||||||
|
|
@ -170,14 +171,6 @@ export class InjectedScript {
|
||||||
return new Set<Element>(list.slice(nth, nth + 1));
|
return new Set<Element>(list.slice(nth, nth + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _queryOr(elements: Set<Element>, part: ParsedSelectorPart): Set<Element> {
|
|
||||||
const list = [...elements];
|
|
||||||
let nth = +part.body;
|
|
||||||
if (nth === -1)
|
|
||||||
nth = list.length - 1;
|
|
||||||
return new Set<Element>(list.slice(nth, nth + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _queryLayoutSelector(elements: Set<Element>, part: ParsedSelectorPart, originalRoot: Node): Set<Element> {
|
private _queryLayoutSelector(elements: Set<Element>, part: ParsedSelectorPart, originalRoot: Node): Set<Element> {
|
||||||
const name = part.name as LayoutSelectorName;
|
const name = part.name as LayoutSelectorName;
|
||||||
const body = part.body as NestedSelectorBody;
|
const body = part.body as NestedSelectorBody;
|
||||||
|
|
@ -222,6 +215,9 @@ export class InjectedScript {
|
||||||
} else if (part.name === 'internal:or') {
|
} else if (part.name === 'internal:or') {
|
||||||
const orElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root);
|
const orElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root);
|
||||||
roots = new Set(sortInDOMOrder(new Set([...roots, ...orElements])));
|
roots = new Set(sortInDOMOrder(new Set([...roots, ...orElements])));
|
||||||
|
} else if (part.name === 'internal:and') {
|
||||||
|
const andElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root);
|
||||||
|
roots = new Set(andElements.filter(e => roots.has(e)));
|
||||||
} else if (kLayoutSelectorNames.includes(part.name as LayoutSelectorName)) {
|
} else if (kLayoutSelectorNames.includes(part.name as LayoutSelectorName)) {
|
||||||
roots = this._queryLayoutSelector(roots, part, root);
|
roots = this._queryLayoutSelector(roots, part, root);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export class Selectors {
|
||||||
'data-testid', 'data-testid:light',
|
'data-testid', 'data-testid:light',
|
||||||
'data-test-id', 'data-test-id:light',
|
'data-test-id', 'data-test-id:light',
|
||||||
'data-test', 'data-test:light',
|
'data-test', 'data-test:light',
|
||||||
'nth', 'visible', 'internal:control', 'internal:has', 'internal:has-text', 'internal:or',
|
'nth', 'visible', 'internal:control', 'internal:has', 'internal:has-text', 'internal:or', 'internal:and',
|
||||||
'role', 'internal:attr', 'internal:label', 'internal:text', 'internal:role', 'internal:testid',
|
'role', 'internal:attr', 'internal:label', 'internal:text', 'internal:role', 'internal:testid',
|
||||||
]);
|
]);
|
||||||
this._builtinEnginesInMainWorld = new Set([
|
this._builtinEnginesInMainWorld = new Set([
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { type NestedSelectorBody, parseAttributeSelector, parseSelector, stringi
|
||||||
import type { ParsedSelector } from './selectorParser';
|
import type { ParsedSelector } from './selectorParser';
|
||||||
|
|
||||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
||||||
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has' | 'frame' | 'or';
|
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has' | 'frame' | 'or' | 'and';
|
||||||
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||||
|
|
||||||
type LocatorOptions = { attrs?: { name: string, value: string | boolean | number}[], exact?: boolean, name?: string | RegExp };
|
type LocatorOptions = { attrs?: { name: string, value: string | boolean | number}[], exact?: boolean, name?: string | RegExp };
|
||||||
|
|
@ -91,6 +91,11 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||||
tokens.push(factory.generateLocator(base, 'or', inner));
|
tokens.push(factory.generateLocator(base, 'or', inner));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (part.name === 'internal:and') {
|
||||||
|
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
||||||
|
tokens.push(factory.generateLocator(base, 'and', inner));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (part.name === 'internal:label') {
|
if (part.name === 'internal:label') {
|
||||||
const { exact, text } = detectExact(part.body as string);
|
const { exact, text } = detectExact(part.body as string);
|
||||||
tokens.push(factory.generateLocator(base, 'label', text, { exact }));
|
tokens.push(factory.generateLocator(base, 'label', text, { exact }));
|
||||||
|
|
@ -202,6 +207,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||||
return `filter({ has: ${body} })`;
|
return `filter({ has: ${body} })`;
|
||||||
case 'or':
|
case 'or':
|
||||||
return `or(${body})`;
|
return `or(${body})`;
|
||||||
|
case 'and':
|
||||||
|
return `filter(${body})`;
|
||||||
case 'test-id':
|
case 'test-id':
|
||||||
return `getByTestId(${this.quote(body as string)})`;
|
return `getByTestId(${this.quote(body as string)})`;
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|
@ -272,6 +279,8 @@ export class PythonLocatorFactory implements LocatorFactory {
|
||||||
return `filter(has=${body})`;
|
return `filter(has=${body})`;
|
||||||
case 'or':
|
case 'or':
|
||||||
return `or_(${body})`;
|
return `or_(${body})`;
|
||||||
|
case 'and':
|
||||||
|
return `filter(${body})`;
|
||||||
case 'test-id':
|
case 'test-id':
|
||||||
return `get_by_test_id(${this.quote(body as string)})`;
|
return `get_by_test_id(${this.quote(body as string)})`;
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|
@ -351,6 +360,8 @@ export class JavaLocatorFactory implements LocatorFactory {
|
||||||
return `filter(new ${clazz}.FilterOptions().setHas(${body}))`;
|
return `filter(new ${clazz}.FilterOptions().setHas(${body}))`;
|
||||||
case 'or':
|
case 'or':
|
||||||
return `or(${body})`;
|
return `or(${body})`;
|
||||||
|
case 'and':
|
||||||
|
return `filter(${body})`;
|
||||||
case 'test-id':
|
case 'test-id':
|
||||||
return `getByTestId(${this.quote(body as string)})`;
|
return `getByTestId(${this.quote(body as string)})`;
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|
@ -424,6 +435,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
||||||
return `Filter(new() { Has = ${body} })`;
|
return `Filter(new() { Has = ${body} })`;
|
||||||
case 'or':
|
case 'or':
|
||||||
return `Or(${body})`;
|
return `Or(${body})`;
|
||||||
|
case 'and':
|
||||||
|
return `Filter(${body})`;
|
||||||
case 'test-id':
|
case 'test-id':
|
||||||
return `GetByTestId(${this.quote(body as string)})`;
|
return `GetByTestId(${this.quote(body as string)})`;
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ function shiftParams(template: string, sub: number) {
|
||||||
|
|
||||||
function transform(template: string, params: TemplateParams, testIdAttributeName: string): string {
|
function transform(template: string, params: TemplateParams, testIdAttributeName: string): string {
|
||||||
// Recursively handle filter(has=).
|
// Recursively handle filter(has=).
|
||||||
// TODO: handle or(locator) as well.
|
// TODO: handle or(locator) and filter(locator).
|
||||||
while (true) {
|
while (true) {
|
||||||
const hasMatch = template.match(/filter\(,?has=/);
|
const hasMatch = template.match(/filter\(,?has=/);
|
||||||
if (!hasMatch)
|
if (!hasMatch)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { InvalidSelectorError, parseCSS } from './cssParser';
|
||||||
export { InvalidSelectorError, isInvalidSelectorError } from './cssParser';
|
export { InvalidSelectorError, isInvalidSelectorError } from './cssParser';
|
||||||
|
|
||||||
export type NestedSelectorBody = { parsed: ParsedSelector, distance?: number };
|
export type NestedSelectorBody = { parsed: ParsedSelector, distance?: number };
|
||||||
const kNestedSelectorNames = new Set(['internal:has', 'internal:or', 'left-of', 'right-of', 'above', 'below', 'near']);
|
const kNestedSelectorNames = new Set(['internal:has', 'internal:or', 'internal:and', 'left-of', 'right-of', 'above', 'below', 'near']);
|
||||||
const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']);
|
const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']);
|
||||||
|
|
||||||
export type ParsedSelectorPart = {
|
export type ParsedSelectorPart = {
|
||||||
|
|
|
||||||
27
packages/playwright-core/types/types.d.ts
vendored
27
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -2686,7 +2686,7 @@ export interface Page {
|
||||||
/**
|
/**
|
||||||
* Allows locating elements that contain given text.
|
* Allows locating elements that contain given text.
|
||||||
*
|
*
|
||||||
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to
|
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1) that allows to
|
||||||
* match by another criteria, like an accessible role, and then filter by the text content.
|
* match by another criteria, like an accessible role, and then filter by the text content.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
|
|
@ -6107,7 +6107,7 @@ export interface Frame {
|
||||||
/**
|
/**
|
||||||
* Allows locating elements that contain given text.
|
* Allows locating elements that contain given text.
|
||||||
*
|
*
|
||||||
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to
|
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1) that allows to
|
||||||
* match by another criteria, like an accessible role, and then filter by the text content.
|
* match by another criteria, like an accessible role, and then filter by the text content.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
|
|
@ -10785,6 +10785,21 @@ export interface Locator {
|
||||||
hasText?: string|RegExp;
|
hasText?: string|RegExp;
|
||||||
}): Locator;
|
}): Locator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a locator that matches both this locator and the argument locator.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* The following example finds a button with a specific title.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* const button = page.getByRole('button').filter(page.getByTitle('Subscribe'));
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param locator Additional locator to match.
|
||||||
|
*/
|
||||||
|
filter(locator: Locator): Locator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns locator to the first matching element.
|
* Returns locator to the first matching element.
|
||||||
*/
|
*/
|
||||||
|
|
@ -11065,7 +11080,7 @@ export interface Locator {
|
||||||
/**
|
/**
|
||||||
* Allows locating elements that contain given text.
|
* Allows locating elements that contain given text.
|
||||||
*
|
*
|
||||||
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to
|
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1) that allows to
|
||||||
* match by another criteria, like an accessible role, and then filter by the text content.
|
* match by another criteria, like an accessible role, and then filter by the text content.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
|
|
@ -11409,7 +11424,7 @@ export interface Locator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The method finds an element matching the specified selector in the locator's subtree. It also accepts filter
|
* The method finds an element matching the specified selector in the locator's subtree. It also accepts filter
|
||||||
* options, similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter)
|
* options, similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1)
|
||||||
* method.
|
* method.
|
||||||
*
|
*
|
||||||
* [Learn more about locators](https://playwright.dev/docs/locators).
|
* [Learn more about locators](https://playwright.dev/docs/locators).
|
||||||
|
|
@ -16952,7 +16967,7 @@ export interface FrameLocator {
|
||||||
/**
|
/**
|
||||||
* Allows locating elements that contain given text.
|
* Allows locating elements that contain given text.
|
||||||
*
|
*
|
||||||
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to
|
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1) that allows to
|
||||||
* match by another criteria, like an accessible role, and then filter by the text content.
|
* match by another criteria, like an accessible role, and then filter by the text content.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
|
|
@ -17036,7 +17051,7 @@ export interface FrameLocator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The method finds an element matching the specified selector in the locator's subtree. It also accepts filter
|
* The method finds an element matching the specified selector in the locator's subtree. It also accepts filter
|
||||||
* options, similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter)
|
* options, similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1)
|
||||||
* method.
|
* method.
|
||||||
*
|
*
|
||||||
* [Learn more about locators](https://playwright.dev/docs/locators).
|
* [Learn more about locators](https://playwright.dev/docs/locators).
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,16 @@ it('should support playwright.locator({ has })', async ({ page }) => {
|
||||||
expect(await page.evaluate(`playwright.locator('div', { has: playwright.locator('text=Hello') }).element.innerHTML`)).toContain('span');
|
expect(await page.evaluate(`playwright.locator('div', { has: playwright.locator('text=Hello') }).element.innerHTML`)).toContain('span');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support playwright.or()', async ({ page }) => {
|
it('should support locator.or()', async ({ page }) => {
|
||||||
await page.setContent('<div>Hi</div><span>Hello</span>');
|
await page.setContent('<div>Hi</div><span>Hello</span>');
|
||||||
expect(await page.evaluate(`playwright.locator('div').or(playwright.locator('span')).elements.map(e => e.innerHTML)`)).toEqual(['Hi', 'Hello']);
|
expect(await page.evaluate(`playwright.locator('div').or(playwright.locator('span')).elements.map(e => e.innerHTML)`)).toEqual(['Hi', 'Hello']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support locator.filter(locator)', async ({ page }) => {
|
||||||
|
await page.setContent('<div data-testid=Hey>Hi</div>');
|
||||||
|
expect(await page.evaluate(`playwright.locator('div').filter(playwright.getByTestId('Hey')).elements.map(e => e.innerHTML)`)).toEqual(['Hi']);
|
||||||
|
});
|
||||||
|
|
||||||
it('should support playwright.getBy*', async ({ page }) => {
|
it('should support playwright.getBy*', async ({ page }) => {
|
||||||
await page.setContent('<span>Hello</span><span title="world">World</span>');
|
await page.setContent('<span>Hello</span><span title="world">World</span>');
|
||||||
expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello');
|
expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello');
|
||||||
|
|
|
||||||
|
|
@ -359,6 +359,13 @@ it('asLocator internal:or', async () => {
|
||||||
expect.soft(asLocator('csharp', 'div >> internal:or="span >> article"', false)).toBe(`Locator("div").Or(Locator("span").Locator("article"))`);
|
expect.soft(asLocator('csharp', 'div >> internal:or="span >> article"', false)).toBe(`Locator("div").Or(Locator("span").Locator("article"))`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('asLocator internal:and', async () => {
|
||||||
|
expect.soft(asLocator('javascript', 'div >> internal:and="span >> article"', false)).toBe(`locator('div').filter(locator('span').locator('article'))`);
|
||||||
|
expect.soft(asLocator('python', 'div >> internal:and="span >> article"', false)).toBe(`locator("div").filter(locator("span").locator("article"))`);
|
||||||
|
expect.soft(asLocator('java', 'div >> internal:and="span >> article"', false)).toBe(`locator("div").filter(locator("span").locator("article"))`);
|
||||||
|
expect.soft(asLocator('csharp', 'div >> internal:and="span >> article"', false)).toBe(`Locator("div").Filter(Locator("span").Locator("article"))`);
|
||||||
|
});
|
||||||
|
|
||||||
it('parse locators strictly', () => {
|
it('parse locators strictly', () => {
|
||||||
const selector = 'div >> internal:has-text=\"Goodbye world\"i >> span';
|
const selector = 'div >> internal:has-text=\"Goodbye world\"i >> span';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,19 @@ it('should support locator.or', async ({ page }) => {
|
||||||
await expect(page.locator('span').or(page.locator('article'))).toHaveText('world');
|
await expect(page.locator('span').or(page.locator('article'))).toHaveText('world');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support locator.filter(locator)', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<div data-testid=foo>hello</div><div data-testid=bar>world</div>
|
||||||
|
<span data-testid=foo>hello2</span><span data-testid=bar>world2</span>
|
||||||
|
`);
|
||||||
|
await expect(page.locator('div').filter(page.locator('div'))).toHaveCount(2);
|
||||||
|
await expect(page.locator('div').filter(page.getByTestId('foo'))).toHaveText(['hello']);
|
||||||
|
await expect(page.locator('div').filter(page.getByTestId('bar'))).toHaveText(['world']);
|
||||||
|
await expect(page.getByTestId('foo').filter(page.locator('div'))).toHaveText(['hello']);
|
||||||
|
await expect(page.getByTestId('bar').filter(page.locator('span'))).toHaveText(['world2']);
|
||||||
|
await expect(page.locator('span').filter(page.getByTestId(/bar|foo/))).toHaveCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
it('should enforce same frame for has/leftOf/rightOf/above/below/near', async ({ page, server }) => {
|
it('should enforce same frame for has/leftOf/rightOf/above/below/near', 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];
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,19 @@ it('should work with internal:or=', async ({ page, server }) => {
|
||||||
expect(await page.locator(`span >> internal:or="article"`).textContent()).toBe('world');
|
expect(await page.locator(`span >> internal:or="article"`).textContent()).toBe('world');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should work with internal:and=', async ({ page, server }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<div class=foo>hello</div><div class=bar>world</div>
|
||||||
|
<span class=foo>hello2</span><span class=bar>world2</span>
|
||||||
|
`);
|
||||||
|
expect(await page.$$eval(`div >> internal:and="span"`, els => els.map(e => e.textContent))).toEqual([]);
|
||||||
|
expect(await page.$$eval(`div >> internal:and=".foo"`, els => els.map(e => e.textContent))).toEqual(['hello']);
|
||||||
|
expect(await page.$$eval(`div >> internal:and=".bar"`, els => els.map(e => e.textContent))).toEqual(['world']);
|
||||||
|
expect(await page.$$eval(`span >> internal:and="span"`, els => els.map(e => e.textContent))).toEqual(['hello2', 'world2']);
|
||||||
|
expect(await page.$$eval(`.foo >> internal:and="div"`, els => els.map(e => e.textContent))).toEqual(['hello']);
|
||||||
|
expect(await page.$$eval(`.bar >> internal:and="span"`, els => els.map(e => e.textContent))).toEqual(['world2']);
|
||||||
|
});
|
||||||
|
|
||||||
it('chaining should work with large DOM @smoke', async ({ page, server }) => {
|
it('chaining should work with large DOM @smoke', async ({ page, server }) => {
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
let last = document.body;
|
let last = document.body;
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,14 @@ module.exports = function lint(documentation, jsSources, apiFileName) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const [methodName, params] of methods) {
|
for (const [methodName, params] of methods) {
|
||||||
const member = docClass.membersArray.find(m => m.alias === methodName && m.kind !== 'event');
|
const members = docClass.membersArray.filter(m => m.alias === methodName && m.kind !== 'event');
|
||||||
if (!member) {
|
if (!members.length) {
|
||||||
errors.push(`Missing documentation for "${className}.${methodName}"`);
|
errors.push(`Missing documentation for "${className}.${methodName}"`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const memberParams = paramsForMember(member);
|
|
||||||
for (const paramName of params) {
|
for (const paramName of params) {
|
||||||
if (!memberParams.has(paramName))
|
const found = members.some(member => paramsForMember(member).has(paramName));
|
||||||
|
if (!found)
|
||||||
errors.push(`Missing documentation for "${className}.${methodName}.${paramName}"`);
|
errors.push(`Missing documentation for "${className}.${methodName}.${paramName}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue