mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 03:39:48 +03:00
feat(role selector): docs and minor fixes (#13203)
- Added docs to `selectors.md`. - `[pressed]` and `[checked]` do not match `"mixed"` states. - Disallow `[name]` shorthand without a value. - Renamed `includeHidden` to `include-hidden`.
This commit is contained in:
parent
b2c863f6a3
commit
e5182259b1
@ -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 `<input type=checkbox>` 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 `<h1>-<h6>` 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
|
||||
|
@ -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, ['<truthy>', '=']);
|
||||
if (attr.op === '<truthy>') {
|
||||
// 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, ['<truthy>', '=']);
|
||||
if (attr.op === '<truthy>') {
|
||||
// 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 !== '<truthy>' && typeof attr.value !== 'string' && !(attr.value instanceof RegExp))
|
||||
if (attr.op === '<truthy>')
|
||||
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, ['<truthy>', '=']);
|
||||
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 === '<truthy>' || !!attr.value;
|
||||
continue;
|
||||
}
|
||||
|
@ -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([
|
||||
`<input type="checkbox" checked="">`,
|
||||
`<input type="checkbox" indeterminate="">`,
|
||||
`<div role="checkbox" aria-checked="true">Hi</div>`,
|
||||
]);
|
||||
expect(await page.$$eval(`role=checkbox[checked=true]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
@ -96,6 +95,9 @@ test('should support checked', async ({ page }) => {
|
||||
`<div role="checkbox" aria-checked="false">Hello</div>`,
|
||||
`<div role="checkbox">Unknown</div>`,
|
||||
]);
|
||||
expect(await page.$$eval(`role=checkbox[checked="mixed"]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<input type="checkbox" indeterminate="">`,
|
||||
]);
|
||||
expect(await page.$$eval(`role=checkbox`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<input type="checkbox">`,
|
||||
`<input type="checkbox" checked="">`,
|
||||
@ -115,7 +117,6 @@ test('should support pressed', async ({ page }) => {
|
||||
`);
|
||||
expect(await page.$$eval(`role=button[pressed]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button aria-pressed="true">Hello</button>`,
|
||||
`<button aria-pressed="mixed">Mixed</button>`,
|
||||
]);
|
||||
expect(await page.$$eval(`role=button[pressed=true]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button aria-pressed="true">Hello</button>`,
|
||||
@ -127,6 +128,12 @@ test('should support pressed', async ({ page }) => {
|
||||
expect(await page.$$eval(`role=button[pressed="mixed"]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button aria-pressed="mixed">Mixed</button>`,
|
||||
]);
|
||||
expect(await page.$$eval(`role=button`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button>Hi</button>`,
|
||||
`<button aria-pressed="true">Hello</button>`,
|
||||
`<button aria-pressed="false">Bye</button>`,
|
||||
`<button aria-pressed="mixed">Mixed</button>`,
|
||||
]);
|
||||
});
|
||||
|
||||
test('should support expanded', async ({ page }) => {
|
||||
@ -223,7 +230,7 @@ test('should filter hidden, unless explicitly asked for', async ({ page }) => {
|
||||
`<button style="visibility:visible">Still here</button>`,
|
||||
`<button>Shadow1</button>`,
|
||||
]);
|
||||
expect(await page.$$eval(`role=button[includeHidden]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
expect(await page.$$eval(`role=button[include-hidden]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button>Hi</button>`,
|
||||
`<button hidden="">Hello</button>`,
|
||||
`<button aria-hidden="true">Yay</button>`,
|
||||
@ -235,7 +242,7 @@ test('should filter hidden, unless explicitly asked for', async ({ page }) => {
|
||||
`<button>Shadow1</button>`,
|
||||
`<button>Shadow2</button>`,
|
||||
]);
|
||||
expect(await page.$$eval(`role=button[includeHidden=true]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
expect(await page.$$eval(`role=button[include-hidden=true]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button>Hi</button>`,
|
||||
`<button hidden="">Hello</button>`,
|
||||
`<button aria-hidden="true">Yay</button>`,
|
||||
@ -247,7 +254,7 @@ test('should filter hidden, unless explicitly asked for', async ({ page }) => {
|
||||
`<button>Shadow1</button>`,
|
||||
`<button>Shadow2</button>`,
|
||||
]);
|
||||
expect(await page.$$eval(`role=button[includeHidden=false]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
expect(await page.$$eval(`role=button[include-hidden=false]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button>Hi</button>`,
|
||||
`<button aria-hidden="false">Nay</button>`,
|
||||
`<button style="visibility:visible">Still here</button>`,
|
||||
@ -275,7 +282,7 @@ test('should support name', async ({ page }) => {
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label="Hallo"></div>`,
|
||||
]);
|
||||
expect(await page.$$eval(`role=button[name="Hello"][includeHidden]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
expect(await page.$$eval(`role=button[name="Hello"][include-hidden]`, els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
`<div role="button" aria-label="Hello" aria-hidden="true"></div>`,
|
||||
]);
|
||||
@ -286,7 +293,7 @@ test('errors', async ({ page }) => {
|
||||
expect(e0.message).toContain(`Role must not be empty`);
|
||||
|
||||
const e1 = await page.$('role=foo[sElected]').catch(e => e);
|
||||
expect(e1.message).toContain(`Unknown attribute "sElected", must be one of "checked", "disabled", "expanded", "includeHidden", "level", "name", "pressed", "selected"`);
|
||||
expect(e1.message).toContain(`Unknown attribute "sElected", must be one of "checked", "disabled", "expanded", "include-hidden", "level", "name", "pressed", "selected"`);
|
||||
|
||||
const e2 = await page.$('role=foo[bar . qux=true]').catch(e => e);
|
||||
expect(e2.message).toContain(`Unknown attribute "bar.qux"`);
|
||||
@ -302,4 +309,7 @@ test('errors', async ({ page }) => {
|
||||
|
||||
const e6 = await page.$('role=button[level=3]').catch(e => e);
|
||||
expect(e6.message).toContain(`"level" attribute is only supported for roles: "heading", "listitem", "row", "treeitem"`);
|
||||
|
||||
const e7 = await page.$('role=button[name]').catch(e => e);
|
||||
expect(e7.message).toContain(`"name" attribute must have a value`);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user