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 { generateSelector } 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 { getAriaCheckedStrict, getAriaDisabled, getAriaRole, getElementAccessibleName } from './roleUtils';
|
import { getAriaCheckedStrict, getAriaDisabled, getAriaLabelledByElements, getAriaRole, getElementAccessibleName } from './roleUtils';
|
||||||
import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
||||||
import { asLocator } from '../isomorphic/locatorGenerators';
|
import { asLocator } from '../isomorphic/locatorGenerators';
|
||||||
import type { Language } from '../isomorphic/locatorGenerators';
|
import type { Language } from '../isomorphic/locatorGenerators';
|
||||||
@ -296,14 +296,13 @@ export class InjectedScript {
|
|||||||
return {
|
return {
|
||||||
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
||||||
const { matcher } = createTextMatcher(selector, true);
|
const { matcher } = createTextMatcher(selector, true);
|
||||||
const result: Element[] = [];
|
const allElements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, '*');
|
||||||
const labels = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, 'label') as HTMLLabelElement[];
|
return allElements.filter(element => {
|
||||||
for (const label of labels) {
|
let labels: Element[] | NodeListOf<Element> | null | undefined = getAriaLabelledByElements(element);
|
||||||
const control = label.control;
|
if (labels === null)
|
||||||
if (control && matcher(elementText(this._evaluator._cacheText, label)))
|
labels = (element as HTMLInputElement).labels;
|
||||||
result.push(control);
|
return !!labels && [...labels].some(label => matcher(elementText(this._evaluator._cacheText, label)));
|
||||||
}
|
});
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -309,6 +309,13 @@ function getPseudoContent(pseudoStyle: CSSStyleDeclaration | undefined) {
|
|||||||
return '';
|
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 {
|
export function getElementAccessibleName(element: Element, includeHidden: boolean, hiddenCache: Map<Element, boolean>): string {
|
||||||
// https://w3c.github.io/accname/#computation-steps
|
// https://w3c.github.io/accname/#computation-steps
|
||||||
|
|
||||||
@ -360,7 +367,7 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
|
|||||||
|
|
||||||
// step 2b.
|
// step 2b.
|
||||||
if (options.embeddedInLabelledBy === 'none') {
|
if (options.embeddedInLabelledBy === 'none') {
|
||||||
const refs = getIdRefs(element, element.getAttribute('aria-labelledby'));
|
const refs = getAriaLabelledByElements(element) || [];
|
||||||
const accessibleName = refs.map(ref => getElementAccessibleNameInternal(ref, {
|
const accessibleName = refs.map(ref => getElementAccessibleNameInternal(ref, {
|
||||||
...options,
|
...options,
|
||||||
embeddedInLabelledBy: 'self',
|
embeddedInLabelledBy: 'self',
|
||||||
|
@ -80,6 +80,38 @@ it('getByLabel should work with nested elements', async ({ page }) => {
|
|||||||
expect(await page.getByLabel(/last name/).elementHandles()).toEqual([]);
|
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 }) => {
|
it('getByPlaceholder should work', async ({ page }) => {
|
||||||
await page.setContent(`<div>
|
await page.setContent(`<div>
|
||||||
<input placeholder='Hello'>
|
<input placeholder='Hello'>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user