java-topology/docs/tickets/playwright-0001-roleutils-validroles-array-includes.md
russell@unturf.com 9a0253e724 browser-automation: 4 CWE-407 patches (selenium x2, playwright, webdriverio)
selenium-0001: SessionCapabilitiesMutator list.contains O(NxM) -> LinkedHashSet
  O(N+M). Grid Node session mutation hot path. Bench: 192x at N=M=1000.

selenium-0002: ChromiumOptions merge helpers consolidate four list.contains
  loops behind addArgumentsUnique/addEncodedExtensionsUnique. Bench: 254x
  at N=M=1000.

playwright-0001: roleUtils validRoles / allowsNameFromContent Array.includes
  on 20-70 element constant arrays per element. Converted to Set<string>
  at module load. Bench: 11x at N=10000 elements.

webdriverio-0001: xpath-conditions extractOrConditions orMatches.find +
  values.includes per regex match -> Map<attr, Set<values>>. Bench: 6x at
  K=V=60 in the 'mobileSelectorPerformanceOptimizer'.

Each defect ships: ticket, patch with complexity-gate header, Python
benchmark + correctness test, Makefile, outreach brief. All 16 tests
pass. UNDF IDs: 1276 (playwright), 1277 (selenium-0001), 1288
(selenium-0002), 1289 (webdriverio).
2026-04-22 18:14:39 -04:00

3.4 KiB
Raw Permalink Blame History

playwright-0001: roleUtils — O(N×k) ARIA role validation in per-element snapshot

Target: microsoft/playwright Severity: MEDIUM-HIGH CWE: CWE-407 (Inefficient Algorithmic Complexity) MOAD: MOAD-0001 (A Sedimentary Defect) File: packages/injected/src/roleUtils.ts:262-268, 499-500, 51-52, 262-268 Language: TypeScript Status: open

Description

Playwright injects roleUtils.ts into every target page to compute accessible names and ARIA roles. Several frequently-called helpers use Array.includes on constant arrays holding 70+ role strings. When Playwright snapshots a page's accessibility tree, getExplicitAriaRole, allowsNameFromContent, and hasGlobalAriaAttribute run once per element — thousands of times on modern pages.

Each Array.includes call is O(k) where k is the array length. Inside a DOM traversal of N elements, total cost is O(N×k). On a page with 5000 elements and the 70-entry validRoles array, that's 350,000 string comparisons per snapshot. Converting the arrays to Set<string> drops lookup to O(1), producing O(N+k).

Root Cause

// roleUtils.ts:262-268
const validRoles: AriaRole[] = ['alert', 'alertdialog', 'application', ...70 items];

function getExplicitAriaRole(element: Element): AriaRole | null {
  const roles = (element.getAttribute('role') || '').split(' ').map(...);
  return roles.find(role => validRoles.includes(role as any)) as AriaRole || null;
}

// roleUtils.ts:499-500 (inside allowsNameFromContent — called per element)
const alwaysAllowsNameFromContent = [
  'button', 'cell', 'checkbox', 'columnheader', ...20 items
].includes(role);
const descendantAllowsNameFromContent = targetDescendant && [
  '', 'caption', 'code', ...30 items
].includes(role);

// roleUtils.ts:51-52 (kGlobalAriaAttributes prohibited-list scan inside
//  hasGlobalAriaAttribute, called per element):
!prohibited?.includes(forRole || '')

Each Array.includes is O(k). Called per-element across thousands of elements, cost compounds to O(N×k). The arrays are constant and could be built once as Sets at module load.

Fix

Convert hot-path constant arrays to Set<string> at module scope, use set.has(x) for O(1) lookup:

const validRolesSet = new Set<string>(validRoles);

function getExplicitAriaRole(element: Element): AriaRole | null {
  const roles = (element.getAttribute('role') || '').split(' ').map(...);
  return roles.find(role => validRolesSet.has(role)) as AriaRole || null;
}

const alwaysAllowsNameFromContentSet = new Set([
  'button', 'cell', 'checkbox', ...
]);
const descendantAllowsNameFromContentSet = new Set([...]);

function allowsNameFromContent(role: string, targetDescendant: boolean) {
  return alwaysAllowsNameFromContentSet.has(role) ||
         (targetDescendant && descendantAllowsNameFromContentSet.has(role));
}

Set construction runs once at module load. Per-element lookups drop to O(1).

Severity Note

Runs on every accessibility snapshot, every getByRole locator, every ARIA tree traversal. Per-element cost is microseconds, but pages with 5000+ elements compound quickly. Large enterprise apps (dashboards, CRMs, grid layouts) commonly exceed this element count. Impact amplifies under @playwright/test parallel runs with toHaveAccessibleName assertions.

Complexity Gate

  • N=5000 elements, 70-element role arrays: fixed must complete in <10ms
  • k-scaling 5×: time ratio must be <17.5× (O(k) ≈5×, not O(k²) ≈25×)