diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md
index 347d75584c..504f3aa4ac 100644
--- a/docs/src/api/class-frame.md
+++ b/docs/src/api/class-frame.md
@@ -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
diff --git a/docs/src/api/class-framelocator.md b/docs/src/api/class-framelocator.md
index 81332862ba..6b643b36f7 100644
--- a/docs/src/api/class-framelocator.md
+++ b/docs/src/api/class-framelocator.md
@@ -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
diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md
index a16277fbde..1a7b0ca416 100644
--- a/docs/src/api/class-locator.md
+++ b/docs/src/api/class-locator.md
@@ -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
diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md
index 7535cf7507..08daf543a4 100644
--- a/docs/src/api/class-page.md
+++ b/docs/src/api/class-page.md
@@ -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
diff --git a/docs/src/api/params.md b/docs/src/api/params.md
index 8fef217fed..cac56703c1 100644
--- a/docs/src/api/params.md
+++ b/docs/src/api/params.md
@@ -1109,7 +1109,7 @@ Required aria role.
* since: v1.27
- `checked` <[boolean]>
-An attribute that is usually set by `aria-checked` or native `` controls. Available values for checked are `true`, `false` and `"mixed"`.
+An attribute that is usually set by `aria-checked` or native `` 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`
-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).
diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts
index b2c05d7123..8e762d0226 100644
--- a/packages/playwright-core/src/server/injected/injectedScript.ts
+++ b/packages/playwright-core/src/server/injected/injectedScript.ts
@@ -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);
diff --git a/packages/playwright-core/src/server/injected/roleSelectorEngine.ts b/packages/playwright-core/src/server/injected/roleSelectorEngine.ts
index 0e306ee1f1..3e4d1ac5e9 100644
--- a/packages/playwright-core/src/server/injected/roleSelectorEngine.ts
+++ b/packages/playwright-core/src/server/injected/roleSelectorEngine.ts
@@ -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 };
+}
diff --git a/packages/playwright-core/src/server/injected/selectorGenerator.ts b/packages/playwright-core/src/server/injected/selectorGenerator.ts
index 93487a1934..61e985f003 100644
--- a/packages/playwright-core/src/server/injected/selectorGenerator.ts
+++ b/packages/playwright-core/src/server/injected/selectorGenerator.ts
@@ -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 {
diff --git a/packages/playwright-core/src/server/isomorphic/locatorGenerators.ts b/packages/playwright-core/src/server/isomorphic/locatorGenerators.ts
index 316520b2a9..9ce963f721 100644
--- a/packages/playwright-core/src/server/isomorphic/locatorGenerators.ts
+++ b/packages/playwright-core/src/server/isomorphic/locatorGenerators.ts
@@ -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, 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 = {};
- 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, 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, 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, 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, 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) {
diff --git a/packages/playwright-core/src/utils/isomorphic/locatorUtils.ts b/packages/playwright-core/src/utils/isomorphic/locatorUtils.ts
index a82c5c2a98..3896713d0d 100644
--- a/packages/playwright-core/src/utils/isomorphic/locatorUtils.ts
+++ b/packages/playwright-core/src/utils/isomorphic/locatorUtils.ts
@@ -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('')}`;
diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts
index d3365eb9ab..0bcf424a17 100644
--- a/packages/playwright-core/types/types.d.ts
+++ b/packages/playwright-core/types/types.d.ts
@@ -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 `` controls. Available values for
- * checked are `true`, `false` and `"mixed"`.
+ * An attribute that is usually set by `aria-checked` or native `` 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 `` controls. Available values for
- * checked are `true`, `false` and `"mixed"`.
+ * An attribute that is usually set by `aria-checked` or native `` 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 `` controls. Available values for
- * checked are `true`, `false` and `"mixed"`.
+ * An attribute that is usually set by `aria-checked` or native `` 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 `` controls. Available values for
- * checked are `true`, `false` and `"mixed"`.
+ * An attribute that is usually set by `aria-checked` or native `` 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).
*/
diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts
index dff037b37a..c905d3d918 100644
--- a/tests/library/debug-controller.spec.ts
+++ b/tests/library/debug-controller.spec.ts
@@ -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\' })',
},
]);
diff --git a/tests/library/locator-generator.spec.ts b/tests/library/locator-generator.spec.ts
index 4365519c8e..2835b107ca 100644
--- a/tests/library/locator-generator.spec.ts
+++ b/tests/library/locator-generator.spec.ts
@@ -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"))`,
diff --git a/tests/library/selector-generator.spec.ts b/tests/library/selector-generator.spec.ts
index 2f7e6d5007..856ec64bbd 100644
--- a/tests/library/selector-generator.spec.ts
+++ b/tests/library/selector-generator.spec.ts
@@ -50,7 +50,7 @@ it.describe('selector generator', () => {
it('should generate text for ', async ({ page }) => {
await page.setContent(``);
- 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(``);
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(`