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-%% ### 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 ## method: Frame.getByRole
* since: v1.27 * since: v1.27
- returns: <[Locator]> - returns: <[Locator]>

View file

@ -124,6 +124,16 @@ in that iframe.
### option: FrameLocator.getByLabelText.exact = %%-locator-get-by-text-exact-%% ### 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 ## method: FrameLocator.getByRole
* since: v1.27 * since: v1.27
- returns: <[Locator]> - returns: <[Locator]>

View file

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

View file

@ -2193,6 +2193,16 @@ Attribute name to get the value for.
### option: Page.getByLabelText.exact = %%-locator-get-by-text-exact-%% ### 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 ## method: Page.getByRole
* since: v1.27 * since: v1.27
- returns: <[Locator]> - 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> <label for="password-input">Password:</label>
<input id="password-input"> <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 ## 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)); 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 { getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByTextSelector(text, options)); return this.locator(Locator.getByTextSelector(text, options));
} }

View file

@ -52,7 +52,7 @@ export class Locator implements api.Locator {
} }
static getByTestIdSelector(testId: string): string { 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 { static getByLabelTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
@ -63,6 +63,12 @@ export class Locator implements api.Locator {
return selector + ' >> control=resolve-label'; 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 { static getByTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
if (!isString(text)) if (!isString(text))
return `text=${text}`; return `text=${text}`;
@ -197,6 +203,10 @@ export class Locator implements api.Locator {
return this.locator(Locator.getByLabelTextSelector(text, options)); 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 { getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByTextSelector(text, options)); 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 { getByLabelText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByLabelTextSelector(text, options)); 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 { getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByTextSelector(text, options)); 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); 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 { getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.mainFrame().getByText(text, options); return this.mainFrame().getByText(text, options);
} }

View file

@ -19,6 +19,7 @@ import { XPathEngine } from './xpathSelectorEngine';
import { ReactEngine } from './reactSelectorEngine'; import { ReactEngine } from './reactSelectorEngine';
import { VueEngine } from './vueSelectorEngine'; import { VueEngine } from './vueSelectorEngine';
import { RoleEngine } from './roleSelectorEngine'; import { RoleEngine } from './roleSelectorEngine';
import { parseAttributeSelector } from '../isomorphic/selectorParser';
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser'; import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser';
import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser'; import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
import { type TextMatcher, elementMatchesText, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher, elementText } from './selectorUtils'; 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('visible', this._createVisibleEngine());
this._engines.set('control', this._createControlEngine()); this._engines.set('control', this._createControlEngine());
this._engines.set('has', this._createHasEngine()); this._engines.set('has', this._createHasEngine());
this._engines.set('attr', this._createNamedAttributeEngine());
for (const { name, engine } of customEngines) for (const { name, engine } of customEngines)
this._engines.set(name, engine); 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 { private _createControlEngine(): SelectorEngineV2 {
return { return {
queryAll(root: SelectorRoot, body: any) { queryAll(root: SelectorRoot, body: any) {

View file

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

View file

@ -2475,6 +2475,24 @@ export interface Page {
exact?: boolean; exact?: boolean;
}): Locator; }): 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), * 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 * [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
@ -5509,6 +5527,24 @@ export interface Frame {
exact?: boolean; exact?: boolean;
}): Locator; }): 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), * 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 * [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
@ -9891,6 +9927,24 @@ export interface Locator {
exact?: boolean; exact?: boolean;
}): Locator; }): 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), * 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 * [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
@ -15094,6 +15148,24 @@ export interface FrameLocator {
exact?: boolean; exact?: boolean;
}): Locator; }): 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), * 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 * [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> </div>
<span>1</span> <span>1</span>
<span>2</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>`, </html>`,
contentType: 'text/html' contentType: 'text/html'
}).catch(() => {}); }).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'); 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 routeIframe(page);
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const button1 = page.frameLocator('iframe').getByRole('button'); 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(button1).toHaveText('Hello iframe');
await expect(button2).toHaveText('Hello iframe'); await expect(button2).toHaveText('Hello iframe');
await expect(button3).toHaveText('Hello iframe'); await expect(button3).toHaveText('Hello iframe');
const input = page.frameLocator('iframe').getByLabelText('Name'); const input1 = page.frameLocator('iframe').getByLabelText('Name');
await expect(input).toHaveValue(''); 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 div.$eval(`.find-me`, e => e.id)).toBe('target2');
expect(await page.$eval(`div >> .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. // Should double escape inside quoted text.
await expect(page.locator(`div >> text='pattern "^-?\\\\d+$"'`)).toBeVisible(); 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');
});