feat: expect(locator).toHaveAccessibleErrorMessage (#33904)
This commit is contained in:
parent
3ec8ee7a9b
commit
7f141b2c42
|
|
@ -1217,6 +1217,56 @@ Expected accessible description.
|
||||||
* since: v1.44
|
* since: v1.44
|
||||||
|
|
||||||
|
|
||||||
|
## async method: LocatorAssertions.toHaveAccessibleErrorMessage
|
||||||
|
* since: v1.50
|
||||||
|
* langs:
|
||||||
|
- alias-java: hasAccessibleErrorMessage
|
||||||
|
|
||||||
|
Ensures the [Locator] points to an element with a given [aria errormessage](https://w3c.github.io/aria/#aria-errormessage).
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const locator = page.getByTestId('username-input');
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage('Username is required.');
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
Locator locator = page.getByTestId("username-input");
|
||||||
|
assertThat(locator).hasAccessibleErrorMessage("Username is required.");
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
locator = page.get_by_test_id("username-input")
|
||||||
|
await expect(locator).to_have_accessible_error_message("Username is required.")
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
locator = page.get_by_test_id("username-input")
|
||||||
|
expect(locator).to_have_accessible_error_message("Username is required.")
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var locator = Page.GetByTestId("username-input");
|
||||||
|
await Expect(locator).ToHaveAccessibleErrorMessageAsync("Username is required.");
|
||||||
|
```
|
||||||
|
|
||||||
|
### param: LocatorAssertions.toHaveAccessibleErrorMessage.errorMessage
|
||||||
|
* since: v1.50
|
||||||
|
- `errorMessage` <[string]|[RegExp]>
|
||||||
|
|
||||||
|
Expected accessible error message.
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveAccessibleErrorMessage.timeout = %%-js-assertions-timeout-%%
|
||||||
|
* since: v1.50
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveAccessibleErrorMessage.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||||
|
* since: v1.50
|
||||||
|
|
||||||
|
### option: LocatorAssertions.toHaveAccessibleErrorMessage.ignoreCase = %%-assertions-ignore-case-%%
|
||||||
|
* since: v1.50
|
||||||
|
|
||||||
|
|
||||||
## async method: LocatorAssertions.toHaveAccessibleName
|
## async method: LocatorAssertions.toHaveAccessibleName
|
||||||
* since: v1.44
|
* since: v1.44
|
||||||
* langs:
|
* langs:
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser';
|
||||||
import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator';
|
import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { Highlight } from './highlight';
|
import { Highlight } from './highlight';
|
||||||
import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription, getReadonly } from './roleUtils';
|
import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription, getReadonly, getElementAccessibleErrorMessage } from './roleUtils';
|
||||||
import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
||||||
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
|
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
|
||||||
import type { Language } from '../../utils/isomorphic/locatorGenerators';
|
import type { Language } from '../../utils/isomorphic/locatorGenerators';
|
||||||
|
|
@ -1321,6 +1321,8 @@ export class InjectedScript {
|
||||||
received = getElementAccessibleName(element, false /* includeHidden */);
|
received = getElementAccessibleName(element, false /* includeHidden */);
|
||||||
} else if (expression === 'to.have.accessible.description') {
|
} else if (expression === 'to.have.accessible.description') {
|
||||||
received = getElementAccessibleDescription(element, false /* includeHidden */);
|
received = getElementAccessibleDescription(element, false /* includeHidden */);
|
||||||
|
} else if (expression === 'to.have.accessible.error.message') {
|
||||||
|
received = getElementAccessibleErrorMessage(element);
|
||||||
} else if (expression === 'to.have.role') {
|
} else if (expression === 'to.have.role') {
|
||||||
received = getAriaRole(element) || '';
|
received = getAriaRole(element) || '';
|
||||||
} else if (expression === 'to.have.title') {
|
} else if (expression === 'to.have.title') {
|
||||||
|
|
|
||||||
|
|
@ -461,6 +461,59 @@ export function getElementAccessibleDescription(element: Element, includeHidden:
|
||||||
return accessibleDescription;
|
return accessibleDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/wai-aria-1.2/#aria-invalid
|
||||||
|
const kAriaInvalidRoles = ['application', 'checkbox', 'combobox', 'gridcell', 'listbox', 'radiogroup', 'slider', 'spinbutton', 'textbox', 'tree', 'columnheader', 'rowheader', 'searchbox', 'switch', 'treegrid'];
|
||||||
|
|
||||||
|
function getAriaInvalid(element: Element): 'false' | 'true' | 'grammar' | 'spelling' {
|
||||||
|
const role = getAriaRole(element) || '';
|
||||||
|
if (!role || !kAriaInvalidRoles.includes(role))
|
||||||
|
return 'false';
|
||||||
|
const ariaInvalid = element.getAttribute('aria-invalid');
|
||||||
|
if (!ariaInvalid || ariaInvalid.trim() === '' || ariaInvalid.toLocaleLowerCase() === 'false')
|
||||||
|
return 'false';
|
||||||
|
if (ariaInvalid === 'true' || ariaInvalid === 'grammar' || ariaInvalid === 'spelling')
|
||||||
|
return ariaInvalid;
|
||||||
|
return 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidityInvalid(element: Element) {
|
||||||
|
if ('validity' in element){
|
||||||
|
const validity = element.validity as ValidityState | undefined;
|
||||||
|
return validity?.valid === false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getElementAccessibleErrorMessage(element: Element): string {
|
||||||
|
// SPEC: https://w3c.github.io/aria/#aria-errormessage
|
||||||
|
//
|
||||||
|
// TODO: support https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/validationMessage
|
||||||
|
const cache = cacheAccessibleErrorMessage;
|
||||||
|
let accessibleErrorMessage = cacheAccessibleErrorMessage?.get(element);
|
||||||
|
|
||||||
|
if (accessibleErrorMessage === undefined) {
|
||||||
|
accessibleErrorMessage = '';
|
||||||
|
|
||||||
|
const isAriaInvalid = getAriaInvalid(element) !== 'false';
|
||||||
|
const isValidityInvalid = getValidityInvalid(element);
|
||||||
|
if (isAriaInvalid || isValidityInvalid) {
|
||||||
|
const errorMessageId = element.getAttribute('aria-errormessage');
|
||||||
|
const errorMessages = getIdRefs(element, errorMessageId);
|
||||||
|
// Ideally, this should be a separate "embeddedInErrorMessage", but it would follow the exact same rules.
|
||||||
|
// Relevant vague spec: https://w3c.github.io/core-aam/#ariaErrorMessage.
|
||||||
|
const parts = errorMessages.map(errorMessage => asFlatString(
|
||||||
|
getTextAlternativeInternal(errorMessage, {
|
||||||
|
visitedElements: new Set(),
|
||||||
|
embeddedInDescribedBy: { element: errorMessage, hidden: isElementHiddenForAria(errorMessage) },
|
||||||
|
})
|
||||||
|
));
|
||||||
|
accessibleErrorMessage = parts.join(' ').trim();
|
||||||
|
}
|
||||||
|
cache?.set(element, accessibleErrorMessage);
|
||||||
|
}
|
||||||
|
return accessibleErrorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
type AccessibleNameOptions = {
|
type AccessibleNameOptions = {
|
||||||
visitedElements: Set<Element>,
|
visitedElements: Set<Element>,
|
||||||
includeHidden?: boolean,
|
includeHidden?: boolean,
|
||||||
|
|
@ -972,6 +1025,7 @@ let cacheAccessibleName: Map<Element, string> | undefined;
|
||||||
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
|
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
|
||||||
let cacheAccessibleDescription: Map<Element, string> | undefined;
|
let cacheAccessibleDescription: Map<Element, string> | undefined;
|
||||||
let cacheAccessibleDescriptionHidden: Map<Element, string> | undefined;
|
let cacheAccessibleDescriptionHidden: Map<Element, string> | undefined;
|
||||||
|
let cacheAccessibleErrorMessage: Map<Element, string> | undefined;
|
||||||
let cacheIsHidden: Map<Element, boolean> | undefined;
|
let cacheIsHidden: Map<Element, boolean> | undefined;
|
||||||
let cachePseudoContentBefore: Map<Element, string> | undefined;
|
let cachePseudoContentBefore: Map<Element, string> | undefined;
|
||||||
let cachePseudoContentAfter: Map<Element, string> | undefined;
|
let cachePseudoContentAfter: Map<Element, string> | undefined;
|
||||||
|
|
@ -983,6 +1037,7 @@ export function beginAriaCaches() {
|
||||||
cacheAccessibleNameHidden ??= new Map();
|
cacheAccessibleNameHidden ??= new Map();
|
||||||
cacheAccessibleDescription ??= new Map();
|
cacheAccessibleDescription ??= new Map();
|
||||||
cacheAccessibleDescriptionHidden ??= new Map();
|
cacheAccessibleDescriptionHidden ??= new Map();
|
||||||
|
cacheAccessibleErrorMessage ??= new Map();
|
||||||
cacheIsHidden ??= new Map();
|
cacheIsHidden ??= new Map();
|
||||||
cachePseudoContentBefore ??= new Map();
|
cachePseudoContentBefore ??= new Map();
|
||||||
cachePseudoContentAfter ??= new Map();
|
cachePseudoContentAfter ??= new Map();
|
||||||
|
|
@ -994,6 +1049,7 @@ export function endAriaCaches() {
|
||||||
cacheAccessibleNameHidden = undefined;
|
cacheAccessibleNameHidden = undefined;
|
||||||
cacheAccessibleDescription = undefined;
|
cacheAccessibleDescription = undefined;
|
||||||
cacheAccessibleDescriptionHidden = undefined;
|
cacheAccessibleDescriptionHidden = undefined;
|
||||||
|
cacheAccessibleErrorMessage = undefined;
|
||||||
cacheIsHidden = undefined;
|
cacheIsHidden = undefined;
|
||||||
cachePseudoContentBefore = undefined;
|
cachePseudoContentBefore = undefined;
|
||||||
cachePseudoContentAfter = undefined;
|
cachePseudoContentAfter = undefined;
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
toContainText,
|
toContainText,
|
||||||
toHaveAccessibleDescription,
|
toHaveAccessibleDescription,
|
||||||
toHaveAccessibleName,
|
toHaveAccessibleName,
|
||||||
|
toHaveAccessibleErrorMessage,
|
||||||
toHaveAttribute,
|
toHaveAttribute,
|
||||||
toHaveClass,
|
toHaveClass,
|
||||||
toHaveCount,
|
toHaveCount,
|
||||||
|
|
@ -224,6 +225,7 @@ const customAsyncMatchers = {
|
||||||
toContainText,
|
toContainText,
|
||||||
toHaveAccessibleDescription,
|
toHaveAccessibleDescription,
|
||||||
toHaveAccessibleName,
|
toHaveAccessibleName,
|
||||||
|
toHaveAccessibleErrorMessage,
|
||||||
toHaveAttribute,
|
toHaveAttribute,
|
||||||
toHaveClass,
|
toHaveClass,
|
||||||
toHaveCount,
|
toHaveCount,
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,18 @@ export function toHaveAccessibleName(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toHaveAccessibleErrorMessage(
|
||||||
|
this: ExpectMatcherState,
|
||||||
|
locator: LocatorEx,
|
||||||
|
expected: string | RegExp,
|
||||||
|
options?: { timeout?: number; ignoreCase?: boolean },
|
||||||
|
) {
|
||||||
|
return toMatchText.call(this, 'toHaveAccessibleErrorMessage', locator, 'Locator', async (isNot, timeout) => {
|
||||||
|
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
|
||||||
|
return await locator._expect('to.have.accessible.error.message', { expectedText: expectedText, isNot, timeout });
|
||||||
|
}, expected, options);
|
||||||
|
}
|
||||||
|
|
||||||
export function toHaveAttribute(
|
export function toHaveAttribute(
|
||||||
this: ExpectMatcherState,
|
this: ExpectMatcherState,
|
||||||
locator: LocatorEx,
|
locator: LocatorEx,
|
||||||
|
|
|
||||||
28
packages/playwright/types/test.d.ts
vendored
28
packages/playwright/types/test.d.ts
vendored
|
|
@ -8112,6 +8112,34 @@ interface LocatorAssertions {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with a given
|
||||||
|
* [aria errormessage](https://w3c.github.io/aria/#aria-errormessage).
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* const locator = page.getByTestId('username-input');
|
||||||
|
* await expect(locator).toHaveAccessibleErrorMessage('Username is required.');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param errorMessage Expected accessible error message.
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
toHaveAccessibleErrorMessage(errorMessage: string|RegExp, options?: {
|
||||||
|
/**
|
||||||
|
* Whether to perform case-insensitive match.
|
||||||
|
* [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-error-message-option-ignore-case)
|
||||||
|
* option takes precedence over the corresponding regular expression flag if specified.
|
||||||
|
*/
|
||||||
|
ignoreCase?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
}): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with a given
|
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with a given
|
||||||
* [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
* [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,136 @@ test('toHaveAccessibleDescription', async ({ page }) => {
|
||||||
await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz');
|
await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('toHaveAccessibleErrorMessage', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<form>
|
||||||
|
<input role="textbox" aria-invalid="true" aria-errormessage="error-message" />
|
||||||
|
<div id="error-message">Hello</div>
|
||||||
|
<div id="irrelevant-error">This should not be considered.</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const locator = page.locator('input[role="textbox"]');
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage('Hello');
|
||||||
|
await expect(locator).not.toHaveAccessibleErrorMessage('hello');
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage('hello', { ignoreCase: true });
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage(/ell\w/);
|
||||||
|
await expect(locator).not.toHaveAccessibleErrorMessage(/hello/);
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage(/hello/, { ignoreCase: true });
|
||||||
|
await expect(locator).not.toHaveAccessibleErrorMessage('This should not be considered.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toHaveAccessibleErrorMessage should handle multiple aria-errormessage references', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<form>
|
||||||
|
<input role="textbox" aria-invalid="true" aria-errormessage="error1 error2" />
|
||||||
|
<div id="error1">First error message.</div>
|
||||||
|
<div id="error2">Second error message.</div>
|
||||||
|
<div id="irrelevant-error">This should not be considered.</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const locator = page.locator('input[role="textbox"]');
|
||||||
|
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage('First error message. Second error message.');
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage(/first error message./i);
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage(/second error message./i);
|
||||||
|
await expect(locator).not.toHaveAccessibleErrorMessage(/This should not be considered./i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('toHaveAccessibleErrorMessage should handle aria-invalid attribute', () => {
|
||||||
|
const errorMessageText = 'Error message';
|
||||||
|
|
||||||
|
async function setupPage(page, ariaInvalidValue: string | null) {
|
||||||
|
const ariaInvalidAttr = ariaInvalidValue === null ? '' : `aria-invalid="${ariaInvalidValue}"`;
|
||||||
|
await page.setContent(`
|
||||||
|
<form>
|
||||||
|
<input id="node" role="textbox" ${ariaInvalidAttr} aria-errormessage="error-msg" />
|
||||||
|
<div id="error-msg">${errorMessageText}</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
return page.locator('#node');
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('evaluated in false', () => {
|
||||||
|
test('no aria-invalid attribute', async ({ page }) => {
|
||||||
|
const locator = await setupPage(page, null);
|
||||||
|
await expect(locator).not.toHaveAccessibleErrorMessage(errorMessageText);
|
||||||
|
});
|
||||||
|
test('aria-invalid="false"', async ({ page }) => {
|
||||||
|
const locator = await setupPage(page, 'false');
|
||||||
|
await expect(locator).not.toHaveAccessibleErrorMessage(errorMessageText);
|
||||||
|
});
|
||||||
|
test('aria-invalid="" (empty string)', async ({ page }) => {
|
||||||
|
const locator = await setupPage(page, '');
|
||||||
|
await expect(locator).not.toHaveAccessibleErrorMessage(errorMessageText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test.describe('evaluated in true', () => {
|
||||||
|
test('aria-invalid="true"', async ({ page }) => {
|
||||||
|
const locator = await setupPage(page, 'true');
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage(errorMessageText);
|
||||||
|
});
|
||||||
|
test('aria-invalid="foo" (unrecognized value)', async ({ page }) => {
|
||||||
|
const locator = await setupPage(page, 'foo');
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage(errorMessageText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('toHaveAccessibleErrorMessage should handle validity state with aria-invalid', () => {
|
||||||
|
const errorMessageText = 'Error message';
|
||||||
|
|
||||||
|
test('should show error message when validity is false and aria-invalid is true', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<form>
|
||||||
|
<input id="node" role="textbox" type="number" min="1" max="100" aria-invalid="true" aria-errormessage="error-msg" />
|
||||||
|
<div id="error-msg">${errorMessageText}</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
const locator = page.locator('#node');
|
||||||
|
await locator.fill('101');
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage(errorMessageText);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error message when validity is true and aria-invalid is true', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<form>
|
||||||
|
<input id="node" role="textbox" type="number" min="1" max="100" aria-invalid="true" aria-errormessage="error-msg" />
|
||||||
|
<div id="error-msg">${errorMessageText}</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
const locator = page.locator('#node');
|
||||||
|
await locator.fill('99');
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage(errorMessageText);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error message when validity is false and aria-invalid is false', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<form>
|
||||||
|
<input id="node" role="textbox" type="number" min="1" max="100" aria-invalid="false" aria-errormessage="error-msg" />
|
||||||
|
<div id="error-msg">${errorMessageText}</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
const locator = page.locator('#node');
|
||||||
|
await locator.fill('101');
|
||||||
|
await expect(locator).toHaveAccessibleErrorMessage(errorMessageText);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not show error message when validity is true and aria-invalid is false', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<form>
|
||||||
|
<input id="node" role="textbox" type="number" min="1" max="100" aria-invalid="false" aria-errormessage="error-msg" />
|
||||||
|
<div id="error-msg">${errorMessageText}</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
const locator = page.locator('#node');
|
||||||
|
await locator.fill('99');
|
||||||
|
await expect(locator).not.toHaveAccessibleErrorMessage(errorMessageText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test('toHaveRole', async ({ page }) => {
|
test('toHaveRole', async ({ page }) => {
|
||||||
await page.setContent(`<div role="button">Button!</div>`);
|
await page.setContent(`<div role="button">Button!</div>`);
|
||||||
await expect(page.locator('div')).toHaveRole('button');
|
await expect(page.locator('div')).toHaveRole('button');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue