feat(api): make withText an option (#10922)

This commit is contained in:
Pavel Feldman 2021-12-14 15:37:31 -08:00 committed by GitHub
parent 34b84841b0
commit 04e82ce71c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 99 additions and 74 deletions

View File

@ -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. 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-%% ### param: Frame.locator.selector = %%-find-selector-%%
### option: Frame.locator.-inline- = %%-locator-options-list-%%
## method: Frame.name ## method: Frame.name
- returns: <[string]> - returns: <[string]>

View File

@ -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. The method finds an element matching the specified selector in the FrameLocator's subtree.
### param: FrameLocator.locator.selector = %%-find-selector-%% ### param: FrameLocator.locator.selector = %%-find-selector-%%
### option: FrameLocator.locator.-inline- = %%-locator-options-list-%%
## method: FrameLocator.nth ## method: FrameLocator.nth

View File

@ -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. The method finds an element matching the specified selector in the `Locator`'s subtree.
### param: Locator.locator.selector = %%-find-selector-%% ### param: Locator.locator.selector = %%-find-selector-%%
### option: Locator.locator.-inline- = %%-locator-options-list-%%
## method: Locator.nth ## method: Locator.nth
- returns: <[Locator]> - returns: <[Locator]>
@ -908,15 +909,3 @@ orderSent.WaitForAsync();
### option: Locator.waitFor.state = %%-wait-for-selector-state-%% ### option: Locator.waitFor.state = %%-wait-for-selector-state-%%
### option: Locator.waitFor.timeout = %%-input-timeout-%% ### 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 `<article><div>Playwright</div></article>`.
### param: Locator.withText.text
- `text` <[string]|[RegExp]>
Text to filter by as a string or as a regular expression.

View File

@ -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`]. Shortcut for main frame's [`method: Frame.locator`].
### param: Page.locator.selector = %%-find-selector-%% ### param: Page.locator.selector = %%-find-selector-%%
### option: Page.locator.-inline- = %%-locator-options-list-%%
## method: Page.mainFrame ## method: Page.mainFrame
- returns: <[Frame]> - returns: <[Frame]>

View File

@ -868,3 +868,12 @@ Slows down Playwright operations by the specified amount of milliseconds. Useful
- %%-browser-option-proxy-%% - %%-browser-option-proxy-%%
- %%-browser-option-timeout-%% - %%-browser-option-timeout-%%
- %%-browser-option-tracesdir-%% - %%-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 `<article><div>Playwright</div></article>`.
## locator-options-list
- %%-locator-option-has-text-%%

View File

@ -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: Query Playwright element using the actual Playwright query engine, for example:
```js ```js
> playwright.locator('.auth-form').withText('Log in'); > playwright.locator('.auth-form', { hasText: 'Log in' });
> Locator () > Locator ()
> - element: button > - element: button

View File

@ -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: Query Playwright element using the actual Playwright query engine, for example:
```js ```js
> playwright.locator('.auth-form').withText('Log in'); > playwright.locator('.auth-form', { hasText: 'Log in' });
> Locator () > Locator ()
> - element: button > - element: button

View File

@ -30,11 +30,11 @@ test('should render counters', async ({ renderComponent }) => {
duration: 100000 duration: 100000
}; };
const component = await renderComponent('HeaderView', { stats }); const component = await renderComponent('HeaderView', { stats });
await expect(component.locator('a').withText('All').locator('.counter')).toHaveText('100'); await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('100');
await expect(component.locator('a').withText('Passed').locator('.counter')).toHaveText('42'); await expect(component.locator('a', { hasText: 'Passed' }).locator('.counter')).toHaveText('42');
await expect(component.locator('a').withText('Failed').locator('.counter')).toHaveText('31'); await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31');
await expect(component.locator('a').withText('Flaky').locator('.counter')).toHaveText('17'); await expect(component.locator('a', { hasText: 'Flaky' }).locator('.counter')).toHaveText('17');
await expect(component.locator('a').withText('Skipped').locator('.counter')).toHaveText('10'); await expect(component.locator('a', { hasText: 'Skipped' }).locator('.counter')).toHaveText('10');
}); });
test('should toggle filters', async ({ page, renderComponent }) => { test('should toggle filters', async ({ page, renderComponent }) => {
@ -52,14 +52,14 @@ test('should toggle filters', async ({ page, renderComponent }) => {
stats, stats,
setFilterText: (filterText: string) => filters.push(filterText) setFilterText: (filterText: string) => filters.push(filterText)
}); });
await component.locator('a').withText('All').click(); await component.locator('a', { hasText: 'All' }).click();
await component.locator('a').withText('Passed').click(); await component.locator('a', { hasText: 'Passed' }).click();
await expect(page).toHaveURL(/#\?q=s:passed/); 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 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 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/); await expect(page).toHaveURL(/#\?q=s:skipped/);
expect(filters).toEqual(['', 's:passed', 's:failed', 's:flaky', 's:skipped']); expect(filters).toEqual(['', 's:passed', 's:failed', 's:flaky', 's:skipped']);
}); });

View File

@ -280,8 +280,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
return await this._channel.fill({ selector, value, ...options }); return await this._channel.fill({ selector, value, ...options });
} }
locator(selector: string): Locator { locator(selector: string, options?: { hasText?: string | RegExp }): Locator {
return new Locator(this, selector); return new Locator(this, selector, options);
} }
frameLocator(selector: string): FrameLocator { frameLocator(selector: string): FrameLocator {

View File

@ -29,9 +29,17 @@ export class Locator implements api.Locator {
private _frame: Frame; private _frame: Frame;
private _selector: string; private _selector: string;
constructor(frame: Frame, selector: string) { constructor(frame: Frame, selector: string, options?: { hasText?: string | RegExp }) {
this._frame = frame; this._frame = frame;
this._selector = selector; 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<R>(task: (handle: ElementHandle<SVGElement | HTMLElement>, timeout?: number) => Promise<R>, timeout?: number): Promise<R> { private async _withElement<R>(task: (handle: ElementHandle<SVGElement | HTMLElement>, timeout?: number) => Promise<R>, timeout?: number): Promise<R> {
@ -94,14 +102,8 @@ export class Locator implements api.Locator {
return this._frame.fill(this._selector, value, { strict: true, ...options }); return this._frame.fill(this._selector, value, { strict: true, ...options });
} }
locator(selector: string): Locator { locator(selector: string, options?: { hasText?: string | RegExp }): Locator {
return new Locator(this._frame, this._selector + ' >> ' + selector); return new Locator(this._frame, this._selector + ' >> ' + selector, options);
}
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, '"')})`);
} }
frameLocator(selector: string): FrameLocator { frameLocator(selector: string): FrameLocator {
@ -269,8 +271,8 @@ export class FrameLocator implements api.FrameLocator {
this._frameSelector = selector; this._frameSelector = selector;
} }
locator(selector: string): Locator { locator(selector: string, options?: { hasText?: string | RegExp }): Locator {
return new Locator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector); return new Locator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector, options);
} }
frameLocator(selector: string): FrameLocator { frameLocator(selector: string): FrameLocator {

View File

@ -505,8 +505,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return this._mainFrame.fill(selector, value, options); return this._mainFrame.fill(selector, value, options);
} }
locator(selector: string): Locator { locator(selector: string, options?: { hasText?: string | RegExp }): Locator {
return this.mainFrame().locator(selector); return this.mainFrame().locator(selector, options);
} }
frameLocator(selector: string): FrameLocator { frameLocator(selector: string): FrameLocator {

View File

@ -18,30 +18,30 @@ import { escapeWithQuotes } from '../../../utils/stringUtils';
import type InjectedScript from '../../injected/injectedScript'; import type InjectedScript from '../../injected/injectedScript';
import { generateSelector } from '../../injected/selectorGenerator'; import { generateSelector } from '../../injected/selectorGenerator';
function createLocator(injectedScript: InjectedScript, initial: string) { function createLocator(injectedScript: InjectedScript, initial: string, options?: { hasText?: string | RegExp }) {
class Locator { class Locator {
selector: string; selector: string;
element: Element | undefined; element: Element | undefined;
elements: Element[]; elements: Element[];
constructor(selector: string) { constructor(selector: string, options?: { hasText?: string | RegExp }) {
this.selector = selector; 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); const parsed = injectedScript.parseSelector(this.selector);
this.element = injectedScript.querySelector(parsed, document, false); this.element = injectedScript.querySelector(parsed, document, false);
this.elements = injectedScript.querySelectorAll(parsed, document); this.elements = injectedScript.querySelectorAll(parsed, document);
} }
locator(selector: string): Locator { locator(selector: string, options?: { hasText: string | RegExp }): Locator {
return new Locator(this.selector ? this.selector + ' >> ' + selector : selector); return new Locator(this.selector ? this.selector + ' >> ' + selector : selector, options);
}
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})`);
} }
} }
return new Locator(initial); return new Locator(initial, options);
} }
type ConsoleAPIInterface = { type ConsoleAPIInterface = {
@ -71,7 +71,7 @@ export class ConsoleAPI {
window.playwright = { window.playwright = {
$: (selector: string, strict?: boolean) => this._querySelector(selector, !!strict), $: (selector: string, strict?: boolean) => this._querySelector(selector, !!strict),
$$: (selector: string) => this._querySelectorAll(selector), $$: (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), inspect: (selector: string) => this._inspect(selector),
selector: (element: Element) => this._selector(element), selector: (element: Element) => this._selector(element),
resume: () => this._resume(), resume: () => this._resume(),

View File

@ -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 * 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. * 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 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 `<article><div>Playwright</div></article>`.
*/
hasText?: string|RegExp;
}): Locator;
/** /**
* The page's main frame. Page is guaranteed to have a main frame which persists during navigations. * 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 * 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. * 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 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 `<article><div>Playwright</div></article>`.
*/
hasText?: string|RegExp;
}): Locator;
/** /**
* Returns frame's name attribute as specified in the tag. * Returns frame's name attribute as specified in the tag.
@ -8469,7 +8484,7 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
/** /**
* Locators are the central piece of Playwright's auto-waiting and retry-ability. In a nutshell, locators represent a way * 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 * 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). * [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. * 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 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 `<article><div>Playwright</div></article>`.
*/
hasText?: string|RegExp;
}): Locator;
/** /**
* Returns locator to the n-th matching element. * 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. * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
*/ */
timeout?: number; timeout?: number;
}): Promise<void>; }): Promise<void>;}
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example,
* `"Playwright"` matches `<article><div>Playwright</div></article>`.
* @param text Text to filter by as a string or as a regular expression.
*/
withText(text: string|RegExp): Locator;}
/** /**
* BrowserType provides methods to launch a specific browser instance or connect to an existing one. The following is a * 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. * 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 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 `<article><div>Playwright</div></article>`.
*/
hasText?: string|RegExp;
}): Locator;
/** /**
* Returns locator to the n-th matching frame. * Returns locator to the n-th matching frame.

View File

@ -48,12 +48,12 @@ it('should support playwright.selector', async ({ page }) => {
it('should support playwright.locator.value', async ({ page }) => { it('should support playwright.locator.value', async ({ page }) => {
await page.setContent('<div>Hello<div>'); await page.setContent('<div>Hello<div>');
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<string, HTMLDivElement>((node: HTMLDivElement) => node.nodeName)).toBe('DIV'); expect(await handle.evaluate<string, HTMLDivElement>((node: HTMLDivElement) => node.nodeName)).toBe('DIV');
}); });
it('should support playwright.locator.values', async ({ page }) => { it('should support playwright.locator.values', async ({ page }) => {
await page.setContent('<div>Hello<div>'); await page.setContent('<div>Hello<div>Bar</div></div>');
const length = await page.evaluate(`playwright.locator('div').withText('Hello').elements.length`); const length = await page.evaluate(`playwright.locator('div', { hasText: 'Hello' }).elements.length`);
expect(length).toBe(1); expect(length).toBe(1);
}); });

View File

@ -62,30 +62,30 @@ it('should throw on due to strictness 2', async ({ page }) => {
it('should filter by text', async ({ page }) => { it('should filter by text', async ({ page }) => {
await page.setContent(`<div>Foobar</div><div>Bar</div>`); await page.setContent(`<div>Foobar</div><div>Bar</div>`);
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 }) => { it('should filter by text 2', async ({ page }) => {
await page.setContent(`<div>foo <span>hello world</span> bar</div>`); await page.setContent(`<div>foo <span>hello world</span> bar</div>`);
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 }) => { it('should filter by regex', async ({ page }) => {
await page.setContent(`<div>Foobar</div><div>Bar</div>`); await page.setContent(`<div>Foobar</div><div>Bar</div>`);
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 }) => { it('should filter by text with quotes', async ({ page }) => {
await page.setContent(`<div>Hello "world"</div><div>Hello world</div>`); await page.setContent(`<div>Hello "world"</div><div>Hello world</div>`);
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 }) => { it('should filter by regex with quotes', async ({ page }) => {
await page.setContent(`<div>Hello "world"</div><div>Hello world</div>`); await page.setContent(`<div>Hello "world"</div><div>Hello world</div>`);
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 }) => { it('should filter by regex and regexp flags', async ({ page }) => {
await page.setContent(`<div>Hello "world"</div><div>Hello world</div>`); await page.setContent(`<div>Hello "world"</div><div>Hello world</div>`);
await expect(page.locator('div').withText(/hElLo "world"/i)).toHaveText('Hello "world"'); await expect(page.locator('div', { hasText: /hElLo "world"/i })).toHaveText('Hello "world"');
}); });