feat: locator.visible (#34905)

This commit is contained in:
Dmitry Gozman 2025-02-25 11:48:15 +00:00 committed by GitHub
parent 9b633ddd2f
commit ed0bf35435
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 98 additions and 6 deletions

View File

@ -2478,6 +2478,18 @@ 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

@ -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').locator('visible=true').click();
await page.locator('button').visible().click();
```
```java
page.locator("button").locator("visible=true").click();
page.locator("button").visible().click();
```
```python async
await page.locator("button").locator("visible=true").click()
await page.locator("button").visible().click()
```
```python sync
page.locator("button").locator("visible=true").click()
page.locator("button").visible().click()
```
```csharp
await page.Locator("button").Locator("visible=true").ClickAsync();
await page.Locator("button").Visible().ClickAsync();
```
## Lists

View File

@ -14615,6 +14615,17 @@ 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

@ -218,6 +218,11 @@ 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

@ -21,7 +21,7 @@ import type { NestedSelectorBody } from './selectorParser';
import type { ParsedSelector } from './selectorParser';
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'frame-locator' | 'and' | 'or' | 'chain';
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'visible' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'frame-locator' | 'and' | 'or' | 'chain';
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
export type Quote = '\'' | '"' | '`';
@ -68,6 +68,10 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
tokens.push([factory.generateLocator(base, 'nth', part.body as string)]);
continue;
}
if (part.name === 'visible') {
tokens.push([factory.generateLocator(base, 'visible', part.body as string), factory.generateLocator(base, 'default', `visible=${part.body}`)]);
continue;
}
if (part.name === 'internal:text') {
const { exact, text } = detectExact(part.body as string);
tokens.push([factory.generateLocator(base, 'text', text, { exact })]);
@ -275,6 +279,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
return `first()`;
case 'last':
return `last()`;
case 'visible':
return `visible(${body === 'true' ? '' : '{ visible: false }'})`;
case 'role':
const attrs: string[] = [];
if (isRegExp(options.name)) {
@ -369,6 +375,8 @@ export class PythonLocatorFactory implements LocatorFactory {
return `first`;
case 'last':
return `last`;
case 'visible':
return `visible(${body === 'true' ? '' : 'visible=False'})`;
case 'role':
const attrs: string[] = [];
if (isRegExp(options.name)) {
@ -476,6 +484,8 @@ export class JavaLocatorFactory implements LocatorFactory {
return `first()`;
case 'last':
return `last()`;
case 'visible':
return `visible(${body === 'true' ? '' : `new ${clazz}.VisibleOptions().setVisible(false)`})`;
case 'role':
const attrs: string[] = [];
if (isRegExp(options.name)) {
@ -573,6 +583,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
return `First`;
case 'last':
return `Last`;
case 'visible':
return `Visible(${body === 'true' ? '' : 'new() { Visible = false }'})`;
case 'role':
const attrs: string[] = [];
if (isRegExp(options.name)) {

View File

@ -170,6 +170,9 @@ 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\(,?hastext=([^)]+)\)/g, 'internal:has-text=$1')
.replace(/filter\(,?hasnottext=([^)]+)\)/g, 'internal:has-not-text=$1')
.replace(/filter\(,?has2=([^)]+)\)/g, 'internal:has=$1')

View File

@ -14615,6 +14615,17 @@ 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

@ -320,6 +320,27 @@ 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').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")`,
});
});
it('reverse engineer has', async ({ page }) => {
expect.soft(generate(page.getByText('Hello').filter({ has: page.locator('div').getByText('bye') }))).toEqual({
csharp: `GetByText("Hello").Filter(new() { Has = Locator("div").GetByText("bye") })`,

View File

@ -150,6 +150,23 @@ 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 }) => {
await page.setContent(`<div>
<div class="item" style="display: none">Hidden data0</div>
<div class="item">visible data1</div>
<div class="item" style="display: none">Hidden data1</div>
<div class="item">visible data2</div>
<div class="item" style="display: none">Hidden data2</div>
<div class="item">visible data3</div>
</div>
`);
const locator = page.locator('.item').visible().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');
});
it('locator.count should work with deleted Map in main world', async ({ page }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/11254' });
await page.evaluate('Map = 1');