# How Ego-Browser Handles ARIA Role Locators and Uniqueness Enforcement

> Learn how ego-browser finds elements with ARIA role locators and enforces uniqueness. Understand transient vs permanent errors for robust testing.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-29

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts).

## ARIA Role Locator Syntax

Ego-Browser introduces a dedicated locator prefix for ARIA roles with optional name matching and indexing:

```ts
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts). When a locator's `kind` equals `"role"`, the resolver branches based on whether an `nth` index is specified:

```ts
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:

```ts
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`](https://github.com/citrolabs/ego-lite/blob/main/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

```ts
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:

```ts
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 |

```ts
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):

```ts
// 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`:

```ts
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

```ts
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) | Core ARIA role resolution, AX tree traversal, uniqueness validation |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | `roleSelector` helper for constructing locator strings |
| [`src/ego-errors.ts`](https://github.com/citrolabs/ego-lite/blob/main/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.getFullAXTree` CDP method via [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts)
- **Filtering**: Matches exact role strings and optional accessible names (string or RegExp)
- **Uniqueness**: Enforces single-element matches unless `nth` is specified; zero matches = transient error, multiple matches = permanent error
- **Error handling**: `ElementResolutionError` in [`src/ego-errors.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ego-errors.ts) enables 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`](https://github.com/citrolabs/ego-lite/blob/main/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.