chore: speedup multiple roleUtils calls (#23745)
When generating a selector, we tend to match by role and call various roleUtils methods multiple times. Apply the usual pattern for "nested operations counter" and aggressively cache the results.
This commit is contained in:
parent
11770156eb
commit
de422b5afb
|
|
@ -46,6 +46,7 @@ function enclosingShadowHost(element: Element): Element | undefined {
|
||||||
return parentElementOrShadowHost(element);
|
return parentElementOrShadowHost(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assumption: if scope is provided, element must be inside scope's subtree.
|
||||||
export function closestCrossShadow(element: Element | undefined, css: string, scope?: Document | Element): Element | undefined {
|
export function closestCrossShadow(element: Element | undefined, css: string, scope?: Document | Element): Element | undefined {
|
||||||
while (element) {
|
while (element) {
|
||||||
const closest = element.closest(css);
|
const closest = element.closest(css);
|
||||||
|
|
|
||||||
|
|
@ -1341,8 +1341,7 @@ export class InjectedScript {
|
||||||
}
|
}
|
||||||
|
|
||||||
getElementAccessibleName(element: Element, includeHidden?: boolean): string {
|
getElementAccessibleName(element: Element, includeHidden?: boolean): string {
|
||||||
const hiddenCache = new Map<Element, boolean>();
|
return getElementAccessibleName(element, !!includeHidden);
|
||||||
return getElementAccessibleName(element, !!includeHidden, hiddenCache);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAriaRole(element: Element) {
|
getAriaRole(element: Element) {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import type { SelectorEngine, SelectorRoot } from './selectorEngine';
|
import type { SelectorEngine, SelectorRoot } from './selectorEngine';
|
||||||
import { matchesAttributePart } from './selectorUtils';
|
import { matchesAttributePart } from './selectorUtils';
|
||||||
import { getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
|
import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
|
||||||
import { parseAttributeSelector, type AttributeSelectorPart, type AttributeSelectorOperator } from '../../utils/isomorphic/selectorParser';
|
import { parseAttributeSelector, type AttributeSelectorPart, type AttributeSelectorOperator } from '../../utils/isomorphic/selectorParser';
|
||||||
import { normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
|
import { normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
|
||||||
|
|
||||||
|
|
@ -125,7 +125,6 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string): RoleE
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: boolean): Element[] {
|
function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: boolean): Element[] {
|
||||||
const hiddenCache = new Map<Element, boolean>();
|
|
||||||
const result: Element[] = [];
|
const result: Element[] = [];
|
||||||
const match = (element: Element) => {
|
const match = (element: Element) => {
|
||||||
if (getAriaRole(element) !== options.role)
|
if (getAriaRole(element) !== options.role)
|
||||||
|
|
@ -143,13 +142,13 @@ function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: bo
|
||||||
if (options.disabled !== undefined && getAriaDisabled(element) !== options.disabled)
|
if (options.disabled !== undefined && getAriaDisabled(element) !== options.disabled)
|
||||||
return;
|
return;
|
||||||
if (!options.includeHidden) {
|
if (!options.includeHidden) {
|
||||||
const isHidden = isElementHiddenForAria(element, hiddenCache);
|
const isHidden = isElementHiddenForAria(element);
|
||||||
if (isHidden)
|
if (isHidden)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.name !== undefined) {
|
if (options.name !== undefined) {
|
||||||
// Always normalize whitespace in the accessible name.
|
// Always normalize whitespace in the accessible name.
|
||||||
const accessibleName = normalizeWhiteSpace(getElementAccessibleName(element, !!options.includeHidden, hiddenCache));
|
const accessibleName = normalizeWhiteSpace(getElementAccessibleName(element, !!options.includeHidden));
|
||||||
if (typeof options.name === 'string')
|
if (typeof options.name === 'string')
|
||||||
options.name = normalizeWhiteSpace(options.name);
|
options.name = normalizeWhiteSpace(options.name);
|
||||||
// internal:role assumes that [name="foo"i] also means substring.
|
// internal:role assumes that [name="foo"i] also means substring.
|
||||||
|
|
@ -185,7 +184,12 @@ export function createRoleEngine(internal: boolean): SelectorEngine {
|
||||||
if (!role)
|
if (!role)
|
||||||
throw new Error(`Role must not be empty`);
|
throw new Error(`Role must not be empty`);
|
||||||
const options = validateAttributes(parsed.attributes, role);
|
const options = validateAttributes(parsed.attributes, role);
|
||||||
return queryRole(scope, options, internal);
|
beginAriaCaches();
|
||||||
|
try {
|
||||||
|
return queryRole(scope, options, internal);
|
||||||
|
} finally {
|
||||||
|
endAriaCaches();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ function getAriaBoolean(attr: string | null) {
|
||||||
// Not implemented:
|
// Not implemented:
|
||||||
// `Any descendants of elements that have the characteristic "Children Presentational: True"`
|
// `Any descendants of elements that have the characteristic "Children Presentational: True"`
|
||||||
// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
|
// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
|
||||||
export function isElementHiddenForAria(element: Element, cache: Map<Element, boolean>): boolean {
|
export function isElementHiddenForAria(element: Element): boolean {
|
||||||
if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(element.tagName))
|
if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(element.tagName))
|
||||||
return true;
|
return true;
|
||||||
const style = getElementComputedStyle(element);
|
const style = getElementComputedStyle(element);
|
||||||
|
|
@ -243,7 +243,7 @@ export function isElementHiddenForAria(element: Element, cache: Map<Element, boo
|
||||||
if (style?.display === 'contents' && !isSlot) {
|
if (style?.display === 'contents' && !isSlot) {
|
||||||
// display:contents is not rendered itself, but its child nodes are.
|
// display:contents is not rendered itself, but its child nodes are.
|
||||||
for (let child = element.firstChild; child; child = child.nextSibling) {
|
for (let child = element.firstChild; child; child = child.nextSibling) {
|
||||||
if (child.nodeType === 1 /* Node.ELEMENT_NODE */ && !isElementHiddenForAria(child as Element, cache))
|
if (child.nodeType === 1 /* Node.ELEMENT_NODE */ && !isElementHiddenForAria(child as Element))
|
||||||
return false;
|
return false;
|
||||||
if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child as Text))
|
if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child as Text))
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -255,12 +255,13 @@ export function isElementHiddenForAria(element: Element, cache: Map<Element, boo
|
||||||
const isOptionInsideSelect = element.nodeName === 'OPTION' && !!element.closest('select');
|
const isOptionInsideSelect = element.nodeName === 'OPTION' && !!element.closest('select');
|
||||||
if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element, style))
|
if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element, style))
|
||||||
return true;
|
return true;
|
||||||
return belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element, cache);
|
return belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element: Element, cache: Map<Element, boolean>): boolean {
|
function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element: Element): boolean {
|
||||||
if (!cache.has(element)) {
|
let hidden = cacheIsHidden?.get(element);
|
||||||
let hidden = false;
|
if (hidden === undefined) {
|
||||||
|
hidden = false;
|
||||||
|
|
||||||
// When parent has a shadow root, all light dom children must be assigned to a slot,
|
// When parent has a shadow root, all light dom children must be assigned to a slot,
|
||||||
// otherwise they are not rendered and considered hidden for aria.
|
// otherwise they are not rendered and considered hidden for aria.
|
||||||
|
|
@ -278,11 +279,11 @@ function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element: Element, cache: M
|
||||||
if (!hidden) {
|
if (!hidden) {
|
||||||
const parent = parentElementOrShadowHost(element);
|
const parent = parentElementOrShadowHost(element);
|
||||||
if (parent)
|
if (parent)
|
||||||
hidden = belongsToDisplayNoneOrAriaHiddenOrNonSlotted(parent, cache);
|
hidden = belongsToDisplayNoneOrAriaHiddenOrNonSlotted(parent);
|
||||||
}
|
}
|
||||||
cache.set(element, hidden);
|
cacheIsHidden?.set(element, hidden);
|
||||||
}
|
}
|
||||||
return cache.get(element)!;
|
return hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIdRefs(element: Element, ref: string | null): Element[] {
|
function getIdRefs(element: Element, ref: string | null): Element[] {
|
||||||
|
|
@ -325,14 +326,14 @@ function queryInAriaOwned(element: Element, selector: string): Element[] {
|
||||||
function getPseudoContent(pseudoStyle: CSSStyleDeclaration | undefined) {
|
function getPseudoContent(pseudoStyle: CSSStyleDeclaration | undefined) {
|
||||||
if (!pseudoStyle)
|
if (!pseudoStyle)
|
||||||
return '';
|
return '';
|
||||||
const content = pseudoStyle.getPropertyValue('content');
|
const content = pseudoStyle.content;
|
||||||
if ((content[0] === '\'' && content[content.length - 1] === '\'') ||
|
if ((content[0] === '\'' && content[content.length - 1] === '\'') ||
|
||||||
(content[0] === '"' && content[content.length - 1] === '"')) {
|
(content[0] === '"' && content[content.length - 1] === '"')) {
|
||||||
const unquoted = content.substring(1, content.length - 1);
|
const unquoted = content.substring(1, content.length - 1);
|
||||||
// SPEC DIFFERENCE.
|
// SPEC DIFFERENCE.
|
||||||
// Spec says "CSS textual content, without a space", but we account for display
|
// Spec says "CSS textual content, without a space", but we account for display
|
||||||
// to pass "name_file-label-inline-block-styles-manual.html"
|
// to pass "name_file-label-inline-block-styles-manual.html"
|
||||||
const display = pseudoStyle.getPropertyValue('display') || 'inline';
|
const display = pseudoStyle.display || 'inline';
|
||||||
if (display !== 'inline')
|
if (display !== 'inline')
|
||||||
return ' ' + unquoted + ' ';
|
return ' ' + unquoted + ' ';
|
||||||
return unquoted;
|
return unquoted;
|
||||||
|
|
@ -360,31 +361,37 @@ function allowsNameFromContent(role: string, targetDescendant: boolean) {
|
||||||
return alwaysAllowsNameFromContent || descendantAllowsNameFromContent;
|
return alwaysAllowsNameFromContent || descendantAllowsNameFromContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getElementAccessibleName(element: Element, includeHidden: boolean, hiddenCache: Map<Element, boolean>): string {
|
export function getElementAccessibleName(element: Element, includeHidden: boolean): string {
|
||||||
// https://w3c.github.io/accname/#computation-steps
|
const cache = (includeHidden ? cacheAccessibleNameHidden : cacheAccessibleName);
|
||||||
|
let accessibleName = cache?.get(element);
|
||||||
|
|
||||||
// step 1.
|
if (accessibleName === undefined) {
|
||||||
// https://w3c.github.io/aria/#namefromprohibited
|
// https://w3c.github.io/accname/#computation-steps
|
||||||
const elementProhibitsNaming = ['caption', 'code', 'definition', 'deletion', 'emphasis', 'generic', 'insertion', 'mark', 'paragraph', 'presentation', 'strong', 'subscript', 'suggestion', 'superscript', 'term', 'time'].includes(getAriaRole(element) || '');
|
accessibleName = '';
|
||||||
if (elementProhibitsNaming)
|
|
||||||
return '';
|
|
||||||
|
|
||||||
// step 2.
|
// step 1.
|
||||||
const accessibleName = normalizeAccessbileName(getElementAccessibleNameInternal(element, {
|
// https://w3c.github.io/aria/#namefromprohibited
|
||||||
includeHidden,
|
const elementProhibitsNaming = ['caption', 'code', 'definition', 'deletion', 'emphasis', 'generic', 'insertion', 'mark', 'paragraph', 'presentation', 'strong', 'subscript', 'suggestion', 'superscript', 'term', 'time'].includes(getAriaRole(element) || '');
|
||||||
hiddenCache,
|
|
||||||
visitedElements: new Set(),
|
if (!elementProhibitsNaming) {
|
||||||
embeddedInLabelledBy: 'none',
|
// step 2.
|
||||||
embeddedInLabel: 'none',
|
accessibleName = normalizeAccessbileName(getElementAccessibleNameInternal(element, {
|
||||||
embeddedInTextAlternativeElement: false,
|
includeHidden,
|
||||||
embeddedInTargetElement: 'self',
|
visitedElements: new Set(),
|
||||||
}));
|
embeddedInLabelledBy: 'none',
|
||||||
|
embeddedInLabel: 'none',
|
||||||
|
embeddedInTextAlternativeElement: false,
|
||||||
|
embeddedInTargetElement: 'self',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
cache?.set(element, accessibleName);
|
||||||
|
}
|
||||||
return accessibleName;
|
return accessibleName;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessibleNameOptions = {
|
type AccessibleNameOptions = {
|
||||||
includeHidden: boolean,
|
includeHidden: boolean,
|
||||||
hiddenCache: Map<Element, boolean>,
|
|
||||||
visitedElements: Set<Element>,
|
visitedElements: Set<Element>,
|
||||||
embeddedInLabelledBy: 'none' | 'self' | 'descendant',
|
embeddedInLabelledBy: 'none' | 'self' | 'descendant',
|
||||||
embeddedInLabel: 'none' | 'self' | 'descendant',
|
embeddedInLabel: 'none' | 'self' | 'descendant',
|
||||||
|
|
@ -404,7 +411,7 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
|
||||||
};
|
};
|
||||||
|
|
||||||
// step 2a.
|
// step 2a.
|
||||||
if (!options.includeHidden && options.embeddedInLabelledBy !== 'self' && isElementHiddenForAria(element, options.hiddenCache)) {
|
if (!options.includeHidden && options.embeddedInLabelledBy !== 'self' && isElementHiddenForAria(element)) {
|
||||||
options.visitedElements.add(element);
|
options.visitedElements.add(element);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
@ -668,7 +675,7 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
|
||||||
if (skipSlotted && (node as Element | Text).assignedSlot)
|
if (skipSlotted && (node as Element | Text).assignedSlot)
|
||||||
return;
|
return;
|
||||||
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
|
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
|
||||||
const display = getElementComputedStyle(node as Element)?.getPropertyValue('display') || 'inline';
|
const display = getElementComputedStyle(node as Element)?.display || 'inline';
|
||||||
let token = getElementAccessibleNameInternal(node as Element, childOptions);
|
let token = getElementAccessibleNameInternal(node as Element, childOptions);
|
||||||
// SPEC DIFFERENCE.
|
// SPEC DIFFERENCE.
|
||||||
// Spec says "append the result to the accumulated text", assuming "with space".
|
// Spec says "append the result to the accumulated text", assuming "with space".
|
||||||
|
|
@ -828,3 +835,23 @@ function hasExplicitAriaDisabled(element: Element | undefined): boolean {
|
||||||
// aria-disabled works across shadow boundaries.
|
// aria-disabled works across shadow boundaries.
|
||||||
return hasExplicitAriaDisabled(parentElementOrShadowHost(element));
|
return hasExplicitAriaDisabled(parentElementOrShadowHost(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cacheAccessibleName: Map<Element, string> | undefined;
|
||||||
|
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
|
||||||
|
let cacheIsHidden: Map<Element, boolean> | undefined;
|
||||||
|
let cachesCounter = 0;
|
||||||
|
|
||||||
|
export function beginAriaCaches() {
|
||||||
|
++cachesCounter;
|
||||||
|
cacheAccessibleName ??= new Map();
|
||||||
|
cacheAccessibleNameHidden ??= new Map();
|
||||||
|
cacheIsHidden ??= new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endAriaCaches() {
|
||||||
|
if (!--cachesCounter) {
|
||||||
|
cacheAccessibleName = undefined;
|
||||||
|
cacheAccessibleNameHidden = undefined;
|
||||||
|
cacheIsHidden = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
|
import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
|
||||||
import { closestCrossShadow, isInsideScope, parentElementOrShadowHost } from './domUtils';
|
import { closestCrossShadow, isInsideScope, parentElementOrShadowHost } from './domUtils';
|
||||||
import type { InjectedScript } from './injectedScript';
|
import type { InjectedScript } from './injectedScript';
|
||||||
import { getAriaRole, getElementAccessibleName } from './roleUtils';
|
import { getAriaRole, getElementAccessibleName, beginAriaCaches, endAriaCaches } from './roleUtils';
|
||||||
import { elementText } from './selectorUtils';
|
import { elementText } from './selectorUtils';
|
||||||
|
|
||||||
type SelectorToken = {
|
type SelectorToken = {
|
||||||
|
|
@ -28,8 +28,6 @@ type SelectorToken = {
|
||||||
|
|
||||||
const cacheAllowText = new Map<Element, SelectorToken[] | null>();
|
const cacheAllowText = new Map<Element, SelectorToken[] | null>();
|
||||||
const cacheDisallowText = new Map<Element, SelectorToken[] | null>();
|
const cacheDisallowText = new Map<Element, SelectorToken[] | null>();
|
||||||
const cacheAccesibleName = new Map<Element, string>();
|
|
||||||
const cacheAccesibleNameHidden = new Map<Element, boolean>();
|
|
||||||
|
|
||||||
const kTextScoreRange = 10;
|
const kTextScoreRange = 10;
|
||||||
const kExactPenalty = kTextScoreRange / 2;
|
const kExactPenalty = kTextScoreRange / 2;
|
||||||
|
|
@ -70,6 +68,7 @@ export type GenerateSelectorOptions = {
|
||||||
|
|
||||||
export function generateSelector(injectedScript: InjectedScript, targetElement: Element, options: GenerateSelectorOptions): { selector: string, elements: Element[] } {
|
export function generateSelector(injectedScript: InjectedScript, targetElement: Element, options: GenerateSelectorOptions): { selector: string, elements: Element[] } {
|
||||||
injectedScript._evaluator.begin();
|
injectedScript._evaluator.begin();
|
||||||
|
beginAriaCaches();
|
||||||
try {
|
try {
|
||||||
targetElement = closestCrossShadow(targetElement, 'button,select,input,[role=button],[role=checkbox],[role=radio],a,[role=link]', options.root) || targetElement;
|
targetElement = closestCrossShadow(targetElement, 'button,select,input,[role=button],[role=checkbox],[role=radio],a,[role=link]', options.root) || targetElement;
|
||||||
const targetTokens = generateSelectorFor(injectedScript, targetElement, options);
|
const targetTokens = generateSelectorFor(injectedScript, targetElement, options);
|
||||||
|
|
@ -82,8 +81,7 @@ export function generateSelector(injectedScript: InjectedScript, targetElement:
|
||||||
} finally {
|
} finally {
|
||||||
cacheAllowText.clear();
|
cacheAllowText.clear();
|
||||||
cacheDisallowText.clear();
|
cacheDisallowText.clear();
|
||||||
cacheAccesibleName.clear();
|
endAriaCaches();
|
||||||
cacheAccesibleNameHidden.clear();
|
|
||||||
injectedScript._evaluator.end();
|
injectedScript._evaluator.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +272,7 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
|
||||||
|
|
||||||
const ariaRole = getAriaRole(element);
|
const ariaRole = getAriaRole(element);
|
||||||
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
|
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
|
||||||
const ariaName = getAccessibleName(element);
|
const ariaName = getElementAccessibleName(element, false);
|
||||||
if (ariaName) {
|
if (ariaName) {
|
||||||
candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, false)}]`, score: kRoleWithNameScore }]);
|
candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, false)}]`, score: kRoleWithNameScore }]);
|
||||||
candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScoreExact }]);
|
candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScoreExact }]);
|
||||||
|
|
@ -289,12 +287,6 @@ function makeSelectorForId(id: string) {
|
||||||
return /^[a-zA-Z][a-zA-Z0-9\-\_]+$/.test(id) ? '#' + id : `[id="${cssEscape(id)}"]`;
|
return /^[a-zA-Z][a-zA-Z0-9\-\_]+$/.test(id) ? '#' + id : `[id="${cssEscape(id)}"]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAccessibleName(element: Element) {
|
|
||||||
if (!cacheAccesibleName.has(element))
|
|
||||||
cacheAccesibleName.set(element, getElementAccessibleName(element, false, cacheAccesibleNameHidden));
|
|
||||||
return cacheAccesibleName.get(element)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cssFallback(injectedScript: InjectedScript, targetElement: Element, options: GenerateSelectorOptions): SelectorToken[] {
|
function cssFallback(injectedScript: InjectedScript, targetElement: Element, options: GenerateSelectorOptions): SelectorToken[] {
|
||||||
const root: Node = options.root ?? targetElement.ownerDocument;
|
const root: Node = options.root ?? targetElement.ownerDocument;
|
||||||
const tokens: string[] = [];
|
const tokens: string[] = [];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue