Skip to content

Commit 1ce3ca2

Browse files
authored
chore(role): cache element list by role (microsoft#29130)
1 parent 8898a53 commit 1ce3ca2

File tree

2 files changed

+59
-30
lines changed

2 files changed

+59
-30
lines changed

packages/playwright-core/src/server/injected/roleSelectorEngine.ts

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616

1717
import type { SelectorEngine, SelectorRoot } from './selectorEngine';
1818
import { matchesAttributePart } from './selectorUtils';
19-
import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
19+
import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaSelected, getElementAccessibleName, getElementsByRole, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
2020
import { parseAttributeSelector, type AttributeSelectorPart, type AttributeSelectorOperator } from '../../utils/isomorphic/selectorParser';
2121
import { normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
22+
import { isInsideScope } from './domUtils';
2223

2324
type RoleEngineOptions = {
2425
role: string;
@@ -125,26 +126,27 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string): RoleE
125126
}
126127

127128
function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: boolean): Element[] {
128-
const result: Element[] = [];
129-
const match = (element: Element) => {
130-
if (getAriaRole(element) !== options.role)
131-
return;
129+
const doc = scope.nodeType === 9 /* Node.DOCUMENT_NODE */ ? scope as Document : scope.ownerDocument;
130+
const elements = doc ? getElementsByRole(doc, options.role) : [];
131+
return elements.filter(element => {
132+
if (!isInsideScope(scope, element))
133+
return false;
132134
if (options.selected !== undefined && getAriaSelected(element) !== options.selected)
133-
return;
135+
return false;
134136
if (options.checked !== undefined && getAriaChecked(element) !== options.checked)
135-
return;
137+
return false;
136138
if (options.pressed !== undefined && getAriaPressed(element) !== options.pressed)
137-
return;
139+
return false;
138140
if (options.expanded !== undefined && getAriaExpanded(element) !== options.expanded)
139-
return;
141+
return false;
140142
if (options.level !== undefined && getAriaLevel(element) !== options.level)
141-
return;
143+
return false;
142144
if (options.disabled !== undefined && getAriaDisabled(element) !== options.disabled)
143-
return;
145+
return false;
144146
if (!options.includeHidden) {
145147
const isHidden = isElementHiddenForAria(element);
146148
if (isHidden)
147-
return;
149+
return false;
148150
}
149151
if (options.name !== undefined) {
150152
// Always normalize whitespace in the accessible name.
@@ -155,25 +157,10 @@ function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: bo
155157
if (internal && !options.exact && options.nameOp === '=')
156158
options.nameOp = '*=';
157159
if (!matchesAttributePart(accessibleName, { name: '', jsonPath: [], op: options.nameOp || '=', value: options.name, caseSensitive: !!options.exact }))
158-
return;
160+
return false;
159161
}
160-
result.push(element);
161-
};
162-
163-
const query = (root: Element | ShadowRoot | Document) => {
164-
const shadows: ShadowRoot[] = [];
165-
if ((root as Element).shadowRoot)
166-
shadows.push((root as Element).shadowRoot!);
167-
for (const element of root.querySelectorAll('*')) {
168-
match(element);
169-
if (element.shadowRoot)
170-
shadows.push(element.shadowRoot);
171-
}
172-
shadows.forEach(query);
173-
};
174-
175-
query(scope);
176-
return result;
162+
return true;
163+
});
177164
}
178165

179166
export function createRoleEngine(internal: boolean): SelectorEngine {

packages/playwright-core/src/server/injected/roleUtils.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,11 +845,51 @@ function getAccessibleNameFromAssociatedLabels(labels: Iterable<HTMLLabelElement
845845
})).filter(accessibleName => !!accessibleName).join(' ');
846846
}
847847

848+
export function getElementsByRole(document: Document, role: string): Element[] {
849+
if (document === cacheElementsByRoleDocument)
850+
return cacheElementsByRole!.get(role) || [];
851+
const map = calculateElementsByRoleMap(document);
852+
if (cachesCounter) {
853+
cacheElementsByRoleDocument = document;
854+
cacheElementsByRole = map;
855+
}
856+
return map.get(role) || [];
857+
}
858+
859+
function calculateElementsByRoleMap(document: Document) {
860+
const result = new Map<string, Element[]>();
861+
862+
const visit = (root: Element | ShadowRoot | Document) => {
863+
const shadows: ShadowRoot[] = [];
864+
if ((root as Element).shadowRoot)
865+
shadows.push((root as Element).shadowRoot!);
866+
for (const element of root.querySelectorAll('*')) {
867+
const role = getAriaRole(element);
868+
if (role) {
869+
let list = result.get(role);
870+
if (!list) {
871+
list = [];
872+
result.set(role, list);
873+
}
874+
list.push(element);
875+
}
876+
if (element.shadowRoot)
877+
shadows.push(element.shadowRoot);
878+
}
879+
shadows.forEach(visit);
880+
};
881+
visit(document);
882+
883+
return result;
884+
}
885+
848886
let cacheAccessibleName: Map<Element, string> | undefined;
849887
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
850888
let cacheIsHidden: Map<Element, boolean> | undefined;
851889
let cachePseudoContentBefore: Map<Element, string> | undefined;
852890
let cachePseudoContentAfter: Map<Element, string> | undefined;
891+
let cacheElementsByRole: Map<string, Element[]> | undefined;
892+
let cacheElementsByRoleDocument: Document | undefined;
853893
let cachesCounter = 0;
854894

855895
export function beginAriaCaches() {
@@ -868,5 +908,7 @@ export function endAriaCaches() {
868908
cacheIsHidden = undefined;
869909
cachePseudoContentBefore = undefined;
870910
cachePseudoContentAfter = undefined;
911+
cacheElementsByRole = undefined;
912+
cacheElementsByRoleDocument = undefined;
871913
}
872914
}

0 commit comments

Comments
 (0)