mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(getByLabel): support aria-labelledby (#19456)
Testing library also treats them equally. Fixes #19284.
This commit is contained in:
parent
e7b8554342
commit
a27f1f744f
@ -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)));
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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'>
|
||||
|
Loading…
x
Reference in New Issue
Block a user