Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/api/class-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/src/api/class-framelocator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/src/api/class-locator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 14 additions & 8 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -1109,15 +1109,15 @@ 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).

## locator-get-by-role-option-disabled
* 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.
Expand All @@ -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).

Expand All @@ -1152,23 +1152,29 @@ 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).

## locator-get-by-role-option-selected
* 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).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
Expand All @@ -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);
Expand Down
17 changes: 12 additions & 5 deletions packages/playwright-core/src/server/injected/roleSelectorEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can follow up and rename all 'i' to 'lax'

if (internal && !nameAttr.caseSensitive && nameAttr.op === '=')
nameAttr.op = '*=';
if (!matchesAttributePart(accessibleName, nameAttr))
return;
}
Expand All @@ -170,5 +176,6 @@ export const RoleEngine: SelectorEngine = {

query(scope);
return result;
}
};
};
return { queryAll };
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading