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-%% ### option: Locator.filter.hasNotText = %%-locator-option-has-not-text-%%
* since: v1.33 * since: v1.33
### option: Locator.filter.visible = %%-locator-option-visible-%%
* since: v1.51
## method: Locator.first ## method: Locator.first
* since: v1.14 * since: v1.14
- returns: <[Locator]> - returns: <[Locator]>
@ -2478,18 +2481,6 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.uncheck.trial = %%-input-trial-%% ### option: Locator.uncheck.trial = %%-input-trial-%%
* since: v1.14 * 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 ## async method: Locator.waitFor
* since: v1.16 * 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. 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-options-list-v1.14
- %%-locator-option-has-text-%% - %%-locator-option-has-text-%%
- %%-locator-option-has-%% - %%-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. * This will only find a second button, because it is visible, and then click it.
```js ```js
await page.locator('button').visible().click(); await page.locator('button').filter({ visible: true }).click();
``` ```
```java ```java
page.locator("button").visible().click(); page.locator("button").filter(new Locator.FilterOptions.setVisible(true)).click();
``` ```
```python async ```python async
await page.locator("button").visible().click() await page.locator("button").filter(visible=True).click()
``` ```
```python sync ```python sync
page.locator("button").visible().click() page.locator("button").filter(visible=True).click()
``` ```
```csharp ```csharp
await page.Locator("button").Visible().ClickAsync(); await page.Locator("button").Filter(new() { Visible = true }).ClickAsync();
``` ```
## Lists ## Lists

View file

@ -13129,6 +13129,11 @@ export interface Locator {
* `<article><div>Playwright</div></article>`. * `<article><div>Playwright</div></article>`.
*/ */
hasText?: string|RegExp; hasText?: string|RegExp;
/**
* Only matches visible or invisible elements.
*/
visible?: boolean;
}): Locator; }): Locator;
/** /**
@ -14520,17 +14525,6 @@ export interface Locator {
trial?: boolean; trial?: boolean;
}): Promise<void>; }): 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 * Returns when element specified by locator satisfies the
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option. * [`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; hasNotText?: string | RegExp;
has?: Locator; has?: Locator;
hasNot?: Locator; hasNot?: Locator;
visible?: boolean;
}; };
export class Locator implements api.Locator { 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); 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) if (this._frame._platform.inspectCustom)
(this as any)[this._frame._platform.inspectCustom] = () => this._inspect(); (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); 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)) if (isString(selectorOrLocator))
return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options); return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options);
if (selectorOrLocator._frame !== this._frame) if (selectorOrLocator._frame !== this._frame)
@ -218,11 +222,6 @@ export class Locator implements api.Locator {
return new Locator(this._frame, this._selector + ` >> nth=${index}`); 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 { and(locator: Locator): Locator {
if (locator._frame !== this._frame) if (locator._frame !== this._frame)
throw new Error(`Locators must belong to the same frame.`); throw new Error(`Locators must belong to the same frame.`);

View file

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

View file

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

View file

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

View file

@ -13129,6 +13129,11 @@ export interface Locator {
* `<article><div>Playwright</div></article>`. * `<article><div>Playwright</div></article>`.
*/ */
hasText?: string|RegExp; hasText?: string|RegExp;
/**
* Only matches visible or invisible elements.
*/
visible?: boolean;
}): Locator; }): Locator;
/** /**
@ -14520,17 +14525,6 @@ export interface Locator {
trial?: boolean; trial?: boolean;
}): Promise<void>; }): 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 * Returns when element specified by locator satisfies the
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option. * [`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 }) => { 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.getByText('hello').element.innerHTML`)).toContain('Hello');
expect(await page.evaluate(`playwright.getByTitle('world').element.innerHTML`)).toContain('World'); 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').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').first().element.innerHTML`)).toContain('Hello');
expect(await page.evaluate(`playwright.locator('span').last().element.innerHTML`)).toContain('World'); 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('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 }) => { 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 }) => { it('reverse engineer visible', async ({ page }) => {
expect.soft(generate(page.getByText('Hello').visible().locator('div'))).toEqual({ expect.soft(generate(page.getByText('Hello').filter({ visible: true }).locator('div'))).toEqual({
csharp: `GetByText("Hello").Visible().Locator("div")`, csharp: `GetByText("Hello").Filter(new() { Visible = true }).Locator("div")`,
java: `getByText("Hello").visible().locator("div")`, java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(true)).locator("div")`,
javascript: `getByText('Hello').visible().locator('div')`, javascript: `getByText('Hello').filter({ visible: true }).locator('div')`,
python: `get_by_text("Hello").visible().locator("div")`, python: `get_by_text("Hello").filter(visible=True).locator("div")`,
}); });
expect.soft(generate(page.getByText('Hello').visible({ visible: true }).locator('div'))).toEqual({ expect.soft(generate(page.getByText('Hello').filter({ visible: false }).locator('div'))).toEqual({
csharp: `GetByText("Hello").Visible().Locator("div")`, csharp: `GetByText("Hello").Filter(new() { Visible = false }).Locator("div")`,
java: `getByText("Hello").visible().locator("div")`, java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(false)).locator("div")`,
javascript: `getByText('Hello').visible().locator('div')`, javascript: `getByText('Hello').filter({ visible: false }).locator('div')`,
python: `get_by_text("Hello").visible().locator("div")`, python: `get_by_text("Hello").filter(visible=False).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")`,
}); });
}); });

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