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(`
+ +
visible data1
+ +
visible 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