Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/src/api/class-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,8 @@ Attribute name to get the value for.

### option: Frame.getByRole.description = %%-locator-get-by-role-option-description-%%

### option: Frame.getByRole.busy = %%-locator-get-by-role-option-busy-%%

## method: Frame.getByTestId
* since: v1.27
- returns: <[Locator]>
Expand Down
2 changes: 2 additions & 0 deletions docs/src/api/class-framelocator.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ in that iframe.

### option: FrameLocator.getByRole.description = %%-locator-get-by-role-option-description-%%

### option: FrameLocator.getByRole.busy = %%-locator-get-by-role-option-busy-%%

## method: FrameLocator.getByTestId
* since: v1.27
- returns: <[Locator]>
Expand Down
2 changes: 2 additions & 0 deletions docs/src/api/class-locator.md
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,8 @@ Attribute name to get the value for.

### option: Locator.getByRole.description = %%-locator-get-by-role-option-description-%%

### option: Locator.getByRole.busy = %%-locator-get-by-role-option-busy-%%

## method: Locator.getByTestId
* since: v1.27
- returns: <[Locator]>
Expand Down
2 changes: 2 additions & 0 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -2282,6 +2282,8 @@ Attribute name to get the value for.

### option: Page.getByRole.description = %%-locator-get-by-role-option-description-%%

### option: Page.getByRole.busy = %%-locator-get-by-role-option-busy-%%

## method: Page.getByTestId
* since: v1.27
- returns: <[Locator]>
Expand Down
8 changes: 8 additions & 0 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,14 @@ Whether to find an exact match: case-sensitive and whole-string. Default to fals

Required aria role.

## locator-get-by-role-option-busy
* since: v1.61
- `busy` <[boolean]>

An attribute that is usually set by `aria-busy`.

Learn more about [`aria-busy`](https://www.w3.org/TR/wai-aria-1.2/#aria-busy).

## locator-get-by-role-option-checked
* since: v1.27
- `checked` <[boolean]>
Expand Down
13 changes: 11 additions & 2 deletions packages/injected/src/roleSelectorEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { parseAttributeSelector } from '@isomorphic/selectorParser';
import { normalizeWhiteSpace } from '@isomorphic/stringUtils';

import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleDescription, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
import { beginAriaCaches, endAriaCaches, getAriaBusy, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleDescription, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
import { matchesAttributePart } from './selectorUtils';

import type { AttributeSelectorOperator, AttributeSelectorPart } from '@isomorphic/selectorParser';
Expand All @@ -37,10 +37,11 @@ type RoleEngineOptions = {
expanded?: boolean;
level?: number;
disabled?: boolean;
busy?: boolean;
includeHidden?: boolean;
};

const kSupportedAttributes = ['selected', 'checked', 'pressed', 'expanded', 'level', 'disabled', 'name', 'description', 'include-hidden'];
const kSupportedAttributes = ['selected', 'checked', 'pressed', 'expanded', 'level', 'disabled', 'busy', 'name', 'description', 'include-hidden'];
kSupportedAttributes.sort();

function validateSupportedRole(attr: string, roles: string[], role: string) {
Expand Down Expand Up @@ -106,6 +107,12 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string): RoleE
options.disabled = attr.op === '<truthy>' ? true : attr.value;
break;
}
case 'busy': {
validateSupportedValues(attr, [true, false]);
validateSupportedOp(attr, ['<truthy>', '=']);
options.busy = attr.op === '<truthy>' ? true : attr.value;
break;
}
case 'name': {
if (attr.op === '<truthy>')
throw new Error(`"name" attribute must have a value`);
Expand Down Expand Up @@ -157,6 +164,8 @@ function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: bo
return;
if (options.disabled !== undefined && getAriaDisabled(element) !== options.disabled)
return;
if (options.busy !== undefined && getAriaBusy(element) !== options.busy)
return;
if (!options.includeHidden) {
const isHidden = isElementHiddenForAria(element);
if (isHidden)
Expand Down
6 changes: 6 additions & 0 deletions packages/injected/src/roleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,12 @@ function hasExplicitAriaDisabled(element: Element | undefined, isAncestor = fals
return false;
}

export function getAriaBusy(element: Element): boolean {
// https://www.w3.org/TR/wai-aria-1.2/#aria-busy
// aria-busy is a global state with a default value of "false".
return getAriaBoolean(element.getAttribute('aria-busy')) === true;
}

function getAccessibleNameFromAssociatedLabels(labels: Iterable<HTMLLabelElement>, options: AccessibleNameOptions) {
return [...labels].map(label => getTextAlternativeInternal(label, {
...options,
Expand Down
3 changes: 3 additions & 0 deletions packages/isomorphic/locatorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { escapeForAttributeSelector, escapeForTextSelector } from './stringUtils';

export type ByRoleOptions = {
busy?: boolean;
checked?: boolean;
description?: string | RegExp;
disabled?: boolean;
Expand Down Expand Up @@ -86,5 +87,7 @@ export function getByRoleSelector(role: string, options: ByRoleOptions = {}): st
props.push(['description', escapeForAttributeSelector(options.description, !!options.exact)]);
if (options.pressed !== undefined)
props.push(['pressed', String(options.pressed)]);
if (options.busy !== undefined)
props.push(['busy', String(options.busy)]);
return `internal:role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}`;
}
28 changes: 28 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2949,6 +2949,13 @@ export interface Page {
* @param options
*/
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-busy`.
*
* Learn more about [`aria-busy`](https://www.w3.org/TR/wai-aria-1.2/#aria-busy).
*/
busy?: boolean;

/**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
*
Expand Down Expand Up @@ -6808,6 +6815,13 @@ export interface Frame {
* @param options
*/
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-busy`.
*
* Learn more about [`aria-busy`](https://www.w3.org/TR/wai-aria-1.2/#aria-busy).
*/
busy?: boolean;

/**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
*
Expand Down Expand Up @@ -14057,6 +14071,13 @@ export interface Locator {
* @param options
*/
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-busy`.
*
* Learn more about [`aria-busy`](https://www.w3.org/TR/wai-aria-1.2/#aria-busy).
*/
busy?: boolean;

/**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
*
Expand Down Expand Up @@ -19499,6 +19520,13 @@ export interface FrameLocator {
* @param options
*/
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-busy`.
*
* Learn more about [`aria-busy`](https://www.w3.org/TR/wai-aria-1.2/#aria-busy).
*/
busy?: boolean;

/**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
*
Expand Down
28 changes: 28 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2949,6 +2949,13 @@ export interface Page {
* @param options
*/
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-busy`.
*
* Learn more about [`aria-busy`](https://www.w3.org/TR/wai-aria-1.2/#aria-busy).
*/
busy?: boolean;

/**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
*
Expand Down Expand Up @@ -6808,6 +6815,13 @@ export interface Frame {
* @param options
*/
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-busy`.
*
* Learn more about [`aria-busy`](https://www.w3.org/TR/wai-aria-1.2/#aria-busy).
*/
busy?: boolean;

/**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
*
Expand Down Expand Up @@ -14057,6 +14071,13 @@ export interface Locator {
* @param options
*/
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-busy`.
*
* Learn more about [`aria-busy`](https://www.w3.org/TR/wai-aria-1.2/#aria-busy).
*/
busy?: boolean;

/**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
*
Expand Down Expand Up @@ -19499,6 +19520,13 @@ export interface FrameLocator {
* @param options
*/
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-busy`.
*
* Learn more about [`aria-busy`](https://www.w3.org/TR/wai-aria-1.2/#aria-busy).
*/
busy?: boolean;

/**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
*
Expand Down
6 changes: 6 additions & 0 deletions tests/library/locator-generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ it('reverse engineer getByRole', async ({ page }) => {
java: `getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setChecked(true).setLevel(3).setPressed(false))`,
csharp: `GetByRole(AriaRole.Button, new() { Checked = true, Level = 3, Pressed = false })`,
});
expect.soft(generate(page.getByRole('cell', { busy: false }))).toEqual({
javascript: `getByRole('cell', { busy: false })`,
python: `get_by_role("cell", busy=False)`,
java: `getByRole(AriaRole.CELL, new Page.GetByRoleOptions().setBusy(false))`,
csharp: `GetByRole(AriaRole.Cell, new() { Busy = false })`,
});
expect.soft(generate(page.getByRole('alert', { name: 'Upload', description: 'doc.pdf' }))).toEqual({
javascript: `getByRole('alert', { name: 'Upload', description: 'doc.pdf' })`,
python: `get_by_role("alert", name="Upload", description="doc.pdf")`,
Expand Down
45 changes: 44 additions & 1 deletion tests/page/selectors-role.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,49 @@ test('should support expanded', async ({ page }) => {
]);
});

test('should support busy', async ({ page }) => {
await page.setContent(`
<div role="cell">Hi</div>
<div role="cell" aria-busy="true">Hello</div>
<div role="cell" aria-busy="false">Bye</div>
<button>Click</button>
<button aria-busy="true">Loading</button>
`);

expect(await page.locator(`role=cell`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<div role="cell">Hi</div>`,
`<div role="cell" aria-busy="true">Hello</div>`,
`<div role="cell" aria-busy="false">Bye</div>`,
]);

expect(await page.locator(`role=cell[busy]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<div role="cell" aria-busy="true">Hello</div>`,
]);
expect(await page.locator(`role=cell[busy=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<div role="cell" aria-busy="true">Hello</div>`,
]);
expect(await page.getByRole('cell', { busy: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<div role="cell" aria-busy="true">Hello</div>`,
]);

expect(await page.locator(`role=cell[busy=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<div role="cell">Hi</div>`,
`<div role="cell" aria-busy="false">Bye</div>`,
]);
expect(await page.getByRole('cell', { busy: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<div role="cell">Hi</div>`,
`<div role="cell" aria-busy="false">Bye</div>`,
]);

// aria-busy is a global ARIA state and should work for any role.
expect(await page.getByRole('button', { busy: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<button aria-busy="true">Loading</button>`,
]);
expect(await page.getByRole('button', { busy: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<button>Click</button>`,
]);
});

test('should support disabled', async ({ page }) => {
await page.setContent(`
<button>Hi</button>
Expand Down Expand Up @@ -511,7 +554,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", "description", "disabled", "expanded", "include-hidden", "level", "name", "pressed", "selected"`);
expect(e1.message).toContain(`Unknown attribute "sElected", must be one of "busy", "checked", "description", "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"`);
Expand Down
Loading