feat(api): add getByPlaceholderText (#17722)
This commit is contained in:
parent
51966bc045
commit
083fb4401c
|
|
@ -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]>
|
||||
|
|
|
|||
|
|
@ -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]>
|
||||
|
|
|
|||
|
|
@ -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]>
|
||||
|
|
|
|||
|
|
@ -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]>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
72
packages/playwright-core/types/types.d.ts
vendored
72
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
60
tests/page/selectors-get-by.spec.ts
Normal file
60
tests/page/selectors-get-by.spec.ts
Normal 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);
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue