From ed0bf354354acc854a3d2410df177a824d0ac85e Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 25 Feb 2025 11:48:15 +0000 Subject: [PATCH] feat: locator.visible (#34905) --- docs/src/api/class-locator.md | 12 +++++++++++ docs/src/locators.md | 10 ++++----- packages/playwright-client/types/types.d.ts | 11 ++++++++++ .../playwright-core/src/client/locator.ts | 5 +++++ .../src/utils/isomorphic/locatorGenerators.ts | 14 ++++++++++++- .../src/utils/isomorphic/locatorParser.ts | 3 +++ packages/playwright-core/types/types.d.ts | 11 ++++++++++ tests/library/locator-generator.spec.ts | 21 +++++++++++++++++++ tests/page/locator-misc-2.spec.ts | 17 +++++++++++++++ 9 files changed, 98 insertions(+), 6 deletions(-) diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 85e3f0396a..c7680319d4 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -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 diff --git a/docs/src/locators.md b/docs/src/locators.md index c3a2817670..ed15a82762 100644 --- a/docs/src/locators.md +++ b/docs/src/locators.md @@ -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 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 72c502d913..874f072035 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -14615,6 +14615,17 @@ export interface Locator { trial?: boolean; }): Promise; + /** + * 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. diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 5d0f0aa0c3..1ed86b847d 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -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.`); diff --git a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts index 77ce63f313..283c4e492a 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts +++ b/packages/playwright-core/src/utils/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' | '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)) { diff --git a/packages/playwright-core/src/utils/isomorphic/locatorParser.ts b/packages/playwright-core/src/utils/isomorphic/locatorParser.ts index f89dbb50f7..e3481b0973 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorParser.ts @@ -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') diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 72c502d913..874f072035 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -14615,6 +14615,17 @@ export interface Locator { trial?: boolean; }): Promise; + /** + * 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. diff --git a/tests/library/locator-generator.spec.ts b/tests/library/locator-generator.spec.ts index 417e096e29..0a2ccb333a 100644 --- a/tests/library/locator-generator.spec.ts +++ b/tests/library/locator-generator.spec.ts @@ -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") })`, diff --git a/tests/page/locator-misc-2.spec.ts b/tests/page/locator-misc-2.spec.ts index 202b9a2957..478d86c30c 100644 --- a/tests/page/locator-misc-2.spec.ts +++ b/tests/page/locator-misc-2.spec.ts @@ -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(`
+ +
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({ 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');