feat: expect(locator).toHaveRole(role) (#30555)

References #13517. Fixes #18332.
This commit is contained in:
Dmitry Gozman 2024-04-25 15:26:10 -07:00 committed by GitHub
parent 9a1b34a4b0
commit 6d20da568e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 135 additions and 7 deletions

View File

@ -1030,7 +1030,8 @@ Attribute name to get the value for.
%%-template-locator-get-by-role-%%
### param: Frame.getByRole.role = %%-locator-get-by-role-role-%%
### param: Frame.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### option: Frame.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27

View File

@ -133,7 +133,8 @@ in that iframe.
%%-template-locator-get-by-role-%%
### param: FrameLocator.getByRole.role = %%-locator-get-by-role-role-%%
### param: FrameLocator.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### option: FrameLocator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27

View File

@ -1173,7 +1173,8 @@ Attribute name to get the value for.
%%-template-locator-get-by-role-%%
### param: Locator.getByRole.role = %%-locator-get-by-role-role-%%
### param: Locator.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### option: Locator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27

View File

@ -373,6 +373,23 @@ Property value.
### option: LocatorAssertions.NotToHaveJSProperty.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
## async method: LocatorAssertions.NotToHaveRole
* since: v1.44
* langs: python
The opposite of [`method: LocatorAssertions.toHaveRole`].
### param: LocatorAssertions.NotToHaveRole.name
* since: v1.44
- `name` <[string]|[RegExp]>
Expected accessible name.
### option: LocatorAssertions.NotToHaveRole.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.44
## async method: LocatorAssertions.NotToHaveText
* since: v1.20
* langs: python
@ -1629,6 +1646,53 @@ Property value.
### option: LocatorAssertions.toHaveJSProperty.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
## async method: LocatorAssertions.toHaveRole
* since: v1.44
* langs:
- alias-java: hasRole
Ensures the [Locator] points to an element with a given [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles).
Note that role is matched as a string, disregarding the ARIA role hierarchy. For example, asserting a superclass role `"checkbox"` on an element with a subclass role `"switch"` will fail.
**Usage**
```js
const locator = page.getByTestId('save-button');
await expect(locator).toHaveRole('button');
```
```java
Locator locator = page.getByTestId("save-button");
assertThat(locator).hasRole(AriaRole.BUTTON);
```
```python async
locator = page.get_by_test_id("save-button")
await expect(locator).to_have_role("button")
```
```python sync
locator = page.get_by_test_id("save-button")
expect(locator).to_have_role("button")
```
```csharp
var locator = Page.GetByTestId("save-button");
await Expect(locator).ToHaveRoleAsync(AriaRole.Button);
```
### param: LocatorAssertions.toHaveRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.44
### option: LocatorAssertions.toHaveRole.timeout = %%-js-assertions-timeout-%%
* since: v1.44
### option: LocatorAssertions.toHaveRole.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.44
## async method: LocatorAssertions.toHaveScreenshot#1
* since: v1.23
* langs: js

View File

@ -2340,7 +2340,8 @@ Attribute name to get the value for.
%%-template-locator-get-by-role-%%
### param: Page.getByRole.role = %%-locator-get-by-role-role-%%
### param: Page.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### option: Page.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27

View File

@ -1203,8 +1203,7 @@ Text to locate the element for.
Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular expression. Note that exact match still trims whitespace.
## locator-get-by-role-role
* since: v1.27
## get-by-role-to-have-role-role
- `role` <[AriaRole]<"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">>
Required aria role.

View File

@ -18,12 +18,15 @@ title: "Assertions"
| [`method: LocatorAssertions.toBeInViewport`] | Element intersects viewport |
| [`method: LocatorAssertions.toBeVisible`] | Element is visible |
| [`method: LocatorAssertions.toContainText`] | Element contains text |
| [`method: LocatorAssertions.toHaveAccessibleDescription`] | Element has a matching [accessible description](https://w3c.github.io/accname/#dfn-accessible-description) |
| [`method: LocatorAssertions.toHaveAccessibleName`] | Element has a matching [accessible name](https://w3c.github.io/accname/#dfn-accessible-name) |
| [`method: LocatorAssertions.toHaveAttribute`] | Element has a DOM attribute |
| [`method: LocatorAssertions.toHaveClass`] | Element has a class property |
| [`method: LocatorAssertions.toHaveCount`] | List has exact number of children |
| [`method: LocatorAssertions.toHaveCSS`] | Element has CSS property |
| [`method: LocatorAssertions.toHaveId`] | Element has an ID |
| [`method: LocatorAssertions.toHaveJSProperty`] | Element has a JavaScript property |
| [`method: LocatorAssertions.toHaveRole`] | Element has a specific [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles) |
| [`method: LocatorAssertions.toHaveText`] | Element matches text |
| [`method: LocatorAssertions.toHaveValue`] | Input has a value |
| [`method: LocatorAssertions.toHaveValues`] | Select has options selected |

View File

@ -40,12 +40,15 @@ Note that retrying assertions are async, so you must `await` them.
| [await expect(locator).toBeInViewport()](./api/class-locatorassertions.md#locator-assertions-to-be-in-viewport) | Element intersects viewport |
| [await expect(locator).toBeVisible()](./api/class-locatorassertions.md#locator-assertions-to-be-visible) | Element is visible |
| [await expect(locator).toContainText()](./api/class-locatorassertions.md#locator-assertions-to-contain-text) | Element contains text |
| [await expect(locator).toHaveAccessibleDescription()](./api/class-locatorassertions.md#locator-assertions-to-have-accessible-description) | Element has a matching [accessible description](https://w3c.github.io/accname/#dfn-accessible-description) |
| [await expect(locator).toHaveAccessibleName()](./api/class-locatorassertions.md#locator-assertions-to-have-accessible-name) | Element has a matching [accessible name](https://w3c.github.io/accname/#dfn-accessible-name) |
| [await expect(locator).toHaveAttribute()](./api/class-locatorassertions.md#locator-assertions-to-have-attribute) | Element has a DOM attribute |
| [await expect(locator).toHaveClass()](./api/class-locatorassertions.md#locator-assertions-to-have-class) | Element has a class property |
| [await expect(locator).toHaveCount()](./api/class-locatorassertions.md#locator-assertions-to-have-count) | List has exact number of children |
| [await expect(locator).toHaveCSS()](./api/class-locatorassertions.md#locator-assertions-to-have-css) | Element has CSS property |
| [await expect(locator).toHaveId()](./api/class-locatorassertions.md#locator-assertions-to-have-id) | Element has an ID |
| [await expect(locator).toHaveJSProperty()](./api/class-locatorassertions.md#locator-assertions-to-have-js-property) | Element has a JavaScript property |
| [await expect(locator).toHaveRole()](./api/class-locatorassertions.md#locator-assertions-to-have-role) | Element has a specific [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles) |
| [await expect(locator).toHaveScreenshot()](./api/class-locatorassertions.md#locator-assertions-to-have-screenshot-1) | Element has a screenshot |
| [await expect(locator).toHaveText()](./api/class-locatorassertions.md#locator-assertions-to-have-text) | Element matches text |
| [await expect(locator).toHaveValue()](./api/class-locatorassertions.md#locator-assertions-to-have-value) | Input has a value |

View File

@ -1227,6 +1227,8 @@ export class InjectedScript {
received = getElementAccessibleName(element, false /* includeHidden */);
} else if (expression === 'to.have.accessible.description') {
received = getElementAccessibleDescription(element, false /* includeHidden */);
} else if (expression === 'to.have.role') {
received = getAriaRole(element) || '';
} else if (expression === 'to.have.title') {
received = this.document.title;
} else if (expression === 'to.have.url') {

View File

@ -40,6 +40,7 @@ import {
toHaveCSS,
toHaveId,
toHaveJSProperty,
toHaveRole,
toHaveText,
toHaveTitle,
toHaveURL,
@ -195,6 +196,7 @@ const customAsyncMatchers = {
toHaveCSS,
toHaveId,
toHaveJSProperty,
toHaveRole,
toHaveText,
toHaveTitle,
toHaveURL,

View File

@ -21,7 +21,7 @@ import { expectTypes, callLogText } from '../util';
import { toBeTruthy } from './toBeTruthy';
import { toEqual } from './toEqual';
import { toExpectedTextValues, toMatchText } from './toMatchText';
import { constructURLBasedOnBaseURL, isRegExp, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils';
import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils';
import { currentTestInfo } from '../common/globals';
import { TestInfoImpl } from '../worker/testInfo';
import type { ExpectMatcherContext } from './expect';
@ -290,6 +290,20 @@ export function toHaveJSProperty(
}, expected, options);
}
export function toHaveRole(
this: ExpectMatcherContext,
locator: LocatorEx,
expected: string,
options?: { timeout?: number, ignoreCase?: boolean },
) {
if (!isString(expected))
throw new Error(`"role" argument in toHaveRole must be a string`);
return toMatchText.call(this, 'toHaveRole', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]);
return await locator._expect('to.have.role', { expectedText, isNot, timeout });
}, expected, options);
}
export function toHaveText(
this: ExpectMatcherContext,
locator: LocatorEx,

View File

@ -7141,6 +7141,30 @@ interface LocatorAssertions {
timeout?: number;
}): Promise<void>;
/**
* Ensures the {@link Locator} points to an element with a given
* [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles).
*
* Note that role is matched as a string, disregarding the ARIA role hierarchy. For example, asserting a superclass
* role `"checkbox"` on an element with a subclass role `"switch"` will fail.
*
* **Usage**
*
* ```js
* const locator = page.getByTestId('save-button');
* await expect(locator).toHaveRole('button');
* ```
*
* @param role Required aria role.
* @param options
*/
toHaveRole(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?: {
/**
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/
timeout?: number;
}): Promise<void>;
/**
* This function will wait until two consecutive locator screenshots yield the same result, and then compare the last
* screenshot with the expectation.

View File

@ -443,3 +443,16 @@ test('toHaveAccessibleDescription', async ({ page }) => {
await expect(page.locator('div')).not.toHaveAccessibleDescription(/hello/);
await expect(page.locator('div')).toHaveAccessibleDescription(/hello/, { ignoreCase: true });
});
test('toHaveRole', async ({ page }) => {
await page.setContent(`<div role="button">Button!</div>`);
await expect(page.locator('div')).toHaveRole('button');
await expect(page.locator('div')).not.toHaveRole('checkbox');
try {
// @ts-expect-error
await expect(page.locator('div')).toHaveRole(/button|checkbox/);
expect(1, 'Must throw when given a regular expression').toBe(2);
} catch (error) {
expect(error.message).toBe(`"role" argument in toHaveRole must be a string`);
}
});