diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md
index 7fbe664d6ae81..d7ee7012f0539 100644
--- a/docs/src/api/class-locator.md
+++ b/docs/src/api/class-locator.md
@@ -2908,6 +2908,47 @@ 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.63
+- returns: <[Locator]>
+
+Returns a locator that matches only [visible](../actionability.md#visible) elements, ignoring the invisible ones. This is the recommended way to distinguish elements by visibility, as opposed to the `:visible` CSS pseudo-class.
+
+Note that visibility is checked every time the locator is used, and not at the moment of the [`method: Locator.visible`] call.
+
+**Usage**
+
+Consider a page with two buttons, the first invisible and the second visible.
+
+```html
+
+
+```
+
+This will only find the second button, because it is visible, and then click it.
+
+```js
+await page.locator('button').visible().click();
+```
+
+```java
+page.locator("button").visible().click();
+```
+
+```python async
+await page.locator("button").visible.click()
+```
+
+```python sync
+page.locator("button").visible.click()
+```
+
+```csharp
+await page.Locator("button").Visible.ClickAsync();
+```
+
+To match invisible elements instead, use [`method: Locator.filter`] with the [`option: Locator.filter.visible`] option set to `false`.
+
## async method: Locator.waitFor
* since: v1.16
diff --git a/docs/src/api/params.md b/docs/src/api/params.md
index bf7a70b9074db..02bd0483dc413 100644
--- a/docs/src/api/params.md
+++ b/docs/src/api/params.md
@@ -1285,7 +1285,7 @@ Matches elements that do not contain specified text somewhere inside, possibly i
## locator-option-visible
- `visible` <[boolean]>
-Only matches visible or invisible elements.
+Only matches visible or invisible elements. Prefer the [`method: Locator.visible`] shortcut when matching only visible elements.
## locator-options-list-v1.14
- %%-locator-option-has-text-%%
diff --git a/docs/src/locators.md b/docs/src/locators.md
index 3a6a5de389fd6..dd3655859c462 100644
--- a/docs/src/locators.md
+++ b/docs/src/locators.md
@@ -1310,21 +1310,23 @@ 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').filter({ visible: true }).click();
+ await page.locator('button').visible().click();
```
```java
- page.locator("button").filter(new Locator.FilterOptions().setVisible(true)).click();
+ page.locator("button").visible().click();
```
```python async
- await page.locator("button").filter(visible=True).click()
+ await page.locator("button").visible.click()
```
```python sync
- page.locator("button").filter(visible=True).click()
+ page.locator("button").visible.click()
```
```csharp
- await page.Locator("button").Filter(new() { Visible = true }).ClickAsync();
+ await page.Locator("button").Visible.ClickAsync();
```
+To match invisible elements instead, use [`method: Locator.filter`] with the [`option: Locator.filter.visible`] option set to `false`.
+
## Lists
### Count items in a list
diff --git a/docs/src/other-locators.md b/docs/src/other-locators.md
index 499464866cb62..439b10d39f701 100644
--- a/docs/src/other-locators.md
+++ b/docs/src/other-locators.md
@@ -126,6 +126,10 @@ Input elements of the type `button` and `submit` are matched by their `value` in
Playwright supports the `:visible` pseudo class in CSS selectors. For example, `css=button` matches all the buttons on the page, while `css=button:visible` only matches visible buttons. This is useful to distinguish elements that are very similar but differ in visibility.
+:::note
+Prefer the [`method: Locator.visible`] shortcut over the `:visible` pseudo-class.
+:::
+
Consider a page with two buttons, first invisible and second visible.
```html
diff --git a/packages/injected/src/consoleApi.ts b/packages/injected/src/consoleApi.ts
index 8cd9b5beaa0c6..cdc073c821fe9 100644
--- a/packages/injected/src/consoleApi.ts
+++ b/packages/injected/src/consoleApi.ts
@@ -60,6 +60,7 @@ class Locator {
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, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator, visible?: boolean }): Locator => new Locator(injectedScript, selector, options);
+ self.visible = (): Locator => new Locator(injectedScript, selector, { visible: true });
self.first = (): Locator => self.locator('nth=0');
self.last = (): Locator => self.locator('nth=-1');
self.nth = (index: number): Locator => self.locator(`nth=${index}`);
@@ -99,6 +100,7 @@ export class ConsoleAPI {
...new Locator(this._injectedScript, ''),
};
delete this._injectedScript.window.playwright.filter;
+ delete this._injectedScript.window.playwright.visible;
delete this._injectedScript.window.playwright.first;
delete this._injectedScript.window.playwright.last;
delete this._injectedScript.window.playwright.nth;
diff --git a/packages/isomorphic/locatorGenerators.ts b/packages/isomorphic/locatorGenerators.ts
index 40b6a9a973d93..942c5d1c3ceb6 100644
--- a/packages/isomorphic/locatorGenerators.ts
+++ b/packages/isomorphic/locatorGenerators.ts
@@ -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' | 'visible' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'frame-locator' | 'any-frame' | 'and' | 'or' | 'chain';
+export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'visible' | 'filter-visible' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'frame-locator' | 'any-frame' | 'and' | 'or' | 'chain';
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
export type Quote = '\'' | '"' | '`';
@@ -104,7 +104,11 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
continue;
}
if (part.name === 'visible') {
- tokens.push([factory.generateLocator(base, 'visible', part.body as string), factory.generateLocator(base, 'default', `visible=${part.body}`)]);
+ const tokenList: string[] = [];
+ if (part.body === 'true')
+ tokenList.push(factory.generateLocator(base, 'visible', ''));
+ tokenList.push(factory.generateLocator(base, 'filter-visible', part.body as string), factory.generateLocator(base, 'default', `visible=${part.body}`));
+ tokens.push(tokenList);
continue;
}
if (part.name === 'internal:text') {
@@ -329,6 +333,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
case 'last':
return `last()`;
case 'visible':
+ return `visible()`;
+ case 'filter-visible':
return `filter({ visible: ${body === 'true' ? 'true' : 'false'} })`;
case 'role':
const attrs: string[] = [];
@@ -430,6 +436,8 @@ export class PythonLocatorFactory implements LocatorFactory {
case 'last':
return `last`;
case 'visible':
+ return `visible`;
+ case 'filter-visible':
return `filter(visible=${body === 'true' ? 'True' : 'False'})`;
case 'role':
const attrs: string[] = [];
@@ -544,6 +552,8 @@ export class JavaLocatorFactory implements LocatorFactory {
case 'last':
return `last()`;
case 'visible':
+ return `visible()`;
+ case 'filter-visible':
return `filter(new ${clazz}.FilterOptions().setVisible(${body === 'true' ? 'true' : 'false'}))`;
case 'role':
const attrs: string[] = [];
@@ -648,6 +658,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
case 'last':
return `Last`;
case 'visible':
+ return `Visible`;
+ case 'filter-visible':
return `Filter(new() { Visible = ${body === 'true' ? 'true' : 'false'} })`;
case 'role':
const attrs: string[] = [];
diff --git a/packages/isomorphic/locatorParser.ts b/packages/isomorphic/locatorParser.ts
index bd2d2e4ab86a7..6f5c8ac31efd2 100644
--- a/packages/isomorphic/locatorParser.ts
+++ b/packages/isomorphic/locatorParser.ts
@@ -174,6 +174,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
.replace(/nth\(([^)]+)\)/g, 'nth=$1')
.replace(/filter\(,?visible=true\)/g, 'visible=true')
.replace(/filter\(,?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')
diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts
index 5831ea1edee2b..74cb7ada8b6d3 100644
--- a/packages/playwright-client/types/types.d.ts
+++ b/packages/playwright-client/types/types.d.ts
@@ -15532,7 +15532,9 @@ export interface Locator {
hasText?: string|RegExp;
/**
- * Only matches visible or invisible elements.
+ * Only matches visible or invisible elements. Prefer the
+ * [locator.visible()](https://playwright.dev/docs/api/class-locator#locator-visible) shortcut when matching only
+ * visible elements.
*/
visible?: boolean;
}): Locator;
@@ -17205,6 +17207,34 @@ export interface Locator {
trial?: boolean;
}): Promise;
+ /**
+ * Returns a locator that matches only [visible](https://playwright.dev/docs/actionability#visible) elements, ignoring the invisible ones.
+ * This is the recommended way to distinguish elements by visibility, as opposed to the `:visible` CSS pseudo-class.
+ *
+ * Note that visibility is checked every time the locator is used, and not at the moment of the
+ * [locator.visible()](https://playwright.dev/docs/api/class-locator#locator-visible) call.
+ *
+ * **Usage**
+ *
+ * Consider a page with two buttons, the first invisible and the second visible.
+ *
+ * ```html
+ *
+ *
+ * ```
+ *
+ * This will only find the second button, because it is visible, and then click it.
+ *
+ * ```js
+ * await page.locator('button').visible().click();
+ * ```
+ *
+ * To match invisible elements instead, use
+ * [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) with the
+ * [`visible`](https://playwright.dev/docs/api/class-locator#locator-filter-option-visible) option set to `false`.
+ */
+ visible(): Locator;
+
/**
* Returns when element specified by locator satisfies the
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option.
diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts
index 1ae9faffcece6..fa3e1fdefc3f8 100644
--- a/packages/playwright-core/src/client/locator.ts
+++ b/packages/playwright-core/src/client/locator.ts
@@ -216,6 +216,10 @@ export class Locator implements api.Locator {
return new Locator(this._frame, this._selector, options);
}
+ visible(): Locator {
+ return new Locator(this._frame, this._selector, { visible: true });
+ }
+
async elementHandle(options?: TimeoutOptions): Promise> {
return await this._frame.waitForSelector(this._selector, { strict: true, state: 'attached', ...options })!;
}
diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts
index 5831ea1edee2b..74cb7ada8b6d3 100644
--- a/packages/playwright-core/types/types.d.ts
+++ b/packages/playwright-core/types/types.d.ts
@@ -15532,7 +15532,9 @@ export interface Locator {
hasText?: string|RegExp;
/**
- * Only matches visible or invisible elements.
+ * Only matches visible or invisible elements. Prefer the
+ * [locator.visible()](https://playwright.dev/docs/api/class-locator#locator-visible) shortcut when matching only
+ * visible elements.
*/
visible?: boolean;
}): Locator;
@@ -17205,6 +17207,34 @@ export interface Locator {
trial?: boolean;
}): Promise;
+ /**
+ * Returns a locator that matches only [visible](https://playwright.dev/docs/actionability#visible) elements, ignoring the invisible ones.
+ * This is the recommended way to distinguish elements by visibility, as opposed to the `:visible` CSS pseudo-class.
+ *
+ * Note that visibility is checked every time the locator is used, and not at the moment of the
+ * [locator.visible()](https://playwright.dev/docs/api/class-locator#locator-visible) call.
+ *
+ * **Usage**
+ *
+ * Consider a page with two buttons, the first invisible and the second visible.
+ *
+ * ```html
+ *
+ *
+ * ```
+ *
+ * This will only find the second button, because it is visible, and then click it.
+ *
+ * ```js
+ * await page.locator('button').visible().click();
+ * ```
+ *
+ * To match invisible elements instead, use
+ * [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) with the
+ * [`visible`](https://playwright.dev/docs/api/class-locator#locator-filter-option-visible) option set to `false`.
+ */
+ visible(): Locator;
+
/**
* Returns when element specified by locator satisfies the
* [`state`](https://playwright.dev/docs/api/class-locator#locator-wait-for-option-state) option.
diff --git a/tests/library/inspector/console-api.spec.ts b/tests/library/inspector/console-api.spec.ts
index ebc9cef72ac95..2951f7bcb7cbf 100644
--- a/tests/library/inspector/console-api.spec.ts
+++ b/tests/library/inspector/console-api.spec.ts
@@ -98,6 +98,7 @@ it('should support playwright.getBy*', async ({ page }) => {
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');
+ expect(await page.evaluate(`playwright.locator('div').visible().element.innerHTML`)).toContain('one');
});
it('expected properties on playwright object', async ({ page }) => {
diff --git a/tests/library/locator-generator.spec.ts b/tests/library/locator-generator.spec.ts
index 595a7d193066c..5f3c7e541dcb2 100644
--- a/tests/library/locator-generator.spec.ts
+++ b/tests/library/locator-generator.spec.ts
@@ -357,11 +357,11 @@ it('reverse engineer hasNotText', async ({ page }) => {
});
it('reverse engineer visible', async ({ page }) => {
- 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().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: false }).locator('div'))).toEqual({
csharp: `GetByText("Hello").Filter(new() { Visible = false }).Locator("div")`,
@@ -369,6 +369,11 @@ it('reverse engineer visible', async ({ page }) => {
javascript: `getByText('Hello').filter({ visible: false }).locator('div')`,
python: `get_by_text("Hello").filter(visible=False).locator("div")`,
});
+ const selector = (page.getByText('Hello').visible() as any)._selector;
+ expect.soft(parseLocator('javascript', `getByText('Hello').filter({ visible: true })`, 'data-testid')).toBe(selector);
+ expect.soft(parseLocator('java', `getByText("Hello").filter(new Locator.FilterOptions().setVisible(true))`, 'data-testid')).toBe(selector);
+ expect.soft(parseLocator('python', `get_by_text("Hello").filter(visible=True)`, 'data-testid')).toBe(selector);
+ expect.soft(parseLocator('csharp', `GetByText("Hello").Filter(new() { Visible = true })`, 'data-testid')).toBe(selector);
});
it('reverse engineer has', async ({ page }) => {
diff --git a/tests/page/locator-misc-2.spec.ts b/tests/page/locator-misc-2.spec.ts
index d28463a730bf2..0b921928bfdfa 100644
--- a/tests/page/locator-misc-2.spec.ts
+++ b/tests/page/locator-misc-2.spec.ts
@@ -161,6 +161,22 @@ it('should support filter(visible)', async ({ page }) => {
await expect(page.locator('.item').filter({ visible: false }).getByText('data1')).toHaveText('Hidden data1');
});
+it('should support visible()', async ({ page }) => {
+ await page.setContent(`
+
Hidden data0
+
visible data1
+
Hidden data1
+
visible data2
+
Hidden data2
+
visible data3
+
+ `);
+ 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()).toHaveCount(3);
+});
+
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');
diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts
index 559e00323fb00..c563e284ddda7 100644
--- a/tests/playwright-test/test-step.spec.ts
+++ b/tests/playwright-test/test-step.spec.ts
@@ -1767,19 +1767,19 @@ pw:api | Create page
pw:api |Set content @ a.test.ts:16
expect |Expect "toBeInvisible" locator('div') @ a.test.ts:17
expect | Expect "poll toBe" @ a.test.ts:7
-pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7
+pw:api | Query count locator('div').visible() @ a.test.ts:7
expect | Expect "toBe" @ a.test.ts:7
expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality
-pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7
+pw:api | Query count locator('div').visible() @ a.test.ts:7
expect | Expect "toBe" @ a.test.ts:7
expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality
-pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7
+pw:api | Query count locator('div').visible() @ a.test.ts:7
expect | Expect "toBe" @ a.test.ts:7
expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality
-pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7
+pw:api | Query count locator('div').visible() @ a.test.ts:7
expect | Expect "toBe" @ a.test.ts:7
expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality
-pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7
+pw:api | Query count locator('div').visible() @ a.test.ts:7
expect | Expect "toBe" @ a.test.ts:7
pw:api |Wait for timeout @ a.test.ts:18
pw:api |Set content @ a.test.ts:19