feat: add toHaveAccessibleErrorMessage

This commit is contained in:
Pengoose 2024-12-07 21:39:11 +09:00
parent 37e155a8a1
commit b3e3545383
4 changed files with 51 additions and 1 deletions

View file

@ -29,7 +29,7 @@ import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser';
import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator';
import type * as channels from '@protocol/channels';
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 { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators';
@ -1321,6 +1321,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.accessible.error.message') {
received = getElementAccessibleErrorMessage(element, false /* includeHidden */);
} else if (expression === 'to.have.role') {
received = getAriaRole(element) || '';
} else if (expression === 'to.have.title') {

View file

@ -461,6 +461,38 @@ export function getElementAccessibleDescription(element: Element, includeHidden:
return accessibleDescription;
}
export function getElementAccessibleErrorMessage(element: Element, includeHidden: boolean): string {
const cache = includeHidden ? cacheAccessibleErrorMessageHidden : cacheAccessibleErrorMessage;
let accessibleErrorMessage = cache?.get(element);
if (accessibleErrorMessage === undefined) {
accessibleErrorMessage = '';
const ariaInvalid = element.getAttribute('aria-invalid');
if (ariaInvalid === 'true') {
const errorMessageId = element.getAttribute('aria-errormessage');
if (errorMessageId) {
// Ensure the ID is valid (no whitespace)
if (!/\s+/.test(errorMessageId)) {
// Retrieve the element referenced by aria-errormessage.
const errorElement = element.ownerDocument.getElementById(errorMessageId);
if (errorElement) {
accessibleErrorMessage = asFlatString(
getTextAlternativeInternal(errorElement, {
includeHidden,
visitedElements: new Set(),
embeddedInDescribedBy: { element: errorElement, hidden: isElementHiddenForAria(errorElement) },
})
);
}
}
}
}
cache?.set(element, accessibleErrorMessage);
}
return accessibleErrorMessage;
}
type AccessibleNameOptions = {
visitedElements: Set<Element>,
includeHidden?: boolean,
@ -972,6 +1004,8 @@ let cacheAccessibleName: Map<Element, string> | undefined;
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
let cacheAccessibleDescription: Map<Element, string> | undefined;
let cacheAccessibleDescriptionHidden: Map<Element, string> | undefined;
let cacheAccessibleErrorMessage: Map<Element, string> | undefined;
let cacheAccessibleErrorMessageHidden: Map<Element, string> | undefined;
let cacheIsHidden: Map<Element, boolean> | undefined;
let cachePseudoContentBefore: Map<Element, string> | undefined;
let cachePseudoContentAfter: Map<Element, string> | undefined;

View file

@ -35,6 +35,7 @@ import {
toContainText,
toHaveAccessibleDescription,
toHaveAccessibleName,
toHaveAccessibleErrorMessage,
toHaveAttribute,
toHaveClass,
toHaveCount,
@ -224,6 +225,7 @@ const customAsyncMatchers = {
toContainText,
toHaveAccessibleDescription,
toHaveAccessibleName,
toHaveAccessibleErrorMessage,
toHaveAttribute,
toHaveClass,
toHaveCount,

View file

@ -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 });
return await locator._expect('to.have.accessible.error.message', { expectedText: expectedText, isNot, timeout });
}, expected, options);
}
export function toHaveAttribute(
this: ExpectMatcherState,
locator: LocatorEx,