Skip to content

Commit a702595

Browse files
committed
cherry-pick(#29765): Revert "chore(role): cache element list by role (#29130)"
This reverts commit 1ce3ca2. Added a regression test. Fixes #29760.
1 parent b670506 commit a702595

File tree

3 files changed

+47
-59
lines changed

3 files changed

+47
-59
lines changed

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

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

1717
import type { SelectorEngine, SelectorRoot } from './selectorEngine';
1818
import { matchesAttributePart } from './selectorUtils';
19-
import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaSelected, getElementAccessibleName, getElementsByRole, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
19+
import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, 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';
2322

2423
type RoleEngineOptions = {
2524
role: string;
@@ -126,27 +125,26 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string): RoleE
126125
}
127126

128127
function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: boolean): Element[] {
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;
128+
const result: Element[] = [];
129+
const match = (element: Element) => {
130+
if (getAriaRole(element) !== options.role)
131+
return;
134132
if (options.selected !== undefined && getAriaSelected(element) !== options.selected)
135-
return false;
133+
return;
136134
if (options.checked !== undefined && getAriaChecked(element) !== options.checked)
137-
return false;
135+
return;
138136
if (options.pressed !== undefined && getAriaPressed(element) !== options.pressed)
139-
return false;
137+
return;
140138
if (options.expanded !== undefined && getAriaExpanded(element) !== options.expanded)
141-
return false;
139+
return;
142140
if (options.level !== undefined && getAriaLevel(element) !== options.level)
143-
return false;
141+
return;
144142
if (options.disabled !== undefined && getAriaDisabled(element) !== options.disabled)
145-
return false;
143+
return;
146144
if (!options.includeHidden) {
147145
const isHidden = isElementHiddenForAria(element);
148146
if (isHidden)
149-
return false;
147+
return;
150148
}
151149
if (options.name !== undefined) {
152150
// Always normalize whitespace in the accessible name.
@@ -157,10 +155,25 @@ function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: bo
157155
if (internal && !options.exact && options.nameOp === '=')
158156
options.nameOp = '*=';
159157
if (!matchesAttributePart(accessibleName, { name: '', jsonPath: [], op: options.nameOp || '=', value: options.name, caseSensitive: !!options.exact }))
160-
return false;
158+
return;
161159
}
162-
return true;
163-
});
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;
164177
}
165178

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

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

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -845,51 +845,11 @@ 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-
886848
let cacheAccessibleName: Map<Element, string> | undefined;
887849
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
888850
let cacheIsHidden: Map<Element, boolean> | undefined;
889851
let cachePseudoContentBefore: Map<Element, string> | undefined;
890852
let cachePseudoContentAfter: Map<Element, string> | undefined;
891-
let cacheElementsByRole: Map<string, Element[]> | undefined;
892-
let cacheElementsByRoleDocument: Document | undefined;
893853
let cachesCounter = 0;
894854

895855
export function beginAriaCaches() {
@@ -908,7 +868,5 @@ export function endAriaCaches() {
908868
cacheIsHidden = undefined;
909869
cachePseudoContentBefore = undefined;
910870
cachePseudoContentAfter = undefined;
911-
cacheElementsByRole = undefined;
912-
cacheElementsByRoleDocument = undefined;
913871
}
914872
}

tests/page/selectors-role.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,3 +484,20 @@ test('should support output accessible name', async ({ page }) => {
484484
await page.setContent(`<label>Output1<output>output</output></label>`);
485485
await expect(page.getByRole('status', { name: 'Output1' })).toBeVisible();
486486
});
487+
488+
test('should not match scope by default', async ({ page }) => {
489+
await page.setContent(`
490+
<ul>
491+
<li aria-label="Parent list">
492+
Parent list
493+
<ul>
494+
<li>child 1</li>
495+
<li>child 2</li>
496+
</ul>
497+
</li>
498+
</ul>
499+
`);
500+
const children = page.getByRole('listitem', { name: 'Parent list' }).getByRole('listitem');
501+
await expect(children).toHaveCount(2);
502+
await expect(children).toHaveText(['child 1', 'child 2']);
503+
});

0 commit comments

Comments
 (0)