diff --git a/docs/src/selectors.md b/docs/src/selectors.md
index bb7de8d012..db9f25a83a 100644
--- a/docs/src/selectors.md
+++ b/docs/src/selectors.md
@@ -814,6 +814,76 @@ Vue selectors, as well as [Vue DevTools](https://chrome.google.com/webstore/deta
:::
+## Role selector
+
+:::note
+Role selector is experimental, only available when running with `PLAYWRIGHT_EXPERIMENTAL_FEATURES=1` enviroment variable.
+:::
+
+Role selector allows selecting elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). Note that role selector **does not replace** accessibility audits and conformance tests, but rather gives early feedback about the ARIA guidelines.
+
+The syntax is very similar to [CSS attribute selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors). For example, `role=button[name="Click me"][pressed]` selects a pressed button that has accessible name "Click me".
+
+Note that many html elements have an implicitly [defined role](https://w3c.github.io/html-aam/#html-element-role-mappings) that is recognized by the role selector. You can find all the [supported roles here](https://www.w3.org/TR/wai-aria-1.2/#role_definitions). ARIA guidelines **do not recommend** duplicating implicit roles and attributes by setting `role` and/or `aria-*` attributes to default values.
+
+Attributes supported by the role selector:
+* `checked` - an attribute that is usually set by `aria-checked` or native `` controls. Available values for checked are `true`, `false` and `"mixed"`. Examples:
+ - `role=checkbox[checked=true]`, equivalent to `role=checkbox[checked]`
+ - `role=checkbox[checked=false]`
+ - `role=checkbox[checked="mixed"]`
+
+ Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
+
+* `disabled` - a boolean attribute that is usually set by `aria-disabled` or `disabled`. Examples:
+ - `role=button[disabled=true]`, equivalent to `role=button[disabled]`
+ - `role=button[disabled=false]`
+
+ Note that 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).
+
+* `expanded` - a boolean attribute that is usually set by `aria-expanded`. Examples:
+ - `role=button[expanded=true]`, equivalent to `role=button[expanded]`
+ - `role=button[expanded=false]`
+
+ Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
+
+* `include-hidden` - 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. With `[include-hidden]`, both hidden and non-hidden elements are matched. Examples:
+ - `role=button[include-hidden=true]`, equivalent to `role=button[include-hidden]`
+ - `role=button[include-hidden=false]`
+
+ Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
+
+* `level` - a number attribute that is usually present for roles `heading`, `listitem`, `row`, `treeitem`, with default values for `
-
` elements. Examples:
+ - `role=heading[level=1]`
+
+ Learn more about [`aria-level`](https://www.w3.org/TR/wai-aria-1.2/#aria-level).
+
+* `name` - a string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). Supports attribute operators like `=` and `*=`, and regular expressions.
+ - `role=button[name="Click me"]`
+ - `role=button[name*="Click"]`
+ - `role=button[name=/Click( me)?/]`
+
+ Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
+
+* `pressed` - an attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`. Examples:
+ - `role=button[pressed=true]`, equivalent to `role=button[pressed]`
+ - `role=button[pressed=false]`
+ - `role=button[pressed="mixed"]`
+
+ Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
+
+* `selected` - a boolean attribute that is usually set by `aria-selected`. Examples:
+ - `role=option[selected=true]`, equivalent to `role=option[selected]`
+ - `role=option[selected=false]`
+
+ Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
+
+Examples:
+* `role=button` matches all buttons;
+* `role=button[name="Click me"]` matches buttons with "Click me" accessible name;
+* `role=checkbox[checked][include-hidden]` matches checkboxes that are checked, including those that are currently hidden.
+
+
## id, data-testid, data-test-id, data-test selectors
Playwright supports shorthand for selecting elements using certain attributes. Currently, only
diff --git a/packages/playwright-core/src/server/injected/roleSelectorEngine.ts b/packages/playwright-core/src/server/injected/roleSelectorEngine.ts
index 0d3a51712f..4ae908d897 100644
--- a/packages/playwright-core/src/server/injected/roleSelectorEngine.ts
+++ b/packages/playwright-core/src/server/injected/roleSelectorEngine.ts
@@ -18,7 +18,7 @@ import { SelectorEngine, SelectorRoot } from './selectorEngine';
import { matchesAttribute, parseComponentSelector, ParsedComponentAttribute, ParsedAttributeOperator } from './componentUtils';
import { getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
-const kSupportedAttributes = ['selected', 'checked', 'pressed', 'expanded', 'level', 'disabled', 'name', 'includeHidden'];
+const kSupportedAttributes = ['selected', 'checked', 'pressed', 'expanded', 'level', 'disabled', 'name', 'include-hidden'];
kSupportedAttributes.sort();
function validateSupportedRole(attr: string, roles: string[], role: string) {
@@ -43,12 +43,22 @@ function validateAttributes(attrs: ParsedComponentAttribute[], role: string) {
validateSupportedRole(attr.name, kAriaCheckedRoles, role);
validateSupportedValues(attr, [true, false, 'mixed']);
validateSupportedOp(attr, ['', '=']);
+ if (attr.op === '') {
+ // Do not match "mixed" in "option[checked]".
+ attr.op = '=';
+ attr.value = true;
+ }
break;
}
case 'pressed': {
validateSupportedRole(attr.name, kAriaPressedRoles, role);
validateSupportedValues(attr, [true, false, 'mixed']);
validateSupportedOp(attr, ['', '=']);
+ if (attr.op === '') {
+ // Do not match "mixed" in "button[pressed]".
+ attr.op = '=';
+ attr.value = true;
+ }
break;
}
case 'selected': {
@@ -75,11 +85,13 @@ function validateAttributes(attrs: ParsedComponentAttribute[], role: string) {
break;
}
case 'name': {
- if (attr.op !== '' && typeof attr.value !== 'string' && !(attr.value instanceof RegExp))
+ if (attr.op === '')
+ throw new Error(`"name" attribute must have a value`);
+ if (typeof attr.value !== 'string' && !(attr.value instanceof RegExp))
throw new Error(`"name" attribute must be a string or a regular expression`);
break;
}
- case 'includeHidden': {
+ case 'include-hidden': {
validateSupportedValues(attr, [true, false]);
validateSupportedOp(attr, ['', '=']);
break;
@@ -107,7 +119,7 @@ export const RoleEngine: SelectorEngine = {
let includeHidden = false; // By default, hidden elements are excluded.
let nameAttr: ParsedComponentAttribute | undefined;
for (const attr of parsed.attributes) {
- if (attr.name === 'includeHidden') {
+ if (attr.name === 'include-hidden') {
includeHidden = attr.op === '' || !!attr.value;
continue;
}
diff --git a/tests/page/selectors-role.spec.ts b/tests/page/selectors-role.spec.ts
index 4dcaf3dbd2..9a41ece1b6 100644
--- a/tests/page/selectors-role.spec.ts
+++ b/tests/page/selectors-role.spec.ts
@@ -84,7 +84,6 @@ test('should support checked', async ({ page }) => {
await page.$eval('[indeterminate]', input => (input as HTMLInputElement).indeterminate = true);
expect(await page.$$eval(`role=checkbox[checked]`, els => els.map(e => e.outerHTML))).toEqual([
``,
- ``,
`