From 04e82ce71cceda36a202a0ca9ffa3e38c7c5c096 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 14 Dec 2021 15:37:31 -0800 Subject: [PATCH] feat(api): make withText an option (#10922) --- docs/src/api/class-frame.md | 1 + docs/src/api/class-framelocator.md | 1 + docs/src/api/class-locator.md | 13 +---- docs/src/api/class-page.md | 1 + docs/src/api/params.md | 9 ++++ docs/src/cli.md | 2 +- docs/src/inspector.md | 2 +- packages/html-reporter/src/headerView.spec.ts | 20 ++++---- packages/playwright-core/src/client/frame.ts | 4 +- .../playwright-core/src/client/locator.ts | 24 +++++---- packages/playwright-core/src/client/page.ts | 4 +- .../server/supplements/injected/consoleApi.ts | 24 ++++----- packages/playwright-core/types/types.d.ts | 50 +++++++++++++------ tests/inspector/console-api.spec.ts | 6 +-- tests/page/locator-query.spec.ts | 12 ++--- 15 files changed, 99 insertions(+), 74 deletions(-) diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index 8c38e899f5..34b59c1a05 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -989,6 +989,7 @@ The method returns an element locator that can be used to perform actions in the Locator is resolved to the element immediately before performing an action, so a series of actions on the same locator can in fact be performed on different DOM elements. That would happen if the DOM structure between those actions has changed. ### param: Frame.locator.selector = %%-find-selector-%% +### option: Frame.locator.-inline- = %%-locator-options-list-%% ## method: Frame.name - returns: <[string]> diff --git a/docs/src/api/class-framelocator.md b/docs/src/api/class-framelocator.md index bb2ad7da0b..9e57a0be87 100644 --- a/docs/src/api/class-framelocator.md +++ b/docs/src/api/class-framelocator.md @@ -122,6 +122,7 @@ Returns locator to the last matching frame. The method finds an element matching the specified selector in the FrameLocator's subtree. ### param: FrameLocator.locator.selector = %%-find-selector-%% +### option: FrameLocator.locator.-inline- = %%-locator-options-list-%% ## method: FrameLocator.nth diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 4b13d5a4af..1c2bf4679b 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -541,6 +541,7 @@ Returns locator to the last matching element. The method finds an element matching the specified selector in the `Locator`'s subtree. ### param: Locator.locator.selector = %%-find-selector-%% +### option: Locator.locator.-inline- = %%-locator-options-list-%% ## method: Locator.nth - returns: <[Locator]> @@ -908,15 +909,3 @@ orderSent.WaitForAsync(); ### option: Locator.waitFor.state = %%-wait-for-selector-state-%% ### option: Locator.waitFor.timeout = %%-input-timeout-%% - -## method: Locator.withText -- returns: <[Locator]> - -Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example, `"Playwright"` -matches `
Playwright
`. - - -### param: Locator.withText.text -- `text` <[string]|[RegExp]> - -Text to filter by as a string or as a regular expression. diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 40b87c1d40..68a39f1327 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2113,6 +2113,7 @@ Locator is resolved to the element immediately before performing an action, so a Shortcut for main frame's [`method: Frame.locator`]. ### param: Page.locator.selector = %%-find-selector-%% +### option: Page.locator.-inline- = %%-locator-options-list-%% ## method: Page.mainFrame - returns: <[Frame]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index d311a3dda8..bff26dbf18 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -868,3 +868,12 @@ Slows down Playwright operations by the specified amount of milliseconds. Useful - %%-browser-option-proxy-%% - %%-browser-option-timeout-%% - %%-browser-option-tracesdir-%% + +## locator-option-has-text +- `hasText` <[string]|[RegExp]> + +Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. +For example, `"Playwright"` matches `
Playwright
`. + +## locator-options-list +- %%-locator-option-has-text-%% diff --git a/docs/src/cli.md b/docs/src/cli.md index 63a21d2b58..0c4f7a0725 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -432,7 +432,7 @@ Reveal element in the Elements panel (if DevTools of the respective browser supp Query Playwright element using the actual Playwright query engine, for example: ```js -> playwright.locator('.auth-form').withText('Log in'); +> playwright.locator('.auth-form', { hasText: 'Log in' }); > Locator () > - element: button diff --git a/docs/src/inspector.md b/docs/src/inspector.md index a76f4b52f3..13f8f76ddd 100644 --- a/docs/src/inspector.md +++ b/docs/src/inspector.md @@ -188,7 +188,7 @@ Reveal element in the Elements panel (if DevTools of the respective browser supp Query Playwright element using the actual Playwright query engine, for example: ```js -> playwright.locator('.auth-form').withText('Log in'); +> playwright.locator('.auth-form', { hasText: 'Log in' }); > Locator () > - element: button diff --git a/packages/html-reporter/src/headerView.spec.ts b/packages/html-reporter/src/headerView.spec.ts index 7219407de6..1096461d93 100644 --- a/packages/html-reporter/src/headerView.spec.ts +++ b/packages/html-reporter/src/headerView.spec.ts @@ -30,11 +30,11 @@ test('should render counters', async ({ renderComponent }) => { duration: 100000 }; const component = await renderComponent('HeaderView', { stats }); - await expect(component.locator('a').withText('All').locator('.counter')).toHaveText('100'); - await expect(component.locator('a').withText('Passed').locator('.counter')).toHaveText('42'); - await expect(component.locator('a').withText('Failed').locator('.counter')).toHaveText('31'); - await expect(component.locator('a').withText('Flaky').locator('.counter')).toHaveText('17'); - await expect(component.locator('a').withText('Skipped').locator('.counter')).toHaveText('10'); + await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('100'); + await expect(component.locator('a', { hasText: 'Passed' }).locator('.counter')).toHaveText('42'); + await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31'); + await expect(component.locator('a', { hasText: 'Flaky' }).locator('.counter')).toHaveText('17'); + await expect(component.locator('a', { hasText: 'Skipped' }).locator('.counter')).toHaveText('10'); }); test('should toggle filters', async ({ page, renderComponent }) => { @@ -52,14 +52,14 @@ test('should toggle filters', async ({ page, renderComponent }) => { stats, setFilterText: (filterText: string) => filters.push(filterText) }); - await component.locator('a').withText('All').click(); - await component.locator('a').withText('Passed').click(); + await component.locator('a', { hasText: 'All' }).click(); + await component.locator('a', { hasText: 'Passed' }).click(); await expect(page).toHaveURL(/#\?q=s:passed/); - await component.locator('a').withText('Failed').click(); + await component.locator('a', { hasText: 'Failed' }).click(); await expect(page).toHaveURL(/#\?q=s:failed/); - await component.locator('a').withText('Flaky').click(); + await component.locator('a', { hasText: 'Flaky' }).click(); await expect(page).toHaveURL(/#\?q=s:flaky/); - await component.locator('a').withText('Skipped').click(); + await component.locator('a', { hasText: 'Skipped' }).click(); await expect(page).toHaveURL(/#\?q=s:skipped/); expect(filters).toEqual(['', 's:passed', 's:failed', 's:flaky', 's:skipped']); }); diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index b8dcc0e101..ed2769a5ee 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -280,8 +280,8 @@ export class Frame extends ChannelOwner implements api.Fr return await this._channel.fill({ selector, value, ...options }); } - locator(selector: string): Locator { - return new Locator(this, selector); + locator(selector: string, options?: { hasText?: string | RegExp }): Locator { + return new Locator(this, selector, options); } frameLocator(selector: string): FrameLocator { diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 2e4c37081a..272f5787f1 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -29,9 +29,17 @@ export class Locator implements api.Locator { private _frame: Frame; private _selector: string; - constructor(frame: Frame, selector: string) { + constructor(frame: Frame, selector: string, options?: { hasText?: string | RegExp }) { this._frame = frame; this._selector = selector; + + if (options?.hasText) { + const text = options.hasText; + if (isRegExp(text)) + this._selector += ` >> :scope:text-matches(${escapeWithQuotes(text.source, '"')}, "${text.flags}")`; + else + this._selector += ` >> :scope:has-text(${escapeWithQuotes(text, '"')})`; + } } private async _withElement(task: (handle: ElementHandle, timeout?: number) => Promise, timeout?: number): Promise { @@ -94,14 +102,8 @@ export class Locator implements api.Locator { return this._frame.fill(this._selector, value, { strict: true, ...options }); } - locator(selector: string): Locator { - return new Locator(this._frame, this._selector + ' >> ' + selector); - } - - withText(text: string | RegExp): Locator { - if (isRegExp(text)) - return new Locator(this._frame, this._selector + ` >> :scope:text-matches(${escapeWithQuotes(text.source, '"')}, "${text.flags}")`); - return new Locator(this._frame, this._selector + ` >> :scope:has-text(${escapeWithQuotes(text, '"')})`); + locator(selector: string, options?: { hasText?: string | RegExp }): Locator { + return new Locator(this._frame, this._selector + ' >> ' + selector, options); } frameLocator(selector: string): FrameLocator { @@ -269,8 +271,8 @@ export class FrameLocator implements api.FrameLocator { this._frameSelector = selector; } - locator(selector: string): Locator { - return new Locator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector); + locator(selector: string, options?: { hasText?: string | RegExp }): Locator { + return new Locator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector, options); } frameLocator(selector: string): FrameLocator { diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index bd404fdf67..33b1e93841 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -505,8 +505,8 @@ export class Page extends ChannelOwner implements api.Page return this._mainFrame.fill(selector, value, options); } - locator(selector: string): Locator { - return this.mainFrame().locator(selector); + locator(selector: string, options?: { hasText?: string | RegExp }): Locator { + return this.mainFrame().locator(selector, options); } frameLocator(selector: string): FrameLocator { diff --git a/packages/playwright-core/src/server/supplements/injected/consoleApi.ts b/packages/playwright-core/src/server/supplements/injected/consoleApi.ts index 64589b4bcc..ee050302ad 100644 --- a/packages/playwright-core/src/server/supplements/injected/consoleApi.ts +++ b/packages/playwright-core/src/server/supplements/injected/consoleApi.ts @@ -18,30 +18,30 @@ import { escapeWithQuotes } from '../../../utils/stringUtils'; import type InjectedScript from '../../injected/injectedScript'; import { generateSelector } from '../../injected/selectorGenerator'; -function createLocator(injectedScript: InjectedScript, initial: string) { +function createLocator(injectedScript: InjectedScript, initial: string, options?: { hasText?: string | RegExp }) { class Locator { selector: string; element: Element | undefined; elements: Element[]; - constructor(selector: string) { + constructor(selector: string, options?: { hasText?: string | RegExp }) { this.selector = selector; + if (options?.hasText) { + const text = options.hasText; + const matcher = text instanceof RegExp ? 'text-matches' : 'has-text'; + const source = escapeWithQuotes(text instanceof RegExp ? text.source : text, '"'); + this.selector += ` >> :scope:${matcher}(${source})`; + } const parsed = injectedScript.parseSelector(this.selector); this.element = injectedScript.querySelector(parsed, document, false); this.elements = injectedScript.querySelectorAll(parsed, document); } - locator(selector: string): Locator { - return new Locator(this.selector ? this.selector + ' >> ' + selector : selector); - } - - withText(text: string | RegExp): Locator { - const matcher = text instanceof RegExp ? 'text-matches' : 'has-text'; - const source = escapeWithQuotes(text instanceof RegExp ? text.source : text, '"'); - return new Locator(this.selector + ` >> :scope:${matcher}(${source})`); + locator(selector: string, options?: { hasText: string | RegExp }): Locator { + return new Locator(this.selector ? this.selector + ' >> ' + selector : selector, options); } } - return new Locator(initial); + return new Locator(initial, options); } type ConsoleAPIInterface = { @@ -71,7 +71,7 @@ export class ConsoleAPI { window.playwright = { $: (selector: string, strict?: boolean) => this._querySelector(selector, !!strict), $$: (selector: string) => this._querySelectorAll(selector), - locator: (selector: string) => createLocator(this._injectedScript, selector), + locator: (selector: string, options?: { hasText?: string | RegExp }) => createLocator(this._injectedScript, selector, options), inspect: (selector: string) => this._inspect(selector), selector: (element: Element) => this._selector(element), resume: () => this._resume(), diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 724bda1334..b109101019 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2556,10 +2556,18 @@ export interface Page { * element immediately before performing an action, so a series of actions on the same locator can in fact be performed on * different DOM elements. That would happen if the DOM structure between those actions has changed. * - * Shortcut for main frame's [frame.locator(selector)](https://playwright.dev/docs/api/class-frame#frame-locator). + * Shortcut for main frame's + * [frame.locator(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-locator). * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param options */ - locator(selector: string): Locator; + locator(selector: string, options?: { + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example, + * `"Playwright"` matches `
Playwright
`. + */ + hasText?: string|RegExp; + }): Locator; /** * The page's main frame. Page is guaranteed to have a main frame which persists during navigations. @@ -5324,8 +5332,15 @@ export interface Frame { * element immediately before performing an action, so a series of actions on the same locator can in fact be performed on * different DOM elements. That would happen if the DOM structure between those actions has changed. * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param options */ - locator(selector: string): Locator; + locator(selector: string, options?: { + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example, + * `"Playwright"` matches `
Playwright
`. + */ + hasText?: string|RegExp; + }): Locator; /** * Returns frame's name attribute as specified in the tag. @@ -8469,7 +8484,7 @@ export interface ElementHandle extends JSHandle { /** * Locators are the central piece of Playwright's auto-waiting and retry-ability. In a nutshell, locators represent a way * to find element(s) on the page at any moment. Locator can be created with the - * [page.locator(selector)](https://playwright.dev/docs/api/class-page#page-locator) method. + * [page.locator(selector[, options])](https://playwright.dev/docs/api/class-page#page-locator) method. * * [Learn more about locators](https://playwright.dev/docs/locators). */ @@ -9230,8 +9245,15 @@ export interface Locator { /** * The method finds an element matching the specified selector in the `Locator`'s subtree. * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param options */ - locator(selector: string): Locator; + locator(selector: string, options?: { + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example, + * `"Playwright"` matches `
Playwright
`. + */ + hasText?: string|RegExp; + }): Locator; /** * Returns locator to the n-th matching element. @@ -9753,14 +9775,7 @@ export interface Locator { * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. */ timeout?: number; - }): Promise; - - /** - * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example, - * `"Playwright"` matches `
Playwright
`. - * @param text Text to filter by as a string or as a regular expression. - */ - withText(text: string|RegExp): Locator;} + }): Promise;} /** * BrowserType provides methods to launch a specific browser instance or connect to an existing one. The following is a @@ -13639,8 +13654,15 @@ export interface FrameLocator { /** * The method finds an element matching the specified selector in the FrameLocator's subtree. * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param options */ - locator(selector: string): Locator; + locator(selector: string, options?: { + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example, + * `"Playwright"` matches `
Playwright
`. + */ + hasText?: string|RegExp; + }): Locator; /** * Returns locator to the n-th matching frame. diff --git a/tests/inspector/console-api.spec.ts b/tests/inspector/console-api.spec.ts index 1e04ad4897..84f39a2956 100644 --- a/tests/inspector/console-api.spec.ts +++ b/tests/inspector/console-api.spec.ts @@ -48,12 +48,12 @@ it('should support playwright.selector', async ({ page }) => { it('should support playwright.locator.value', async ({ page }) => { await page.setContent('
Hello
'); - const handle = await page.evaluateHandle(`playwright.locator('div').withText('Hello').element`); + const handle = await page.evaluateHandle(`playwright.locator('div', { hasText: 'Hello' }).element`); expect(await handle.evaluate((node: HTMLDivElement) => node.nodeName)).toBe('DIV'); }); it('should support playwright.locator.values', async ({ page }) => { - await page.setContent('
Hello
'); - const length = await page.evaluate(`playwright.locator('div').withText('Hello').elements.length`); + await page.setContent('
Hello
Bar
'); + const length = await page.evaluate(`playwright.locator('div', { hasText: 'Hello' }).elements.length`); expect(length).toBe(1); }); diff --git a/tests/page/locator-query.spec.ts b/tests/page/locator-query.spec.ts index 51de7bb365..e1c8a3ff5d 100644 --- a/tests/page/locator-query.spec.ts +++ b/tests/page/locator-query.spec.ts @@ -62,30 +62,30 @@ it('should throw on due to strictness 2', async ({ page }) => { it('should filter by text', async ({ page }) => { await page.setContent(`
Foobar
Bar
`); - await expect(page.locator('div').withText('Foo')).toHaveText('Foobar'); + await expect(page.locator('div', { hasText: 'Foo' })).toHaveText('Foobar'); }); it('should filter by text 2', async ({ page }) => { await page.setContent(`
foo hello world bar
`); - await expect(page.locator('div').withText('hello world')).toHaveText('foo hello world bar'); + await expect(page.locator('div', { hasText: 'hello world' })).toHaveText('foo hello world bar'); }); it('should filter by regex', async ({ page }) => { await page.setContent(`
Foobar
Bar
`); - await expect(page.locator('div').withText(/Foo.*/)).toHaveText('Foobar'); + await expect(page.locator('div', { hasText: /Foo.*/ })).toHaveText('Foobar'); }); it('should filter by text with quotes', async ({ page }) => { await page.setContent(`
Hello "world"
Hello world
`); - await expect(page.locator('div').withText('Hello "world"')).toHaveText('Hello "world"'); + await expect(page.locator('div', { hasText: 'Hello "world"' })).toHaveText('Hello "world"'); }); it('should filter by regex with quotes', async ({ page }) => { await page.setContent(`
Hello "world"
Hello world
`); - await expect(page.locator('div').withText(/Hello "world"/)).toHaveText('Hello "world"'); + await expect(page.locator('div', { hasText: /Hello "world"/ })).toHaveText('Hello "world"'); }); it('should filter by regex and regexp flags', async ({ page }) => { await page.setContent(`
Hello "world"
Hello world
`); - await expect(page.locator('div').withText(/hElLo "world"/i)).toHaveText('Hello "world"'); + await expect(page.locator('div', { hasText: /hElLo "world"/i })).toHaveText('Hello "world"'); });