Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/src/api/class-locator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<button style='display: none'>Invisible</button>
<button>Visible</button>
```

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

Expand Down
2 changes: 1 addition & 1 deletion docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-%%
Expand Down
12 changes: 7 additions & 5 deletions docs/src/locators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/src/other-locators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/injected/src/consoleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down Expand Up @@ -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;
Expand Down
16 changes: 14 additions & 2 deletions packages/isomorphic/locatorGenerators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '\'' | '"' | '`';

Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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[] = [];
Expand Down
1 change: 1 addition & 0 deletions packages/isomorphic/locatorParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
32 changes: 31 additions & 1 deletion packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -17205,6 +17207,34 @@ export interface Locator {
trial?: boolean;
}): Promise<void>;

/**
* 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
* <button style='display: none'>Invisible</button>
* <button>Visible</button>
* ```
*
* 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.
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright-core/src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ElementHandle<SVGElement | HTMLElement>> {
return await this._frame.waitForSelector(this._selector, { strict: true, state: 'attached', ...options })!;
}
Expand Down
32 changes: 31 additions & 1 deletion packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -17205,6 +17207,34 @@ export interface Locator {
trial?: boolean;
}): Promise<void>;

/**
* 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
* <button style='display: none'>Invisible</button>
* <button>Visible</button>
* ```
*
* 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.
Expand Down
1 change: 1 addition & 0 deletions tests/library/inspector/console-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
15 changes: 10 additions & 5 deletions tests/library/locator-generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,18 +357,23 @@ 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")`,
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")`,
});
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 }) => {
Expand Down
16 changes: 16 additions & 0 deletions tests/page/locator-misc-2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<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()).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');
Expand Down
10 changes: 5 additions & 5 deletions tests/playwright-test/test-step.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading