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

View File

@ -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 `<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:
```js
> playwright.locator('.auth-form').withText('Log in');
> playwright.locator('.auth-form', { hasText: 'Log in' });
> Locator ()
> - 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:
```js
> playwright.locator('.auth-form').withText('Log in');
> playwright.locator('.auth-form', { hasText: 'Log in' });
> Locator ()
> - element: button

View File

@ -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']);
});

View File

@ -280,8 +280,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel> 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 {

View File

@ -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<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 });
}
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 {

View File

@ -505,8 +505,8 @@ export class Page extends ChannelOwner<channels.PageChannel> 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 {

View File

@ -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(),

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
* 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 `<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.
@ -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 `<article><div>Playwright</div></article>`.
*/
hasText?: string|RegExp;
}): Locator;
/**
* 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
* 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 `<article><div>Playwright</div></article>`.
*/
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<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;}
}): Promise<void>;}
/**
* 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 `<article><div>Playwright</div></article>`.
*/
hasText?: string|RegExp;
}): Locator;
/**
* 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 }) => {
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');
});
it('should support playwright.locator.values', async ({ page }) => {
await page.setContent('<div>Hello<div>');
const length = await page.evaluate(`playwright.locator('div').withText('Hello').elements.length`);
await page.setContent('<div>Hello<div>Bar</div></div>');
const length = await page.evaluate(`playwright.locator('div', { hasText: 'Hello' }).elements.length`);
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 }) => {
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 }) => {
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 }) => {
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 }) => {
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 }) => {
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 }) => {
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"');
});