feat(getByLabel): support aria-labelledby (#19456)

Testing library also treats them equally.

Fixes #19284.
This commit is contained in:
Dmitry Gozman 2022-12-14 13:51:05 -08:00 committed by GitHub
parent e7b8554342
commit a27f1f744f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 48 additions and 10 deletions

View File

@ -29,7 +29,7 @@ import type { CSSComplexSelectorList } from '../isomorphic/cssParser';
import { generateSelector } from './selectorGenerator';
import type * as channels from '@protocol/channels';
import { Highlight } from './highlight';
import { getAriaCheckedStrict, getAriaDisabled, getAriaRole, getElementAccessibleName } from './roleUtils';
import { getAriaCheckedStrict, getAriaDisabled, getAriaLabelledByElements, getAriaRole, getElementAccessibleName } from './roleUtils';
import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
import { asLocator } from '../isomorphic/locatorGenerators';
import type { Language } from '../isomorphic/locatorGenerators';
@ -296,14 +296,13 @@ export class InjectedScript {
return {
queryAll: (root: SelectorRoot, selector: string): Element[] => {
const { matcher } = createTextMatcher(selector, true);
const result: Element[] = [];
const labels = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, 'label') as HTMLLabelElement[];
for (const label of labels) {
const control = label.control;
if (control && matcher(elementText(this._evaluator._cacheText, label)))
result.push(control);
}
return result;
const allElements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, '*');
return allElements.filter(element => {
let labels: Element[] | NodeListOf<Element> | null | undefined = getAriaLabelledByElements(element);
if (labels === null)
labels = (element as HTMLInputElement).labels;
return !!labels && [...labels].some(label => matcher(elementText(this._evaluator._cacheText, label)));
});
}
};
}

View File

@ -309,6 +309,13 @@ function getPseudoContent(pseudoStyle: CSSStyleDeclaration | undefined) {
return '';
}
export function getAriaLabelledByElements(element: Element): Element[] | null {
const ref = element.getAttribute('aria-labelledby');
if (ref === null)
return null;
return getIdRefs(element, ref);
}
export function getElementAccessibleName(element: Element, includeHidden: boolean, hiddenCache: Map<Element, boolean>): string {
// https://w3c.github.io/accname/#computation-steps
@ -360,7 +367,7 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
// step 2b.
if (options.embeddedInLabelledBy === 'none') {
const refs = getIdRefs(element, element.getAttribute('aria-labelledby'));
const refs = getAriaLabelledByElements(element) || [];
const accessibleName = refs.map(ref => getElementAccessibleNameInternal(ref, {
...options,
embeddedInLabelledBy: 'self',

View File

@ -80,6 +80,38 @@ it('getByLabel should work with nested elements', async ({ page }) => {
expect(await page.getByLabel(/last name/).elementHandles()).toEqual([]);
});
it('getByLabel should work with multiply-labelled input', async ({ page }) => {
await page.setContent(`<label for=target>Name</label><input id=target type=text><label for=target>First or Last</label>`);
expect(await page.getByLabel('Name').evaluate(e => e.id)).toBe('target');
expect(await page.getByLabel('First or Last').evaluate(e => e.id)).toBe('target');
});
it('getByLabel should work with ancestor label and multiple controls', async ({ page }) => {
await page.setContent(`<label>Name<button id=target>Click me</button><input type=text></label>`);
expect(await page.getByLabel('Name').evaluate(e => e.id)).toBe('target');
});
it('getByLabel should work with ancestor label and for', async ({ page }) => {
await page.setContent(`
<label for=target>Name<input type=text id=nontarget></label>
<input type=text id=target>
`);
expect(await page.getByLabel('Name').evaluate(e => e.id)).toBe('target');
});
it('getByLabel should work with aria-labelledby', async ({ page }) => {
await page.setContent(`<label id=name-label>Name</label><button aria-labelledby=name-label>Click me</button>`);
expect(await page.getByLabel('Name').evaluate(e => e.textContent)).toBe('Click me');
});
it('getByLabel should prioritize aria-labelledby over native label', async ({ page }) => {
await page.setContent(`
<label id=name-label>Name</label>
<label>Wrong<button aria-labelledby=name-label>Click me</button></label>
`);
expect(await page.getByLabel('Name').evaluate(e => e.textContent)).toBe('Click me');
});
it('getByPlaceholder should work', async ({ page }) => {
await page.setContent(`<div>
<input placeholder='Hello'>