feat(api): add getByPlaceholderText (#17722)

This commit is contained in:
Pavel Feldman 2022-09-29 17:12:49 -08:00 committed by GitHub
parent 51966bc045
commit 083fb4401c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 237 additions and 26 deletions

View file

@ -918,6 +918,16 @@ Attribute name to get the value for.
### option: Frame.getByLabelText.exact = %%-locator-get-by-text-exact-%%
## method: Frame.getByPlaceholderText
* since: v1.27
- returns: <[Locator]>
%%-template-locator-get-by-placeholder-text-%%
### param: Frame.getByPlaceholderText.text = %%-locator-get-by-text-text-%%
### option: Frame.getByPlaceholderText.exact = %%-locator-get-by-text-exact-%%
## method: Frame.getByRole
* since: v1.27
- returns: <[Locator]>

View file

@ -124,6 +124,16 @@ in that iframe.
### option: FrameLocator.getByLabelText.exact = %%-locator-get-by-text-exact-%%
## method: FrameLocator.getByPlaceholderText
* since: v1.27
- returns: <[Locator]>
%%-template-locator-get-by-placeholder-text-%%
### param: FrameLocator.getByPlaceholderText.text = %%-locator-get-by-text-text-%%
### option: FrameLocator.getByPlaceholderText.exact = %%-locator-get-by-text-exact-%%
## method: FrameLocator.getByRole
* since: v1.27
- returns: <[Locator]>

View file

@ -644,6 +644,16 @@ Attribute name to get the value for.
### option: Locator.getByLabelText.exact = %%-locator-get-by-text-exact-%%
## method: Locator.getByPlaceholderText
* since: v1.27
- returns: <[Locator]>
%%-template-locator-get-by-placeholder-text-%%
### param: Locator.getByPlaceholderText.text = %%-locator-get-by-text-text-%%
### option: Locator.getByPlaceholderText.exact = %%-locator-get-by-text-exact-%%
## method: Locator.getByRole
* since: v1.27
- returns: <[Locator]>

View file

@ -2193,6 +2193,16 @@ Attribute name to get the value for.
### option: Page.getByLabelText.exact = %%-locator-get-by-text-exact-%%
## method: Page.getByPlaceholderText
* since: v1.27
- returns: <[Locator]>
%%-template-locator-get-by-placeholder-text-%%
### param: Page.getByPlaceholderText.text = %%-locator-get-by-text-text-%%
### option: Page.getByPlaceholderText.exact = %%-locator-get-by-text-exact-%%
## method: Page.getByRole
* since: v1.27
- returns: <[Locator]>

View file

@ -1188,6 +1188,13 @@ Allows locating input elements by the text of the associated label. For example,
<label for="password-input">Password:</label>
<input id="password-input">
```
## template-locator-get-by-placeholder-text
Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder "Country":
```html
<input placeholder="Country">
```
## template-locator-get-by-role

View file

@ -307,6 +307,10 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
return this.locator(Locator.getByLabelTextSelector(text, options));
}
getByPlaceholderText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByPlaceholderTextSelector(text, options));
}
getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByTextSelector(text, options));
}

View file

@ -52,7 +52,7 @@ export class Locator implements api.Locator {
}
static getByTestIdSelector(testId: string): string {
return `css=[${Locator._testIdAttributeName}=${JSON.stringify(testId)}]`;
return `attr=[${Locator._testIdAttributeName}=${JSON.stringify(testId)}]`;
}
static getByLabelTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
@ -63,6 +63,12 @@ export class Locator implements api.Locator {
return selector + ' >> control=resolve-label';
}
static getByPlaceholderTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
if (!isString(text))
return `attr=[placeholder=${text}]`;
return `attr=[placeholder=${JSON.stringify(text)}${options?.exact ? 's' : 'i'}]`;
}
static getByTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
if (!isString(text))
return `text=${text}`;
@ -197,6 +203,10 @@ export class Locator implements api.Locator {
return this.locator(Locator.getByLabelTextSelector(text, options));
}
getByPlaceholderText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByPlaceholderTextSelector(text, options));
}
getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByTextSelector(text, options));
}
@ -386,6 +396,11 @@ export class FrameLocator implements api.FrameLocator {
getByLabelText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByLabelTextSelector(text, options));
}
getByPlaceholderText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByPlaceholderTextSelector(text, options));
}
getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByTextSelector(text, options));
}

View file

@ -572,6 +572,10 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return this.mainFrame().getByLabelText(text, options);
}
getByPlaceholderText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.mainFrame().getByPlaceholderText(text, options);
}
getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.mainFrame().getByText(text, options);
}

View file

@ -19,6 +19,7 @@ import { XPathEngine } from './xpathSelectorEngine';
import { ReactEngine } from './reactSelectorEngine';
import { VueEngine } from './vueSelectorEngine';
import { RoleEngine } from './roleSelectorEngine';
import { parseAttributeSelector } from '../isomorphic/selectorParser';
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser';
import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
import { type TextMatcher, elementMatchesText, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher, elementText } from './selectorUtils';
@ -104,6 +105,7 @@ export class InjectedScript {
this._engines.set('visible', this._createVisibleEngine());
this._engines.set('control', this._createControlEngine());
this._engines.set('has', this._createHasEngine());
this._engines.set('attr', this._createNamedAttributeEngine());
for (const { name, engine } of customEngines)
this._engines.set(name, engine);
@ -271,6 +273,31 @@ export class InjectedScript {
};
}
private _createNamedAttributeEngine(): SelectorEngine {
const queryList = (root: SelectorRoot, selector: string): Element[] => {
const parsed = parseAttributeSelector(selector, true);
if (parsed.name || parsed.attributes.length !== 1)
throw new Error('Malformed attribute selector: ' + selector);
const { name, value, caseSensitive } = parsed.attributes[0];
const lowerCaseValue = caseSensitive ? null : value.toLowerCase();
let matcher: (s: string) => boolean;
if (value instanceof RegExp)
matcher = s => !!s.match(value);
else if (caseSensitive)
matcher = s => s === value;
else
matcher = s => s.toLowerCase().includes(lowerCaseValue!);
const elements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, `[${name}]`);
return elements.filter(e => matcher(e.getAttribute(name)!));
};
return {
queryAll: (root: SelectorRoot, selector: string): Element[] => {
return queryList(root, selector);
}
};
}
private _createControlEngine(): SelectorEngineV2 {
return {
queryAll(root: SelectorRoot, body: any) {

View file

@ -46,7 +46,7 @@ export class Selectors {
'data-test-id', 'data-test-id:light',
'data-test', 'data-test:light',
'nth', 'visible', 'control', 'has',
'role',
'role', 'attr'
]);
this._builtinEnginesInMainWorld = new Set([
'_react', '_vue',

View file

@ -2475,6 +2475,24 @@ export interface Page {
exact?: boolean;
}): Locator;
/**
* Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder
* "Country":
*
* ```html
* <input placeholder="Country">
* ```
*
* @param text Text to locate the element for.
* @param options
*/
getByPlaceholderText(text: string|RegExp, options?: {
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false.
*/
exact?: boolean;
}): Locator;
/**
* Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles),
* [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
@ -5509,6 +5527,24 @@ export interface Frame {
exact?: boolean;
}): Locator;
/**
* Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder
* "Country":
*
* ```html
* <input placeholder="Country">
* ```
*
* @param text Text to locate the element for.
* @param options
*/
getByPlaceholderText(text: string|RegExp, options?: {
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false.
*/
exact?: boolean;
}): Locator;
/**
* Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles),
* [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
@ -9891,6 +9927,24 @@ export interface Locator {
exact?: boolean;
}): Locator;
/**
* Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder
* "Country":
*
* ```html
* <input placeholder="Country">
* ```
*
* @param text Text to locate the element for.
* @param options
*/
getByPlaceholderText(text: string|RegExp, options?: {
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false.
*/
exact?: boolean;
}): Locator;
/**
* Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles),
* [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
@ -15094,6 +15148,24 @@ export interface FrameLocator {
exact?: boolean;
}): Locator;
/**
* Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder
* "Country":
*
* ```html
* <input placeholder="Country">
* ```
*
* @param text Text to locate the element for.
* @param options
*/
getByPlaceholderText(text: string|RegExp, options?: {
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false.
*/
exact?: boolean;
}): Locator;
/**
* Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles),
* [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and

View file

@ -35,7 +35,7 @@ async function routeIframe(page: Page) {
</div>
<span>1</span>
<span>2</span>
<label for=target>Name</label><input id=target type=text>
<label for=target>Name</label><input id=target type=text placeholder=Placeholder>
</html>`,
contentType: 'text/html'
}).catch(() => {});
@ -239,7 +239,7 @@ it('locator.frameLocator should not throw on first/last/nth', async ({ page, ser
await expect(button3).toHaveText('Hello from iframe-3.html');
});
it('role and text coverage', async ({ page, server }) => {
it('getBy coverage', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button1 = page.frameLocator('iframe').getByRole('button');
@ -248,6 +248,8 @@ it('role and text coverage', async ({ page, server }) => {
await expect(button1).toHaveText('Hello iframe');
await expect(button2).toHaveText('Hello iframe');
await expect(button3).toHaveText('Hello iframe');
const input = page.frameLocator('iframe').getByLabelText('Name');
await expect(input).toHaveValue('');
const input1 = page.frameLocator('iframe').getByLabelText('Name');
await expect(input1).toHaveValue('');
const input2 = page.frameLocator('iframe').getByPlaceholderText('Placeholder');
await expect(input2).toHaveValue('');
});

View file

@ -413,15 +413,3 @@ it('css on the handle should be relative', async ({ page }) => {
expect(await div.$eval(`.find-me`, e => e.id)).toBe('target2');
expect(await page.$eval(`div >> .find-me`, e => e.id)).toBe('target2');
});
it('getByTestId should work', async ({ page }) => {
await page.setContent('<div><div data-testid="Hello">Hello world</div></div>');
await expect(page.getByTestId('Hello')).toHaveText('Hello world');
await expect(page.mainFrame().getByTestId('Hello')).toHaveText('Hello world');
await expect(page.locator('div').getByTestId('Hello')).toHaveText('Hello world');
});
it('getByTestId should escape id', async ({ page }) => {
await page.setContent(`<div><div data-testid='He"llo'>Hello world</div></div>`);
await expect(page.getByTestId('He"llo')).toHaveText('Hello world');
});

View file

@ -0,0 +1,60 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test as it, expect } from './pageTest';
it('getByTestId should work', async ({ page }) => {
await page.setContent('<div><div data-testid="Hello">Hello world</div></div>');
await expect(page.getByTestId('Hello')).toHaveText('Hello world');
await expect(page.mainFrame().getByTestId('Hello')).toHaveText('Hello world');
await expect(page.locator('div').getByTestId('Hello')).toHaveText('Hello world');
});
it('getByTestId should escape id', async ({ page }) => {
await page.setContent(`<div><div data-testid='He"llo'>Hello world</div></div>`);
await expect(page.getByTestId('He"llo')).toHaveText('Hello world');
});
it('getByText should work', async ({ page }) => {
await page.setContent(`<div>yo</div><div>ya</div><div>\nye </div>`);
expect(await page.getByText('ye').evaluate(e => e.outerHTML)).toContain('>\nye </div>');
expect(await page.getByText(/ye/).evaluate(e => e.outerHTML)).toContain('>\nye </div>');
await page.setContent(`<div> ye </div><div>ye</div>`);
expect(await page.getByText('ye', { exact: true }).first().evaluate(e => e.outerHTML)).toContain('> ye </div>');
});
it('getByLabelText should work', async ({ page }) => {
await page.setContent(`<div><label for=target>Name</label><input id=target type=text></div>`);
expect(await page.getByText('Name').evaluate(e => e.nodeName)).toBe('LABEL');
expect(await page.getByLabelText('Name').evaluate(e => e.nodeName)).toBe('INPUT');
expect(await page.mainFrame().getByLabelText('Name').evaluate(e => e.nodeName)).toBe('INPUT');
expect(await page.locator('div').getByLabelText('Name').evaluate(e => e.nodeName)).toBe('INPUT');
});
it('getByPlaceholderText should work', async ({ page }) => {
await page.setContent(`<div>
<input placeholder='Hello'>
<input placeholder='Hello World'>
</div>`);
await expect(page.getByPlaceholderText('hello')).toHaveCount(2);
await expect(page.getByPlaceholderText('Hello', { exact: true })).toHaveCount(1);
await expect(page.getByPlaceholderText(/wor/i)).toHaveCount(1);
// Coverage
await expect(page.mainFrame().getByPlaceholderText('hello')).toHaveCount(2);
await expect(page.locator('div').getByPlaceholderText('hello')).toHaveCount(2);
});

View file

@ -454,11 +454,3 @@ it('should work with paired quotes in the middle of selector', async ({ page })
// Should double escape inside quoted text.
await expect(page.locator(`div >> text='pattern "^-?\\\\d+$"'`)).toBeVisible();
});
it('getByLabelText should work', async ({ page, asset }) => {
await page.setContent(`<div><label for=target>Name</label><input id=target type=text></div>`);
expect(await page.getByText('Name').evaluate(e => e.nodeName)).toBe('LABEL');
expect(await page.getByLabelText('Name').evaluate(e => e.nodeName)).toBe('INPUT');
expect(await page.mainFrame().getByLabelText('Name').evaluate(e => e.nodeName)).toBe('INPUT');
expect(await page.locator('div').getByLabelText('Name').evaluate(e => e.nodeName)).toBe('INPUT');
});