diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index c7680319d4..42c09c79f1 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -1090,6 +1090,9 @@ await rowLocator ### option: Locator.filter.hasNotText = %%-locator-option-has-not-text-%% * since: v1.33 +### option: Locator.filter.visible = %%-locator-option-visible-%% +* since: v1.51 + ## method: Locator.first * since: v1.14 - returns: <[Locator]> @@ -2478,18 +2481,6 @@ 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/api/params.md b/docs/src/api/params.md index de67c7f1c7..a31ea6047c 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -1155,6 +1155,11 @@ Note that outer and inner locators must belong to the same frame. Inner locator Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element. When passed a [string], matching is case-insensitive and searches for a substring. +## locator-option-visible +- `visible` <[boolean]> + +Only matches visible or invisible elements. + ## locator-options-list-v1.14 - %%-locator-option-has-text-%% - %%-locator-option-has-%% diff --git a/docs/src/locators.md b/docs/src/locators.md index ed15a82762..b0a1c0e8f5 100644 --- a/docs/src/locators.md +++ b/docs/src/locators.md @@ -751,10 +751,10 @@ page.locator("x-details", new Page.LocatorOptions().setHasText("Details")) .click(); ``` ```python async -await page.locator("x-details", has_text="Details" ).click() +await page.locator("x-details", has_text="Details").click() ``` ```python sync -page.locator("x-details", has_text="Details" ).click() +page.locator("x-details", has_text="Details").click() ``` ```csharp await page @@ -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').visible().click(); + await page.locator('button').filter({ visible: true }).click(); ``` ```java - page.locator("button").visible().click(); + page.locator("button").filter(new Locator.FilterOptions.setVisible(true)).click(); ``` ```python async - await page.locator("button").visible().click() + await page.locator("button").filter(visible=True).click() ``` ```python sync - page.locator("button").visible().click() + page.locator("button").filter(visible=True).click() ``` ```csharp - await page.Locator("button").Visible().ClickAsync(); + await page.Locator("button").Filter(new() { Visible = true }).ClickAsync(); ``` ## Lists diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 26115f729a..abdc85e923 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -13129,6 +13129,11 @@ export interface Locator { * `
Playwright
`. */ hasText?: string|RegExp; + + /** + * Only matches visible or invisible elements. + */ + visible?: boolean; }): Locator; /** @@ -14520,17 +14525,6 @@ 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 1ed86b847d..a915c0a8ef 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -35,6 +35,7 @@ export type LocatorOptions = { hasNotText?: string | RegExp; has?: Locator; hasNot?: Locator; + visible?: boolean; }; export class Locator implements api.Locator { @@ -65,6 +66,9 @@ export class Locator implements api.Locator { this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector); } + if (options?.visible !== undefined) + this._selector += ` >> visible=${options.visible ? 'true' : 'false'}`; + if (this._frame._platform.inspectCustom) (this as any)[this._frame._platform.inspectCustom] = () => this._inspect(); } @@ -150,7 +154,7 @@ export class Locator implements api.Locator { return await this._frame._highlight(this._selector); } - locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator { + locator(selectorOrLocator: string | Locator, options?: Omit): Locator { if (isString(selectorOrLocator)) return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options); if (selectorOrLocator._frame !== this._frame) @@ -218,11 +222,6 @@ 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/server/injected/consoleApi.ts b/packages/playwright-core/src/server/injected/consoleApi.ts index c79ee6610c..5bcd94a5bf 100644 --- a/packages/playwright-core/src/server/injected/consoleApi.ts +++ b/packages/playwright-core/src/server/injected/consoleApi.ts @@ -29,7 +29,7 @@ class Locator { element: Element | undefined; elements: Element[] | undefined; - constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator }) { + constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator, visible?: boolean }) { if (options?.hasText) selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`; if (options?.hasNotText) @@ -38,6 +38,8 @@ class Locator { selector += ` >> internal:has=` + JSON.stringify(options.has[selectorSymbol]); if (options?.hasNot) selector += ` >> internal:has-not=` + JSON.stringify(options.hasNot[selectorSymbol]); + if (options?.visible !== undefined) + selector += ` >> visible=${options.visible ? 'true' : 'false'}`; this[selectorSymbol] = selector; if (selector) { const parsed = injectedScript.parseSelector(selector); @@ -46,7 +48,7 @@ class Locator { } const selectorBase = selector; const self = this as any; - self.locator = (selector: string, options?: { hasText?: string | RegExp, has?: Locator }): Locator => { + self.locator = (selector: string, options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator }): Locator => { return new Locator(injectedScript, selectorBase ? selectorBase + ' >> ' + selector : selector, options); }; self.getByTestId = (testId: string): Locator => self.locator(getByTestIdSelector(injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen(), testId)); @@ -56,7 +58,7 @@ class Locator { self.getByText = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTextSelector(text, options)); 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, has?: Locator }): Locator => new Locator(injectedScript, selector, options); + self.filter = (options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator, visible?: boolean }): Locator => new Locator(injectedScript, selector, options); self.first = (): Locator => self.locator('nth=0'); self.last = (): Locator => self.locator('nth=-1'); self.nth = (index: number): Locator => self.locator(`nth=${index}`); diff --git a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts index 283c4e492a..5be1f36b96 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts @@ -280,7 +280,7 @@ export class JavaScriptLocatorFactory implements LocatorFactory { case 'last': return `last()`; case 'visible': - return `visible(${body === 'true' ? '' : '{ visible: false }'})`; + return `filter({ visible: ${body === 'true' ? 'true' : 'false'} })`; case 'role': const attrs: string[] = []; if (isRegExp(options.name)) { @@ -376,7 +376,7 @@ export class PythonLocatorFactory implements LocatorFactory { case 'last': return `last`; case 'visible': - return `visible(${body === 'true' ? '' : 'visible=False'})`; + return `filter(visible=${body === 'true' ? 'True' : 'False'})`; case 'role': const attrs: string[] = []; if (isRegExp(options.name)) { @@ -485,7 +485,7 @@ export class JavaLocatorFactory implements LocatorFactory { case 'last': return `last()`; case 'visible': - return `visible(${body === 'true' ? '' : `new ${clazz}.VisibleOptions().setVisible(false)`})`; + return `filter(new ${clazz}.FilterOptions().setVisible(${body === 'true' ? 'true' : 'false'}))`; case 'role': const attrs: string[] = []; if (isRegExp(options.name)) { @@ -584,7 +584,7 @@ export class CSharpLocatorFactory implements LocatorFactory { case 'last': return `Last`; case 'visible': - return `Visible(${body === 'true' ? '' : 'new() { Visible = false }'})`; + return `Filter(new() { Visible = ${body === 'true' ? 'true' : '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 e3481b0973..6dcbc1cbcc 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorParser.ts @@ -170,9 +170,8 @@ 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\(,?visible=true\)/g, 'visible=true') + .replace(/filter\(,?visible=false\)/g, 'visible=false') .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 26115f729a..abdc85e923 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -13129,6 +13129,11 @@ export interface Locator { * `
Playwright
`. */ hasText?: string|RegExp; + + /** + * Only matches visible or invisible elements. + */ + visible?: boolean; }): Locator; /** @@ -14520,17 +14525,6 @@ 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/inspector/console-api.spec.ts b/tests/library/inspector/console-api.spec.ts index 50f4c5f063..2b40b8943a 100644 --- a/tests/library/inspector/console-api.spec.ts +++ b/tests/library/inspector/console-api.spec.ts @@ -92,13 +92,14 @@ it('should support locator.or()', async ({ page }) => { }); it('should support playwright.getBy*', async ({ page }) => { - await page.setContent('HelloWorld'); + await page.setContent('HelloWorld
one
two
'); expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello'); expect(await page.evaluate(`playwright.getByTitle('world').element.innerHTML`)).toContain('World'); expect(await page.evaluate(`playwright.locator('span').filter({ hasText: 'hello' }).element.innerHTML`)).toContain('Hello'); expect(await page.evaluate(`playwright.locator('span').first().element.innerHTML`)).toContain('Hello'); 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'); }); 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 0a2ccb333a..cc54a143e8 100644 --- a/tests/library/locator-generator.spec.ts +++ b/tests/library/locator-generator.spec.ts @@ -321,23 +321,17 @@ 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').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({ 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")`, + 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")`, }); }); diff --git a/tests/page/locator-misc-2.spec.ts b/tests/page/locator-misc-2.spec.ts index 478d86c30c..f09749eca5 100644 --- a/tests/page/locator-misc-2.spec.ts +++ b/tests/page/locator-misc-2.spec.ts @@ -150,7 +150,7 @@ 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 }) => { +it('should support filter(visible)', async ({ page }) => { await page.setContent(`
visible data1
@@ -160,11 +160,10 @@ it('should support .visible()', async ({ page }) => {
visible data3
`); - const locator = page.locator('.item').visible().nth(1); + const locator = page.locator('.item').filter({ visible: true }).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'); + await expect(page.locator('.item').filter({ visible: true }).getByText('data3')).toHaveText('visible data3'); + await expect(page.locator('.item').filter({ visible: false }).getByText('data1')).toHaveText('Hidden data1'); }); it('locator.count should work with deleted Map in main world', async ({ page }) => {