How Ego-Browser Handles ARIA Role Locators and Uniqueness Enforcement
Ego-Browser locates elements by ARIA role using loc=role: syntax and enforces strict uniqueness unless an explicit nth index is provided, classifying errors as transient (retryable) or permanent (unrecoverable).
In citrolabs/ego-lite, the browser automation engine provides first-class support for ARIA role locators—a powerful accessibility-first approach to element selection. This article examines how ego-browser resolves loc=role: locators, filters the Chrome accessibility tree, and enforces element uniqueness through its resolution pipeline implemented in src/element-resolver.ts.
ARIA Role Locator Syntax
Ego-Browser introduces a dedicated locator prefix for ARIA roles with optional name matching and indexing:
// Basic role selection
loc=role:button
// Role with accessible name
loc=role:button[name="Submit"]
// Zero-based index for multiple matches
loc=role:link[nth=1]
The src/helpers.ts file provides a roleSelector helper that constructs these strings programmatically for agent use.
Resolution Pipeline in element-resolver.ts
The core resolution logic resides in src/element-resolver.ts. When a locator's kind equals "role", the resolver branches based on whether an nth index is specified:
if (locator.kind === "role") {
const backendNodeId = locator.nth === undefined
? await findUniqueBackendNodeIdByRoleName(cdp, sessionId, locator.role, locator.name)
: await findBackendNodeIdByRoleName(cdp, sessionId, locator.role, locator.name, locator.nth);
Fetching the Full Accessibility Tree
The helper findBackendNodeIdsByRoleName queries Chrome's Accessibility.getFullAXTree CDP method to retrieve the complete accessibility tree for the current frame or target iframe:
const result = await send(cdp, "Accessibility.getFullAXTree", params, effectiveSessionId);
This returns the AX tree nodes needed for role-based filtering.
Filtering by Role and Accessible Name
Each node undergoes validation against three criteria (lines 45–55 in element-resolver.ts):
- Ignored nodes are skipped entirely
- Role must match exactly:
extractAxString(node.role) !== role - Name must match (if provided):
axNameMatches(extractAxString(node.name), name)supports both string equality and RegExp patterns
if (extractAxString(node.role) !== role) continue;
if (name !== undefined && !axNameMatches(extractAxString(node.name), name)) continue;
Backend Node ID Extraction and Permanent Errors
Every matched node must expose a backendDOMNodeId for DOM reference. Missing this ID triggers a permanent ElementResolutionError—the element exists in the AX tree but cannot be manipulated:
const backendNodeId = node.backendDOMNodeId;
if (backendNodeId === undefined || backendNodeId === null) {
throw new ElementResolutionError(
`AX node has no backendDOMNodeId for role=${role} name=${name}`,
"permanent",
);
}
Uniqueness Enforcement Strategy
Ego-Browser implements strict uniqueness enforcement unless explicitly bypassed.
When nth Is Undefined: Unique Match Required
findUniqueBackendNodeIdByRoleName validates exactly one match exists:
| Match Count | Error Type | Classification |
|---|---|---|
| 0 | matched 0 elements |
transient — element may appear after retry |
| 1 | Success — returns backendNodeId |
N/A |
| 2+ | matched N elements |
permanent — selector is ambiguous |
if (matches.length === 0) { /* transient 0-match error */ }
if (matches.length > 1) { /* permanent multiple-match error */ }
Transient errors enable automatic retry logic; permanent errors halt execution immediately.
When nth Is Provided: Indexed Selection
findBackendNodeIdByRoleName bypasses uniqueness checks and returns the specified index (or the last element if nth exceeds match count):
// Click the second link (zero-based index)
await click('loc=role:link[nth=1]');
Converting to JavaScript Object References
After obtaining a valid backendNodeId, the resolver converts it to a JavaScript objectId via DOM.resolveNode:
const result = await send(cdp, "DOM.resolveNode", {
backendNodeId,
objectGroup: "ego-browser"
}, sessionId);
This objectId enables subsequent actions: clicking, evaluating JavaScript, extracting properties, or taking screenshots.
Practical Code Examples
// 1️⃣ Locate a unique button by role and accessible name
await click('loc=role:button[name="Submit"]');
// 2️⃣ Click the third checkbox in a form
await click('loc=role:checkbox[nth=2]');
// 3️⃣ Programmatic resolution with eval
const nodeId = await ego.eval(`
await ego.resolveLocator('loc=role:heading')
`);
// 4️⃣ Pattern matching with RegExp names
await click('loc=role:link[name=/^Continue/]');
Error Classification System
The ElementResolutionError class in src/ego-errors.ts categorizes failures to guide retry behavior:
- Transient errors: Retryable conditions like zero matches (element not yet rendered)
- Permanent errors: Unrecoverable conditions like multiple matches or missing
backendDOMNodeId
This classification allows Ego-Browser's orchestration layer to implement intelligent backoff strategies without manual intervention.
Key Implementation Files
| File | Responsibility |
|---|---|
src/element-resolver.ts |
Core ARIA role resolution, AX tree traversal, uniqueness validation |
src/helpers.ts |
roleSelector helper for constructing locator strings |
src/ego-errors.ts |
ElementResolutionError with transient/permanent classification |
Summary
- Syntax: Use
loc=role:<role>[name="..."][nth=N]for ARIA-based element location - Resolution: Queries Chrome's
Accessibility.getFullAXTreeCDP method viasrc/element-resolver.ts - Filtering: Matches exact role strings and optional accessible names (string or RegExp)
- Uniqueness: Enforces single-element matches unless
nthis specified; zero matches = transient error, multiple matches = permanent error - Error handling:
ElementResolutionErrorinsrc/ego-errors.tsenables appropriate retry strategies
Frequently Asked Questions
What happens if multiple elements match an ARIA role locator without nth specified?
Ego-Browser throws a permanent ElementResolutionError with message matched N elements. This prevents ambiguous actions and forces explicit indexing or more specific name matching.
Can I use regular expressions to match accessible names?
Yes. The axNameMatches helper in src/element-resolver.ts accepts both strings and RegExp patterns for the name parameter: loc=role:button[name=/^Save/].
Why does Ego-Browser require a backendDOMNodeId?
The backendDOMNodeId bridges the accessibility tree (AX nodes) to the DOM node representation needed for JavaScript execution. Without it, Chrome DevTools Protocol cannot resolve the node to a runtime object, making interaction impossible.
How do I handle dynamically appearing elements with role locators?
Rely on transient error classification. When locator.nth is undefined and zero matches exist, Ego-Browser returns a transient error that triggers automatic retry logic in the surrounding agent framework.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →