fix(selector generator): use the same label definition as getByLabel (#23846)
This extracts `getElementLabels` helper function to be used both for generating and querying.
This commit is contained in:
Родитель
bdac3e28a6
Коммит
5821c547aa
|
@ -22,14 +22,14 @@ import { createRoleEngine } from './roleSelectorEngine';
|
|||
import { parseAttributeSelector } from '../../utils/isomorphic/selectorParser';
|
||||
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../../utils/isomorphic/selectorParser';
|
||||
import { visitAllSelectorParts, parseSelector, stringifySelector } from '../../utils/isomorphic/selectorParser';
|
||||
import { type TextMatcher, elementMatchesText, elementText, type ElementText } from './selectorUtils';
|
||||
import { type TextMatcher, elementMatchesText, elementText, type ElementText, getElementLabels } from './selectorUtils';
|
||||
import { SelectorEvaluatorImpl, sortInDOMOrder } from './selectorEvaluator';
|
||||
import { enclosingShadowRootOrDocument, isElementVisible, parentElementOrShadowHost } from './domUtils';
|
||||
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, getAriaLabelledByElements, getAriaRole, getElementAccessibleName } from './roleUtils';
|
||||
import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName } from './roleUtils';
|
||||
import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
||||
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
|
||||
import type { Language } from '../../utils/isomorphic/locatorGenerators';
|
||||
|
@ -325,15 +325,7 @@ export class InjectedScript {
|
|||
const { matcher } = createTextMatcher(selector, true);
|
||||
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) {
|
||||
const ariaLabel = element.getAttribute('aria-label');
|
||||
if (ariaLabel !== null && !!ariaLabel.trim())
|
||||
return matcher({ full: ariaLabel, immediate: [ariaLabel] });
|
||||
}
|
||||
if (labels === null)
|
||||
labels = (element as HTMLInputElement).labels;
|
||||
return !!labels && [...labels].some(label => matcher(elementText(this._evaluator._cacheText, label)));
|
||||
return getElementLabels(this._evaluator._cacheText, element).some(label => matcher(label));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, normalize
|
|||
import { closestCrossShadow, isInsideScope, parentElementOrShadowHost } from './domUtils';
|
||||
import type { InjectedScript } from './injectedScript';
|
||||
import { getAriaRole, getElementAccessibleName, beginAriaCaches, endAriaCaches } from './roleUtils';
|
||||
import { elementText } from './selectorUtils';
|
||||
import { elementText, getElementLabels } from './selectorUtils';
|
||||
|
||||
type SelectorToken = {
|
||||
engine: string;
|
||||
|
@ -214,12 +214,13 @@ function buildNoTextCandidates(injectedScript: InjectedScript, element: Element,
|
|||
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, false)}]`, score: kPlaceholderScore });
|
||||
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, true)}]`, score: kPlaceholderScoreExact });
|
||||
}
|
||||
const label = input.labels?.[0];
|
||||
if (label) {
|
||||
const labelText = elementText(injectedScript._evaluator._cacheText, label).full.trim();
|
||||
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, false), score: kLabelScore });
|
||||
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, true), score: kLabelScoreExact });
|
||||
}
|
||||
}
|
||||
|
||||
const labels = getElementLabels(injectedScript._evaluator._cacheText, element);
|
||||
for (const label of labels) {
|
||||
const labelText = label.full.trim();
|
||||
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, false), score: kLabelScore });
|
||||
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, true), score: kLabelScoreExact });
|
||||
}
|
||||
|
||||
const ariaRole = getAriaRole(element);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { AttributeSelectorPart } from '../../utils/isomorphic/selectorParser';
|
||||
import { getAriaLabelledByElements } from './roleUtils';
|
||||
|
||||
export function matchesComponentAttribute(obj: any, attr: AttributeSelectorPart) {
|
||||
for (const token of attr.jsonPath) {
|
||||
|
@ -103,3 +104,21 @@ export function elementMatchesText(cache: Map<Element | ShadowRoot, ElementText>
|
|||
return 'selfAndChildren';
|
||||
return 'self';
|
||||
}
|
||||
|
||||
export function getElementLabels(textCache: Map<Element | ShadowRoot, ElementText>, element: Element): ElementText[] {
|
||||
const labels = getAriaLabelledByElements(element);
|
||||
if (labels)
|
||||
return labels.map(label => elementText(textCache, label));
|
||||
const ariaLabel = element.getAttribute('aria-label');
|
||||
if (ariaLabel !== null && !!ariaLabel.trim())
|
||||
return [{ full: ariaLabel, immediate: [ariaLabel] }];
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/forms.html#category-label
|
||||
const isNonHiddenInput = element.nodeName === 'INPUT' && (element as HTMLInputElement).type !== 'hidden';
|
||||
if (['BUTTON', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA'].includes(element.nodeName) || isNonHiddenInput) {
|
||||
const labels = (element as HTMLInputElement).labels;
|
||||
if (labels)
|
||||
return [...labels].map(label => elementText(textCache, label));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -355,7 +355,7 @@ it.describe('selector generator', () => {
|
|||
expect(await generate(page, 'ng\\:switch')).toBe('ng\\:switch');
|
||||
|
||||
await page.setContent(`<button><span></span></button><button></button>`);
|
||||
await page.$eval('button', button => button.setAttribute('aria-label', `!#'!?:`));
|
||||
await page.$eval('span', span => span.textContent = `!#'!?:`);
|
||||
expect(await generate(page, 'button')).toBe(`internal:role=button[name="!#'!?:"i]`);
|
||||
expect(await page.$(`role=button[name="!#'!?:"]`)).toBeTruthy();
|
||||
|
||||
|
@ -380,7 +380,7 @@ it.describe('selector generator', () => {
|
|||
|
||||
it('should accept valid aria-label for candidate consideration', async ({ page }) => {
|
||||
await page.setContent(`<button aria-label="ariaLabel" id="buttonId"></button>`);
|
||||
expect(await generate(page, 'button')).toBe('internal:role=button[name=\"ariaLabel\"i]');
|
||||
expect(await generate(page, 'button')).toBe('internal:label="ariaLabel"i');
|
||||
});
|
||||
|
||||
it('should ignore empty role for candidate consideration', async ({ page }) => {
|
||||
|
@ -404,8 +404,20 @@ it.describe('selector generator', () => {
|
|||
});
|
||||
|
||||
it('should generate label selector', async ({ page }) => {
|
||||
await page.setContent(`<label for=target>Country</label><input id=target>`);
|
||||
expect(await generate(page, 'input')).toBe('internal:label="Country"i');
|
||||
await page.setContent(`
|
||||
<label for=target1>Target1</label><input id=target1>
|
||||
<label for=target2>Target2</label><button id=target2>??</button>
|
||||
<label for=target3>Target3</label><select id=target3><option>hey</option></select>
|
||||
<label for=target4>Target4</label><progress id=target4 value=70 max=100>70%</progress>
|
||||
<label for=target5>Target5</label><input id=target5 type=hidden>
|
||||
<label for=target6>Target6</label><div id=target6>text</div>
|
||||
`);
|
||||
expect(await generate(page, '#target1')).toBe('internal:label="Target1"i');
|
||||
expect(await generate(page, '#target2')).toBe('internal:label="Target2"i');
|
||||
expect(await generate(page, '#target3')).toBe('internal:label="Target3"i');
|
||||
expect(await generate(page, '#target4')).toBe('internal:label="Target4"i');
|
||||
expect(await generate(page, '#target5')).toBe('#target5');
|
||||
expect(await generate(page, '#target6')).toBe('internal:text="text"i');
|
||||
|
||||
await page.setContent(`<label for=target>Coun"try</label><input id=target>`);
|
||||
expect(await generate(page, 'input')).toBe('internal:label="Coun\\\"try"i');
|
||||
|
|
Загрузка…
Ссылка в новой задаче