fix(getByRole): name and exact (#18719)
Following the `getByText()` and other methods: - By default, matching is substring and case-insensitive. Before, it was only case-insensitive, but not substring. - With new option `exact: true`, matching is full string and case-sensitive. - Matching always normalizes whitespace. - Codegen generates `exact: false` by default. - `internal:role` treats `[name="foo"i]` as non-exact match. Various fixes: - Updated `getByRole` docs to match the reality. - Locator generator edge cases.
This commit is contained in:
parent
bc78db07df
commit
a7b2b04588
|
|
@ -949,6 +949,7 @@ Attribute name to get the value for.
|
|||
### param: Frame.getByRole.role = %%-locator-get-by-role-role-%%
|
||||
### option: Frame.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
|
||||
* since: v1.27
|
||||
### option: Frame.getByRole.exact = %%-locator-get-by-role-option-exact-%%
|
||||
|
||||
|
||||
## method: Frame.getByTestId
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ in that iframe.
|
|||
### param: FrameLocator.getByRole.role = %%-locator-get-by-role-role-%%
|
||||
### option: FrameLocator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
|
||||
* since: v1.27
|
||||
### option: FrameLocator.getByRole.exact = %%-locator-get-by-role-option-exact-%%
|
||||
|
||||
|
||||
## method: FrameLocator.getByTestId
|
||||
|
|
|
|||
|
|
@ -695,6 +695,7 @@ Attribute name to get the value for.
|
|||
### param: Locator.getByRole.role = %%-locator-get-by-role-role-%%
|
||||
### option: Locator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
|
||||
* since: v1.27
|
||||
### option: Locator.getByRole.exact = %%-locator-get-by-role-option-exact-%%
|
||||
|
||||
|
||||
## method: Locator.getByTestId
|
||||
|
|
|
|||
|
|
@ -2222,6 +2222,7 @@ Attribute name to get the value for.
|
|||
### param: Page.getByRole.role = %%-locator-get-by-role-role-%%
|
||||
### option: Page.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
|
||||
* since: v1.27
|
||||
### option: Page.getByRole.exact = %%-locator-get-by-role-option-exact-%%
|
||||
|
||||
|
||||
## method: Page.getByTestId
|
||||
|
|
|
|||
|
|
@ -1109,7 +1109,7 @@ Required aria role.
|
|||
* since: v1.27
|
||||
- `checked` <[boolean]>
|
||||
|
||||
An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for checked are `true`, `false` and `"mixed"`.
|
||||
An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
|
||||
|
||||
Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
|
||||
|
||||
|
|
@ -1117,7 +1117,7 @@ Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-check
|
|||
* since: v1.27
|
||||
- `disabled` <[boolean]>
|
||||
|
||||
A boolean attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
An attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
|
||||
:::note
|
||||
Unlike most other attributes, `disabled` is inherited through the DOM hierarchy.
|
||||
|
|
@ -1128,15 +1128,15 @@ Learn more about [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disa
|
|||
* since: v1.27
|
||||
- `expanded` <[boolean]>
|
||||
|
||||
A boolean attribute that is usually set by `aria-expanded`.
|
||||
An attribute that is usually set by `aria-expanded`.
|
||||
|
||||
Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
|
||||
## locator-get-by-role-option-includeHidden
|
||||
* since: v1.27
|
||||
- `includeHidden` <[boolean]>
|
||||
|
||||
A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
Option that controls whether hidden elements are matched. By default, only non-hidden elements, as [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
|
||||
Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
|
||||
|
||||
|
|
@ -1152,15 +1152,21 @@ Learn more about [`aria-level`](https://www.w3.org/TR/wai-aria-1.2/#aria-level).
|
|||
* since: v1.27
|
||||
- `name` <[string]|[RegExp]>
|
||||
|
||||
A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is case-insensitive and searches for a substring, use [`option: exact`] to control this behavior.
|
||||
|
||||
Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
|
||||
## locator-get-by-role-option-exact
|
||||
* since: v1.28
|
||||
- `exact` <[boolean]>
|
||||
|
||||
Whether [`option: name`] is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when [`option: name`] is a regular expression. Note that exact match still trims whitespace.
|
||||
|
||||
## locator-get-by-role-option-pressed
|
||||
* since: v1.27
|
||||
- `pressed` <[boolean]>
|
||||
|
||||
An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`.
|
||||
An attribute that is usually set by `aria-pressed`.
|
||||
|
||||
Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
|
||||
|
||||
|
|
@ -1168,7 +1174,7 @@ Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-press
|
|||
* since: v1.27
|
||||
- `selected` <boolean>
|
||||
|
||||
A boolean attribute that is usually set by `aria-selected`.
|
||||
An attribute that is usually set by `aria-selected`.
|
||||
|
||||
Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import type { SelectorEngine, SelectorRoot } from './selectorEngine';
|
|||
import { XPathEngine } from './xpathSelectorEngine';
|
||||
import { ReactEngine } from './reactSelectorEngine';
|
||||
import { VueEngine } from './vueSelectorEngine';
|
||||
import { RoleEngine } from './roleSelectorEngine';
|
||||
import { createRoleEngine } from './roleSelectorEngine';
|
||||
import { parseAttributeSelector } from '../isomorphic/selectorParser';
|
||||
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser';
|
||||
import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
|
||||
|
|
@ -95,7 +95,7 @@ export class InjectedScript {
|
|||
this._engines.set('xpath:light', XPathEngine);
|
||||
this._engines.set('_react', ReactEngine);
|
||||
this._engines.set('_vue', VueEngine);
|
||||
this._engines.set('role', RoleEngine);
|
||||
this._engines.set('role', createRoleEngine(false));
|
||||
this._engines.set('text', this._createTextEngine(true, false));
|
||||
this._engines.set('text:light', this._createTextEngine(false, false));
|
||||
this._engines.set('id', this._createAttributeEngine('id', true));
|
||||
|
|
@ -116,7 +116,7 @@ export class InjectedScript {
|
|||
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
|
||||
this._engines.set('internal:attr', this._createNamedAttributeEngine());
|
||||
this._engines.set('internal:testid', this._createNamedAttributeEngine());
|
||||
this._engines.set('internal:role', RoleEngine);
|
||||
this._engines.set('internal:role', createRoleEngine(true));
|
||||
|
||||
for (const { name, engine } of customEngines)
|
||||
this._engines.set(name, engine);
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export const RoleEngine: SelectorEngine = {
|
||||
queryAll(scope: SelectorRoot, selector: string): Element[] {
|
||||
export function createRoleEngine(internal: boolean): SelectorEngine {
|
||||
const queryAll = (scope: SelectorRoot, selector: string): Element[] => {
|
||||
const parsed = parseAttributeSelector(selector, true);
|
||||
const role = parsed.name.toLowerCase();
|
||||
if (!role)
|
||||
|
|
@ -149,7 +149,13 @@ export const RoleEngine: SelectorEngine = {
|
|||
return;
|
||||
}
|
||||
if (nameAttr !== undefined) {
|
||||
const accessibleName = getElementAccessibleName(element, includeHidden, hiddenCache);
|
||||
// Always normalize whitespace in the accessible name.
|
||||
const accessibleName = getElementAccessibleName(element, includeHidden, hiddenCache).trim().replace(/\s+/g, ' ');
|
||||
if (typeof nameAttr.value === 'string')
|
||||
nameAttr.value = nameAttr.value.trim().replace(/\s+/g, ' ');
|
||||
// internal:role assumes that [name="foo"i] also means substring.
|
||||
if (internal && !nameAttr.caseSensitive && nameAttr.op === '=')
|
||||
nameAttr.op = '*=';
|
||||
if (!matchesAttributePart(accessibleName, nameAttr))
|
||||
return;
|
||||
}
|
||||
|
|
@ -170,5 +176,6 @@ export const RoleEngine: SelectorEngine = {
|
|||
|
||||
query(scope);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
};
|
||||
return { queryAll };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, testI
|
|||
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
|
||||
const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
|
||||
if (ariaName)
|
||||
candidates.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScore });
|
||||
candidates.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, false)}]`, score: kRoleWithNameScore });
|
||||
else
|
||||
candidates.push({ engine: 'internal:role', selector: ariaRole, score: kRoleWithoutNameScore });
|
||||
}
|
||||
|
|
@ -227,7 +227,7 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
|
|||
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
|
||||
const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
|
||||
if (ariaName)
|
||||
candidate.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScore });
|
||||
candidate.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, false)}]`, score: kRoleWithNameScore });
|
||||
else
|
||||
candidate.push({ engine: 'internal:role', selector: ariaRole, score: kRoleWithoutNameScore });
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
|||
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has' | 'frame';
|
||||
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||
|
||||
type LocatorOptions = { attrs?: { name: string, value: string | boolean | number}[], exact?: boolean, name?: string | RegExp };
|
||||
export interface LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options?: { attrs?: Record<string, string | boolean>, exact?: boolean }): string;
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options?: LocatorOptions): string;
|
||||
}
|
||||
|
||||
export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false): string {
|
||||
|
|
@ -69,10 +70,18 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
|||
}
|
||||
if (part.name === 'internal:role') {
|
||||
const attrSelector = parseAttributeSelector(part.body as string, true);
|
||||
const attrs: Record<string, boolean | string> = {};
|
||||
for (const attr of attrSelector.attributes!)
|
||||
attrs[attr.name === 'include-hidden' ? 'includeHidden' : attr.name] = attr.value;
|
||||
tokens.push(factory.generateLocator(base, 'role', attrSelector.name, { attrs }));
|
||||
const options: LocatorOptions = { attrs: [] };
|
||||
for (const attr of attrSelector.attributes) {
|
||||
if (attr.name === 'name') {
|
||||
options.exact = attr.caseSensitive;
|
||||
options.name = attr.value;
|
||||
} else {
|
||||
if (attr.name === 'level' && typeof attr.value === 'string')
|
||||
attr.value = +attr.value;
|
||||
options.attrs!.push({ name: attr.name === 'include-hidden' ? 'includeHidden' : attr.name, value: attr.value });
|
||||
}
|
||||
}
|
||||
tokens.push(factory.generateLocator(base, 'role', attrSelector.name, options));
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:testid') {
|
||||
|
|
@ -134,7 +143,7 @@ function detectExact(text: string): { exact?: boolean, text: string | RegExp } {
|
|||
}
|
||||
|
||||
export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, exact?: boolean } = {}): string {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
|
|
@ -148,7 +157,14 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
|||
return `last()`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
for (const [name, value] of Object.entries(options.attrs!))
|
||||
if (isRegExp(options.name)) {
|
||||
attrs.push(`name: ${options.name}`);
|
||||
} else if (typeof options.name === 'string') {
|
||||
attrs.push(`name: ${this.quote(options.name)}`);
|
||||
if (options.exact)
|
||||
attrs.push(`exact: true`);
|
||||
}
|
||||
for (const { name, value } of options.attrs!)
|
||||
attrs.push(`${name}: ${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
const attrString = attrs.length ? `, { ${attrs.join(', ')} }` : '';
|
||||
return `getByRole(${this.quote(body as string)}${attrString})`;
|
||||
|
|
@ -191,7 +207,7 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
|
||||
export class PythonLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, exact?: boolean } = {}): string {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
|
|
@ -205,8 +221,19 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
return `last`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
for (const [name, value] of Object.entries(options.attrs!))
|
||||
attrs.push(`${toSnakeCase(name)}=${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
if (isRegExp(options.name)) {
|
||||
attrs.push(`name=${this.regexToString(options.name)}`);
|
||||
} else if (typeof options.name === 'string') {
|
||||
attrs.push(`name=${this.quote(options.name)}`);
|
||||
if (options.exact)
|
||||
attrs.push(`exact=True`);
|
||||
}
|
||||
for (const { name, value } of options.attrs!) {
|
||||
let valueString = typeof value === 'string' ? this.quote(value) : value;
|
||||
if (typeof value === 'boolean')
|
||||
valueString = value ? 'True' : 'False';
|
||||
attrs.push(`${toSnakeCase(name)}=${valueString}`);
|
||||
}
|
||||
const attrString = attrs.length ? `, ${attrs.join(', ')}` : '';
|
||||
return `get_by_role(${this.quote(body as string)}${attrString})`;
|
||||
case 'has-text':
|
||||
|
|
@ -230,21 +257,22 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private regexToString(body: RegExp) {
|
||||
const suffix = body.flags.includes('i') ? ', re.IGNORECASE' : '';
|
||||
return `re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix})`;
|
||||
}
|
||||
|
||||
private toCallWithExact(method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', re.IGNORECASE' : '';
|
||||
return `${method}(re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix}))`;
|
||||
}
|
||||
if (isRegExp(body))
|
||||
return `${method}(${this.regexToString(body)})`;
|
||||
if (exact)
|
||||
return `${method}(${this.quote(body)}, exact=True)`;
|
||||
return `${method}(${this.quote(body)})`;
|
||||
}
|
||||
|
||||
private toHasText(body: string | RegExp) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', re.IGNORECASE' : '';
|
||||
return `re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix})`;
|
||||
}
|
||||
if (isRegExp(body))
|
||||
return this.regexToString(body);
|
||||
return `${this.quote(body)}`;
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +282,7 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
|
||||
export class JavaLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, exact?: boolean } = {}): string {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
let clazz: string;
|
||||
switch (base) {
|
||||
case 'page': clazz = 'Page'; break;
|
||||
|
|
@ -274,7 +302,14 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
return `last()`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
for (const [name, value] of Object.entries(options.attrs!))
|
||||
if (isRegExp(options.name)) {
|
||||
attrs.push(`.setName(${this.regexToString(options.name)})`);
|
||||
} else if (typeof options.name === 'string') {
|
||||
attrs.push(`.setName(${this.quote(options.name)})`);
|
||||
if (options.exact)
|
||||
attrs.push(`.setExact(true)`);
|
||||
}
|
||||
for (const { name, value } of options.attrs!)
|
||||
attrs.push(`.set${toTitleCase(name)}(${typeof value === 'string' ? this.quote(value) : value})`);
|
||||
const attrString = attrs.length ? `, new ${clazz}.GetByRoleOptions()${attrs.join('')}` : '';
|
||||
return `getByRole(AriaRole.${toSnakeCase(body as string).toUpperCase()}${attrString})`;
|
||||
|
|
@ -299,21 +334,22 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private regexToString(body: RegExp) {
|
||||
const suffix = body.flags.includes('i') ? ', Pattern.CASE_INSENSITIVE' : '';
|
||||
return `Pattern.compile(${this.quote(body.source)}${suffix})`;
|
||||
}
|
||||
|
||||
private toCallWithExact(clazz: string, method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', Pattern.CASE_INSENSITIVE' : '';
|
||||
return `${method}(Pattern.compile(${this.quote(body.source)}${suffix}))`;
|
||||
}
|
||||
if (isRegExp(body))
|
||||
return `${method}(${this.regexToString(body)})`;
|
||||
if (exact)
|
||||
return `${method}(${this.quote(body)}, new ${clazz}.${toTitleCase(method)}Options().setExact(true))`;
|
||||
return `${method}(${this.quote(body)})`;
|
||||
}
|
||||
|
||||
private toHasText(body: string | RegExp) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', Pattern.CASE_INSENSITIVE' : '';
|
||||
return `Pattern.compile(${this.quote(body.source)}${suffix})`;
|
||||
}
|
||||
if (isRegExp(body))
|
||||
return this.regexToString(body);
|
||||
return this.quote(body);
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +359,7 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
|
||||
export class CSharpLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, exact?: boolean } = {}): string {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
return `Locator(${this.quote(body as string)})`;
|
||||
|
|
@ -337,14 +373,19 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
|||
return `Last`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
for (const [name, value] of Object.entries(options.attrs!)) {
|
||||
const optionKey = name === 'name' ? 'NameString' : toTitleCase(name);
|
||||
attrs.push(`${optionKey} = ${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
if (isRegExp(options.name)) {
|
||||
attrs.push(`NameRegex = ${this.regexToString(options.name)}`);
|
||||
} else if (typeof options.name === 'string') {
|
||||
attrs.push(`NameString = ${this.quote(options.name)}`);
|
||||
if (options.exact)
|
||||
attrs.push(`Exact = true`);
|
||||
}
|
||||
for (const { name, value } of options.attrs!)
|
||||
attrs.push(`${toTitleCase(name)} = ${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
const attrString = attrs.length ? `, new() { ${attrs.join(', ')} }` : '';
|
||||
return `GetByRole(AriaRole.${toTitleCase(body as string)}${attrString})`;
|
||||
case 'has-text':
|
||||
return `Filter(new() { HasTextString = ${this.toHasText(body)} })`;
|
||||
return `Filter(new() { ${this.toHasText(body)} })`;
|
||||
case 'has':
|
||||
return `Filter(new() { Has = ${body} })`;
|
||||
case 'test-id':
|
||||
|
|
@ -364,22 +405,23 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private regexToString(body: RegExp): string {
|
||||
const suffix = body.flags.includes('i') ? ', RegexOptions.IgnoreCase' : '';
|
||||
return `new Regex(${this.quote(body.source)}${suffix})`;
|
||||
}
|
||||
|
||||
private toCallWithExact(method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', RegexOptions.IgnoreCase' : '';
|
||||
return `${method}(new Regex(${this.quote(body.source)}${suffix}))`;
|
||||
}
|
||||
if (isRegExp(body))
|
||||
return `${method}(${this.regexToString(body)})`;
|
||||
if (exact)
|
||||
return `${method}(${this.quote(body)}, new() { Exact = true })`;
|
||||
return `${method}(${this.quote(body)})`;
|
||||
}
|
||||
|
||||
private toHasText(body: string | RegExp) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', RegexOptions.IgnoreCase' : '';
|
||||
return `new Regex(${this.quote(body.source)}${suffix})`;
|
||||
}
|
||||
return this.quote(body);
|
||||
if (isRegExp(body))
|
||||
return `HasTextRegex = ${this.regexToString(body)}`;
|
||||
return `HasTextString = ${this.quote(body)}`;
|
||||
}
|
||||
|
||||
private quote(text: string) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { escapeForAttributeSelector, escapeForTextSelector, isString } from './s
|
|||
export type ByRoleOptions = {
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
exact?: boolean;
|
||||
expanded?: boolean;
|
||||
includeHidden?: boolean;
|
||||
level?: number;
|
||||
|
|
@ -72,7 +73,7 @@ export function getByRoleSelector(role: string, options: ByRoleOptions = {}): st
|
|||
if (options.level !== undefined)
|
||||
props.push(['level', String(options.level)]);
|
||||
if (options.name !== undefined)
|
||||
props.push(['name', isString(options.name) ? escapeForAttributeSelector(options.name, false) : String(options.name)]);
|
||||
props.push(['name', isString(options.name) ? escapeForAttributeSelector(options.name, !!options.exact) : String(options.name)]);
|
||||
if (options.pressed !== undefined)
|
||||
props.push(['pressed', String(options.pressed)]);
|
||||
return `internal:role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}`;
|
||||
|
|
|
|||
|
|
@ -2517,15 +2517,14 @@ export interface Page {
|
|||
*/
|
||||
getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
|
||||
/**
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
|
||||
*
|
||||
* Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
|
||||
*/
|
||||
checked?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
* An attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
*
|
||||
* > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about
|
||||
* [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled).
|
||||
|
|
@ -2533,14 +2532,20 @@ export interface Page {
|
|||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-expanded`.
|
||||
* Whether `name` is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when `name` is a regular
|
||||
* expression. Note that exact match still trims whitespace.
|
||||
*/
|
||||
exact?: boolean;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-expanded`.
|
||||
*
|
||||
* Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
*/
|
||||
expanded?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* Option that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
*
|
||||
* Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
|
||||
|
|
@ -2556,21 +2561,22 @@ export interface Page {
|
|||
level?: number;
|
||||
|
||||
/**
|
||||
* A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
* Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is
|
||||
* case-insensitive and searches for a substring, use `exact` to control this behavior.
|
||||
*
|
||||
* Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
*/
|
||||
name?: string|RegExp;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-pressed`.
|
||||
*
|
||||
* Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
|
||||
*/
|
||||
pressed?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-selected`.
|
||||
* An attribute that is usually set by `aria-selected`.
|
||||
*
|
||||
* Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
|
||||
*/
|
||||
|
|
@ -5657,15 +5663,14 @@ export interface Frame {
|
|||
*/
|
||||
getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
|
||||
/**
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
|
||||
*
|
||||
* Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
|
||||
*/
|
||||
checked?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
* An attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
*
|
||||
* > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about
|
||||
* [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled).
|
||||
|
|
@ -5673,14 +5678,20 @@ export interface Frame {
|
|||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-expanded`.
|
||||
* Whether `name` is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when `name` is a regular
|
||||
* expression. Note that exact match still trims whitespace.
|
||||
*/
|
||||
exact?: boolean;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-expanded`.
|
||||
*
|
||||
* Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
*/
|
||||
expanded?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* Option that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
*
|
||||
* Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
|
||||
|
|
@ -5696,21 +5707,22 @@ export interface Frame {
|
|||
level?: number;
|
||||
|
||||
/**
|
||||
* A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
* Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is
|
||||
* case-insensitive and searches for a substring, use `exact` to control this behavior.
|
||||
*
|
||||
* Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
*/
|
||||
name?: string|RegExp;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-pressed`.
|
||||
*
|
||||
* Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
|
||||
*/
|
||||
pressed?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-selected`.
|
||||
* An attribute that is usually set by `aria-selected`.
|
||||
*
|
||||
* Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
|
||||
*/
|
||||
|
|
@ -10187,15 +10199,14 @@ export interface Locator {
|
|||
*/
|
||||
getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
|
||||
/**
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
|
||||
*
|
||||
* Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
|
||||
*/
|
||||
checked?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
* An attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
*
|
||||
* > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about
|
||||
* [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled).
|
||||
|
|
@ -10203,14 +10214,20 @@ export interface Locator {
|
|||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-expanded`.
|
||||
* Whether `name` is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when `name` is a regular
|
||||
* expression. Note that exact match still trims whitespace.
|
||||
*/
|
||||
exact?: boolean;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-expanded`.
|
||||
*
|
||||
* Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
*/
|
||||
expanded?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* Option that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
*
|
||||
* Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
|
||||
|
|
@ -10226,21 +10243,22 @@ export interface Locator {
|
|||
level?: number;
|
||||
|
||||
/**
|
||||
* A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
* Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is
|
||||
* case-insensitive and searches for a substring, use `exact` to control this behavior.
|
||||
*
|
||||
* Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
*/
|
||||
name?: string|RegExp;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-pressed`.
|
||||
*
|
||||
* Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
|
||||
*/
|
||||
pressed?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-selected`.
|
||||
* An attribute that is usually set by `aria-selected`.
|
||||
*
|
||||
* Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
|
||||
*/
|
||||
|
|
@ -15633,15 +15651,14 @@ export interface FrameLocator {
|
|||
*/
|
||||
getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
|
||||
/**
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
|
||||
*
|
||||
* Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
|
||||
*/
|
||||
checked?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
* An attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
*
|
||||
* > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about
|
||||
* [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled).
|
||||
|
|
@ -15649,14 +15666,20 @@ export interface FrameLocator {
|
|||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-expanded`.
|
||||
* Whether `name` is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when `name` is a regular
|
||||
* expression. Note that exact match still trims whitespace.
|
||||
*/
|
||||
exact?: boolean;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-expanded`.
|
||||
*
|
||||
* Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
*/
|
||||
expanded?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* Option that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
*
|
||||
* Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
|
||||
|
|
@ -15672,21 +15695,22 @@ export interface FrameLocator {
|
|||
level?: number;
|
||||
|
||||
/**
|
||||
* A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
* Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is
|
||||
* case-insensitive and searches for a substring, use `exact` to control this behavior.
|
||||
*
|
||||
* Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
*/
|
||||
name?: string|RegExp;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-pressed`.
|
||||
*
|
||||
* Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
|
||||
*/
|
||||
pressed?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-selected`.
|
||||
* An attribute that is usually set by `aria-selected`.
|
||||
*
|
||||
* Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -71,10 +71,10 @@ test('should pick element', async ({ backend, connectedBrowser }) => {
|
|||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
selector: 'internal:role=button[name=\"Submit\"s]',
|
||||
selector: 'internal:role=button[name=\"Submit\"i]',
|
||||
locator: 'getByRole(\'button\', { name: \'Submit\' })',
|
||||
}, {
|
||||
selector: 'internal:role=button[name=\"Submit\"s]',
|
||||
selector: 'internal:role=button[name=\"Submit\"i]',
|
||||
locator: 'getByRole(\'button\', { name: \'Submit\' })',
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -175,6 +175,39 @@ it('reverse engineer locators', async ({ page }) => {
|
|||
});
|
||||
});
|
||||
|
||||
it('reverse engineer getByRole', async ({ page }) => {
|
||||
expect.soft(generate(page.getByRole('button'))).toEqual({
|
||||
javascript: `getByRole('button')`,
|
||||
python: `get_by_role("button")`,
|
||||
java: `getByRole(AriaRole.BUTTON)`,
|
||||
csharp: `GetByRole(AriaRole.Button)`,
|
||||
});
|
||||
expect.soft(generate(page.getByRole('button', { name: 'Hello' }))).toEqual({
|
||||
javascript: `getByRole('button', { name: 'Hello' })`,
|
||||
python: `get_by_role("button", name="Hello")`,
|
||||
java: `getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Hello"))`,
|
||||
csharp: `GetByRole(AriaRole.Button, new() { NameString = "Hello" })`,
|
||||
});
|
||||
expect.soft(generate(page.getByRole('button', { name: /Hello/ }))).toEqual({
|
||||
javascript: `getByRole('button', { name: /Hello/ })`,
|
||||
python: `get_by_role("button", name=re.compile(r"Hello"))`,
|
||||
java: `getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("Hello")))`,
|
||||
csharp: `GetByRole(AriaRole.Button, new() { NameRegex = new Regex("Hello") })`,
|
||||
});
|
||||
expect.soft(generate(page.getByRole('button', { name: 'He"llo', exact: true }))).toEqual({
|
||||
javascript: `getByRole('button', { name: 'He"llo', exact: true })`,
|
||||
python: `get_by_role("button", name="He\\"llo", exact=True)`,
|
||||
java: `getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("He\\"llo").setExact(true))`,
|
||||
csharp: `GetByRole(AriaRole.Button, new() { NameString = "He\\"llo", Exact = true })`,
|
||||
});
|
||||
expect.soft(generate(page.getByRole('button', { checked: true, pressed: false, level: 3 }))).toEqual({
|
||||
javascript: `getByRole('button', { checked: true, level: 3, pressed: false })`,
|
||||
python: `get_by_role("button", checked=True, level=3, pressed=False)`,
|
||||
java: `getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setChecked(true).setLevel(3).setPressed(false))`,
|
||||
csharp: `GetByRole(AriaRole.Button, new() { Checked = true, Level = 3, Pressed = false })`,
|
||||
});
|
||||
});
|
||||
|
||||
it('reverse engineer ignore-case locators', async ({ page }) => {
|
||||
expect.soft(generate(page.getByText('hello my\nwo"rld'))).toEqual({
|
||||
csharp: 'GetByText("hello my\\nwo\\"rld")',
|
||||
|
|
@ -244,14 +277,14 @@ it('reverse engineer hasText', async ({ page }) => {
|
|||
});
|
||||
|
||||
expect.soft(generate(page.getByText('Hello').filter({ hasText: /wo\/\srld\n/ }))).toEqual({
|
||||
csharp: `GetByText("Hello").Filter(new() { HasTextString = new Regex("wo\\\\/\\\\srld\\\\n") })`,
|
||||
csharp: `GetByText("Hello").Filter(new() { HasTextRegex = new Regex("wo\\\\/\\\\srld\\\\n") })`,
|
||||
java: `getByText("Hello").filter(new Locator.LocatorOptions().setHasText(Pattern.compile("wo\\\\/\\\\srld\\\\n")))`,
|
||||
javascript: `getByText('Hello').filter({ hasText: /wo\\/\\srld\\n/ })`,
|
||||
python: `get_by_text("Hello").filter(has_text=re.compile(r"wo/\\srld\\n"))`,
|
||||
});
|
||||
|
||||
expect.soft(generate(page.getByText('Hello').filter({ hasText: /wor"ld/ }))).toEqual({
|
||||
csharp: `GetByText("Hello").Filter(new() { HasTextString = new Regex("wor\\"ld") })`,
|
||||
csharp: `GetByText("Hello").Filter(new() { HasTextRegex = new Regex("wor\\"ld") })`,
|
||||
java: `getByText("Hello").filter(new Locator.LocatorOptions().setHasText(Pattern.compile("wor\\"ld")))`,
|
||||
javascript: `getByText('Hello').filter({ hasText: /wor"ld/ })`,
|
||||
python: `get_by_text("Hello").filter(has_text=re.compile(r"wor\\"ld"))`,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ it.describe('selector generator', () => {
|
|||
|
||||
it('should generate text for <input type=button>', async ({ page }) => {
|
||||
await page.setContent(`<input type=button value="Click me">`);
|
||||
expect(await generate(page, 'input')).toBe('internal:role=button[name=\"Click me\"s]');
|
||||
expect(await generate(page, 'input')).toBe('internal:role=button[name=\"Click me\"i]');
|
||||
});
|
||||
|
||||
it('should trim text', async ({ page }) => {
|
||||
|
|
@ -347,7 +347,7 @@ it.describe('selector generator', () => {
|
|||
|
||||
await page.setContent(`<button><span></span></button><button></button>`);
|
||||
await page.$eval('button', button => button.setAttribute('aria-label', `!#'!?:`));
|
||||
expect(await generate(page, 'button')).toBe(`internal:role=button[name="!#'!?:"s]`);
|
||||
expect(await generate(page, 'button')).toBe(`internal:role=button[name="!#'!?:"i]`);
|
||||
expect(await page.$(`role=button[name="!#'!?:"]`)).toBeTruthy();
|
||||
|
||||
await page.setContent(`<div><span></span></div>`);
|
||||
|
|
@ -371,7 +371,7 @@ it.describe('selector generator', () => {
|
|||
|
||||
it('should accept valid aria-label for candidate consideration', async ({ page }) => {
|
||||
await page.setContent(`<button aria-label="ariaLabel" id="buttonId"></button>`);
|
||||
expect(await generate(page, 'button')).toBe('internal:role=button[name=\"ariaLabel\"s]');
|
||||
expect(await generate(page, 'button')).toBe('internal:role=button[name=\"ariaLabel\"i]');
|
||||
});
|
||||
|
||||
it('should ignore empty role for candidate consideration', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -144,3 +144,36 @@ world</label><input id=control />`);
|
|||
await expect(page.getByAltText('hello my\nworld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByTitle('hello my\nworld')).toHaveAttribute('id', 'control');
|
||||
});
|
||||
|
||||
it('getByRole escaping', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<a href="https://playwright.dev">issues 123</a>
|
||||
<a href="https://playwright.dev">he llo 56</a>
|
||||
<button>Click me</button>
|
||||
`);
|
||||
expect.soft(await page.getByRole('button').evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button>Click me</button>`,
|
||||
]);
|
||||
expect.soft(await page.getByRole('link').evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<a href="https://playwright.dev">issues 123</a>`,
|
||||
`<a href="https://playwright.dev">he llo 56</a>`,
|
||||
]);
|
||||
|
||||
expect.soft(await page.getByRole('link', { name: 'issues 123' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<a href="https://playwright.dev">issues 123</a>`,
|
||||
]);
|
||||
expect.soft(await page.getByRole('link', { name: 'sues' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<a href="https://playwright.dev">issues 123</a>`,
|
||||
]);
|
||||
expect.soft(await page.getByRole('link', { name: ' he \n llo ' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<a href="https://playwright.dev">he llo 56</a>`,
|
||||
]);
|
||||
expect.soft(await page.getByRole('button', { name: 'issues' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
]);
|
||||
|
||||
expect.soft(await page.getByRole('link', { name: 'sues', exact: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
]);
|
||||
expect.soft(await page.getByRole('link', { name: ' he \n llo 56 ', exact: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<a href="https://playwright.dev">he llo 56</a>`,
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -317,17 +317,20 @@ test('should filter hidden, unless explicitly asked for', async ({ page }) => {
|
|||
|
||||
test('should support name', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<div role="button" aria-label="Hello"></div>
|
||||
<div role="button" aria-label=" Hello "></div>
|
||||
<div role="button" aria-label="Hallo"></div>
|
||||
<div role="button" aria-label="Hello" aria-hidden="true"></div>
|
||||
<div role="button" aria-label="123" aria-hidden="true"></div>
|
||||
<div role="button" aria-label='foo"bar' aria-hidden="true"></div>
|
||||
`);
|
||||
expect(await page.locator(`role=button[name="Hello"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
]);
|
||||
expect(await page.locator(`role=button[name=" \n Hello "]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
]);
|
||||
expect(await page.getByRole('button', { name: 'Hello' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
]);
|
||||
|
||||
expect(await page.locator(`role=button[name*="all"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
|
|
@ -335,38 +338,38 @@ test('should support name', async ({ page }) => {
|
|||
]);
|
||||
|
||||
expect(await page.locator(`role=button[name=/^H[ae]llo$/]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
`<div role="button" aria-label="Hallo"></div>`,
|
||||
]);
|
||||
expect(await page.getByRole('button', { name: /^H[ae]llo$/ }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
`<div role="button" aria-label="Hallo"></div>`,
|
||||
]);
|
||||
|
||||
expect(await page.locator(`role=button[name=/h.*o/i]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
`<div role="button" aria-label="Hallo"></div>`,
|
||||
]);
|
||||
expect(await page.getByRole('button', { name: /h.*o/i }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
`<div role="button" aria-label="Hallo"></div>`,
|
||||
]);
|
||||
|
||||
expect(await page.locator(`role=button[name="Hello"][include-hidden]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
`<div role="button" aria-label="Hello" aria-hidden="true"></div>`,
|
||||
]);
|
||||
expect(await page.getByRole('button', { name: 'Hello', includeHidden: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
`<div role="button" aria-label="Hello" aria-hidden="true"></div>`,
|
||||
]);
|
||||
expect(await page.getByRole('button', { name: 'hello', includeHidden: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
`<div role="button" aria-label="Hello" aria-hidden="true"></div>`,
|
||||
]);
|
||||
|
||||
expect(await page.locator(`role=button[name=Hello]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
]);
|
||||
expect(await page.locator(`role=button[name=123][include-hidden]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="123" aria-hidden="true"></div>`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue