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:
Dmitry Gozman 2022-11-11 15:58:36 -08:00 committed by GitHub
parent bc78db07df
commit a7b2b04588
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 264 additions and 111 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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).

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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).
*/

View File

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

View File

@ -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"))`,

View File

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

View File

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

View File

@ -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>`,