chore: replace locator.visible with filter({ visible }) (#34947)

This commit is contained in:
Dmitry Gozman 2025-02-27 13:44:53 +00:00 committed by GitHub
parent 837abfbc15
commit 3ce9ae6a7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 57 additions and 79 deletions

View file

@ -1090,6 +1090,9 @@ await rowLocator
### option: Locator.filter.hasNotText = %%-locator-option-has-not-text-%%
* since: v1.33
### option: Locator.filter.visible = %%-locator-option-visible-%%
* since: v1.51
## method: Locator.first
* since: v1.14
- returns: <[Locator]>
@ -2478,18 +2481,6 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.uncheck.trial = %%-input-trial-%%
* since: v1.14
## method: Locator.visible
* since: v1.51
- returns: <[Locator]>
Returns a locator that only matches [visible](../actionability.md#visible) elements.
### option: Locator.visible.visible
* since: v1.51
- `visible` <[boolean]>
Whether to match visible or invisible elements.
## async method: Locator.waitFor
* since: v1.16

View file

@ -1155,6 +1155,11 @@ Note that outer and inner locators must belong to the same frame. Inner locator
Matches elements that do not contain 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.
## locator-option-visible
- `visible` <[boolean]>
Only matches visible or invisible elements.
## locator-options-list-v1.14
- %%-locator-option-has-text-%%
- %%-locator-option-has-%%

View file

@ -1310,19 +1310,19 @@ Consider a page with two buttons, the first invisible and the second [visible](.
* This will only find a second button, because it is visible, and then click it.
```js
await page.locator('button').visible().click();
await page.locator('button').filter({ visible: true }).click();
```
```java
page.locator("button").visible().click();
page.locator("button").filter(new Locator.FilterOptions.setVisible(true)).click();
```
```python async
await page.locator("button").visible().click()
await page.locator("button").filter(visible=True).click()
```
```python sync
page.locator("button").visible().click()
page.locator("button").filter(visible=True).click()
```
```csharp
await page.Locator("button").Visible().ClickAsync();
await page.Locator("button").Filter(new() { Visible = true }).ClickAsync();
```
## Lists

View file

@ -13129,6 +13129,11 @@ export interface Locator {
* `<article><div>Playwright</div></article>`.
*/
hasText?: string|RegExp;
/**
* Only matches visible or invisible elements.
*/
visible?: boolean;
}): Locator;
/**
@ -14520,17 +14525,6 @@ export interface Locator {
trial?: boolean;
}): Promise<void>;
/**
* Returns a locator that only matches [visible](https://playwright.dev/docs/actionability#visible) elements.
* @param options
*/
visible(options?: {
/**
* Whether to match visible or invisible elements.
*/
visible?: boolean;
}): Locator;
/**
* Returns when element specified by locator satisfies the
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option.

View file

@ -35,6 +35,7 @@ export type LocatorOptions = {
hasNotText?: string | RegExp;
has?: Locator;
hasNot?: Locator;
visible?: boolean;
};
export class Locator implements api.Locator {
@ -65,6 +66,9 @@ export class Locator implements api.Locator {
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
}
if (options?.visible !== undefined)
this._selector += ` >> visible=${options.visible ? 'true' : 'false'}`;
if (this._frame._platform.inspectCustom)
(this as any)[this._frame._platform.inspectCustom] = () => this._inspect();
}
@ -150,7 +154,7 @@ export class Locator implements api.Locator {
return await this._frame._highlight(this._selector);
}
locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator {
locator(selectorOrLocator: string | Locator, options?: Omit<LocatorOptions, 'visible'>): Locator {
if (isString(selectorOrLocator))
return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options);
if (selectorOrLocator._frame !== this._frame)
@ -218,11 +222,6 @@ export class Locator implements api.Locator {
return new Locator(this._frame, this._selector + ` >> nth=${index}`);
}
visible(options: { visible?: boolean } = {}): Locator {
const { visible = true } = options;
return new Locator(this._frame, this._selector + ` >> visible=${visible ? 'true' : 'false'}`);
}
and(locator: Locator): Locator {
if (locator._frame !== this._frame)
throw new Error(`Locators must belong to the same frame.`);

View file

@ -29,7 +29,7 @@ class Locator {
element: Element | undefined;
elements: Element[] | undefined;
constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator }) {
constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator, visible?: boolean }) {
if (options?.hasText)
selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
if (options?.hasNotText)
@ -38,6 +38,8 @@ class Locator {
selector += ` >> internal:has=` + JSON.stringify(options.has[selectorSymbol]);
if (options?.hasNot)
selector += ` >> internal:has-not=` + JSON.stringify(options.hasNot[selectorSymbol]);
if (options?.visible !== undefined)
selector += ` >> visible=${options.visible ? 'true' : 'false'}`;
this[selectorSymbol] = selector;
if (selector) {
const parsed = injectedScript.parseSelector(selector);
@ -46,7 +48,7 @@ class Locator {
}
const selectorBase = selector;
const self = this as any;
self.locator = (selector: string, options?: { hasText?: string | RegExp, has?: Locator }): Locator => {
self.locator = (selector: string, options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator }): Locator => {
return new Locator(injectedScript, selectorBase ? selectorBase + ' >> ' + selector : selector, options);
};
self.getByTestId = (testId: string): Locator => self.locator(getByTestIdSelector(injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen(), testId));
@ -56,7 +58,7 @@ class Locator {
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.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 = (options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator, visible?: boolean }): Locator => new Locator(injectedScript, selector, options);
self.first = (): Locator => self.locator('nth=0');
self.last = (): Locator => self.locator('nth=-1');
self.nth = (index: number): Locator => self.locator(`nth=${index}`);

View file

@ -280,7 +280,7 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
case 'last':
return `last()`;
case 'visible':
return `visible(${body === 'true' ? '' : '{ visible: false }'})`;
return `filter({ visible: ${body === 'true' ? 'true' : 'false'} })`;
case 'role':
const attrs: string[] = [];
if (isRegExp(options.name)) {
@ -376,7 +376,7 @@ export class PythonLocatorFactory implements LocatorFactory {
case 'last':
return `last`;
case 'visible':
return `visible(${body === 'true' ? '' : 'visible=False'})`;
return `filter(visible=${body === 'true' ? 'True' : 'False'})`;
case 'role':
const attrs: string[] = [];
if (isRegExp(options.name)) {
@ -485,7 +485,7 @@ export class JavaLocatorFactory implements LocatorFactory {
case 'last':
return `last()`;
case 'visible':
return `visible(${body === 'true' ? '' : `new ${clazz}.VisibleOptions().setVisible(false)`})`;
return `filter(new ${clazz}.FilterOptions().setVisible(${body === 'true' ? 'true' : 'false'}))`;
case 'role':
const attrs: string[] = [];
if (isRegExp(options.name)) {
@ -584,7 +584,7 @@ export class CSharpLocatorFactory implements LocatorFactory {
case 'last':
return `Last`;
case 'visible':
return `Visible(${body === 'true' ? '' : 'new() { Visible = false }'})`;
return `Filter(new() { Visible = ${body === 'true' ? 'true' : 'false'} })`;
case 'role':
const attrs: string[] = [];
if (isRegExp(options.name)) {

View file

@ -170,9 +170,8 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
.replace(/first(\(\))?/g, 'nth=0')
.replace(/last(\(\))?/g, 'nth=-1')
.replace(/nth\(([^)]+)\)/g, 'nth=$1')
.replace(/visible\(,?visible=true\)/g, 'visible=true')
.replace(/visible\(,?visible=false\)/g, 'visible=false')
.replace(/visible\(\)/g, 'visible=true')
.replace(/filter\(,?visible=true\)/g, 'visible=true')
.replace(/filter\(,?visible=false\)/g, 'visible=false')
.replace(/filter\(,?hastext=([^)]+)\)/g, 'internal:has-text=$1')
.replace(/filter\(,?hasnottext=([^)]+)\)/g, 'internal:has-not-text=$1')
.replace(/filter\(,?has2=([^)]+)\)/g, 'internal:has=$1')

View file

@ -13129,6 +13129,11 @@ export interface Locator {
* `<article><div>Playwright</div></article>`.
*/
hasText?: string|RegExp;
/**
* Only matches visible or invisible elements.
*/
visible?: boolean;
}): Locator;
/**
@ -14520,17 +14525,6 @@ export interface Locator {
trial?: boolean;
}): Promise<void>;
/**
* Returns a locator that only matches [visible](https://playwright.dev/docs/actionability#visible) elements.
* @param options
*/
visible(options?: {
/**
* Whether to match visible or invisible elements.
*/
visible?: boolean;
}): Locator;
/**
* Returns when element specified by locator satisfies the
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option.

View file

@ -92,13 +92,14 @@ it('should support locator.or()', 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><div>one</div><div style="display:none">two</div>');
expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello');
expect(await page.evaluate(`playwright.getByTitle('world').element.innerHTML`)).toContain('World');
expect(await page.evaluate(`playwright.locator('span').filter({ hasText: 'hello' }).element.innerHTML`)).toContain('Hello');
expect(await page.evaluate(`playwright.locator('span').first().element.innerHTML`)).toContain('Hello');
expect(await page.evaluate(`playwright.locator('span').last().element.innerHTML`)).toContain('World');
expect(await page.evaluate(`playwright.locator('span').nth(1).element.innerHTML`)).toContain('World');
expect(await page.evaluate(`playwright.locator('div').filter({ visible: false }).element.innerHTML`)).toContain('two');
});
it('expected properties on playwright object', async ({ page }) => {

View file

@ -321,23 +321,17 @@ it('reverse engineer hasNotText', async ({ page }) => {
});
it('reverse engineer visible', async ({ page }) => {
expect.soft(generate(page.getByText('Hello').visible().locator('div'))).toEqual({
csharp: `GetByText("Hello").Visible().Locator("div")`,
java: `getByText("Hello").visible().locator("div")`,
javascript: `getByText('Hello').visible().locator('div')`,
python: `get_by_text("Hello").visible().locator("div")`,
expect.soft(generate(page.getByText('Hello').filter({ visible: true }).locator('div'))).toEqual({
csharp: `GetByText("Hello").Filter(new() { Visible = true }).Locator("div")`,
java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(true)).locator("div")`,
javascript: `getByText('Hello').filter({ visible: true }).locator('div')`,
python: `get_by_text("Hello").filter(visible=True).locator("div")`,
});
expect.soft(generate(page.getByText('Hello').visible({ visible: true }).locator('div'))).toEqual({
csharp: `GetByText("Hello").Visible().Locator("div")`,
java: `getByText("Hello").visible().locator("div")`,
javascript: `getByText('Hello').visible().locator('div')`,
python: `get_by_text("Hello").visible().locator("div")`,
});
expect.soft(generate(page.getByText('Hello').visible({ visible: false }).locator('div'))).toEqual({
csharp: `GetByText("Hello").Visible(new() { Visible = false }).Locator("div")`,
java: `getByText("Hello").visible(new Locator.VisibleOptions().setVisible(false)).locator("div")`,
javascript: `getByText('Hello').visible({ visible: false }).locator('div')`,
python: `get_by_text("Hello").visible(visible=False).locator("div")`,
expect.soft(generate(page.getByText('Hello').filter({ visible: false }).locator('div'))).toEqual({
csharp: `GetByText("Hello").Filter(new() { Visible = false }).Locator("div")`,
java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(false)).locator("div")`,
javascript: `getByText('Hello').filter({ visible: false }).locator('div')`,
python: `get_by_text("Hello").filter(visible=False).locator("div")`,
});
});

View file

@ -150,7 +150,7 @@ it('should combine visible with other selectors', async ({ page }) => {
await expect(page.locator('.item >> visible=true >> text=data3')).toHaveText('visible data3');
});
it('should support .visible()', async ({ page }) => {
it('should support filter(visible)', async ({ page }) => {
await page.setContent(`<div>
<div class="item" style="display: none">Hidden data0</div>
<div class="item">visible data1</div>
@ -160,11 +160,10 @@ it('should support .visible()', async ({ page }) => {
<div class="item">visible data3</div>
</div>
`);
const locator = page.locator('.item').visible().nth(1);
const locator = page.locator('.item').filter({ visible: true }).nth(1);
await expect(locator).toHaveText('visible data2');
await expect(page.locator('.item').visible().getByText('data3')).toHaveText('visible data3');
await expect(page.locator('.item').visible({ visible: true }).getByText('data2')).toHaveText('visible data2');
await expect(page.locator('.item').visible({ visible: false }).getByText('data1')).toHaveText('Hidden data1');
await expect(page.locator('.item').filter({ visible: true }).getByText('data3')).toHaveText('visible data3');
await expect(page.locator('.item').filter({ visible: false }).getByText('data1')).toHaveText('Hidden data1');
});
it('locator.count should work with deleted Map in main world', async ({ page }) => {