# Ego-Browser Element Resolver Logic for Different Selectors: A Deep Dive

> Explore ego-browser element resolver logic. Learn how Ego-Browser parses selectors into kinds and executes queries or accessibility tree lookups for DOM elements.

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

---

**Ego-Browser uses a two-stage resolver that parses selector strings into classified kinds, then generates and executes JavaScript queries or accessibility tree lookups to return DOM elements or their coordinates.**

The `ego-browser` package in the [citrolabs/ego-lite](https://github.com/citrolabs/ego-lite) repository implements a sophisticated element resolution system. This article explains how the **ego-browser element resolver logic for different selectors** transforms strings like `role:button[name=Submit]` or `css:ul > li` into concrete browser elements via the Chrome DevTools Protocol (CDP).

## Parse Stage: Classifying Selectors with `parseLocator`

Every resolution begins in **[`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts)** with the `parseLocator` function ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts#L14)).

The parser performs three operations:

1. **Detects the selector kind** — Identifies prefixes like `role:`, `css:`, `xpath=`, `text:`, `label:`, `placeholder:`, `alt:`, `title:`, `testid:`, `href:`, or `internal:scope:`
2. **Extracts ordinal modifiers** — Parses `internal:nth=` for positional indexing or `internal:last;` for last-element selection
3. **Parses role name matchers** — For role selectors, extracts the optional `[name=...]` accessible name filter

The parser outputs a structured locator object that downstream functions consume to build execution strategies.

## Query Building: Generating JavaScript with `queryAllExpression`

For non-role selectors, **[`locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/locator-query.ts)** provides the `queryAllExpression` function ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/locator-query.ts#L21)) to generate executable JavaScript.

### CSS Selectors (`css:`)

Raw CSS selectors pass directly to `document.querySelectorAll()`.

```ts
// Input: "css:ul > li:nth-child(3)"
// Generated: document.querySelectorAll("ul > li:nth-child(3)")
await resolveElementCenter(cdp, sessionId, refMap, "css:ul > li:nth-child(3)");

```

### XPath Selectors (`xpath=`)

Uses `document.evaluate()` with `ORDERED_NODE_SNAPSHOT_TYPE` and `snapshotItem(i)` for indexed access.

### Href Selectors (`href:`)

Filters all anchor elements by exact URL string match on the `href` attribute.

```ts
await resolveElementCenter(cdp, sessionId, refMap, "href:/products/123");

```

### Text Selectors (`text:` or `text=`)

Walks the DOM tree, normalizes inner text (collapsing whitespace), and applies:

- **Exact match**: `text:exact:Welcome`
- **Fuzzy/substring match**: `text:Welcome`

The normalization matches browser rendering behavior for visible text.

### Attribute-Based Selectors

| Prefix | Targets |
|--------|---------|
| `label:` | Associated `<label>` elements or `aria-label` attributes |
| `placeholder:` | `placeholder` attribute on `<input>` or `<textarea>` |
| `alt:` | `alt` attribute on `<img>` or `<input type="image">` |
| `title:` | Elements with `[title]` attribute |
| `testid:` | Elements with `[data-testid]` attribute |

### Internal Scoped Queries (`internal:scope:` / `internal:filter:`)

Supports nested queries and attribute filtering for complex containment logic.

```ts
await resolveElementObjectId(cdp, sessionId, refMap, 
  "internal:scope:{\"base\":\"#main\",\"child\":\".item\"}");

```

## Role-Based Resolution: Accessibility Tree Queries

When `parseLocator` identifies a `role:` selector, the resolver bypasses DOM queries entirely. The **`findBackendNodeIdsByRoleName`** function in [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts) ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts#L24)):

1. Calls `Accessibility.getFullAXTree` via CDP to fetch the accessibility tree
2. Filters nodes by the requested ARIA role
3. Applies optional name matching using the same text-matching logic as DOM selectors
4. Returns `backendDOMNodeId` values convertible to DOM nodes via `DOM.getBoxModel` or `DOM.resolveNode`

```ts
// Resolve a button by role and accessible name
await resolveElementCenter(cdp, sessionId, refMap, "role:button[name=Submit]");

```

This enables resilient automation that survives DOM restructuring when semantic roles remain stable.

## Ordinal Handling: `nth` and `last` Modifiers

The **`parseInternalNth`** helper in [`locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/locator-query.ts) ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/locator-query.ts#L31)) handles positional selection:

- **`internal:nth=3`** — Selects the fourth element (zero-indexed: `[3]`)
- **`internal:last;`** — Selects `.at(-1)` from the result array

These prefixes extract indices during parsing; the executor applies them after the base query completes.

## Error Classification and Handling

The **`selectorResolutionError`** function ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts#L52)) implements strict resolution semantics:

- **Zero matches** — Throws `ElementResolutionError` with `transient: true` (retryable)
- **Multiple matches without `nth`** — Throws permanent error requiring selector refinement
- **Missing `backendDOMNodeId`** — Special handling for accessibility tree results

This design forces explicit disambiguation rather than defaulting to first-match behavior.

## Complete Resolution API

The two primary entry points in [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts) unify all selector types:

| Function | Returns | Use Case |
|----------|---------|----------|
| `resolveElementCenter` | `{x, y}` coordinates | Clicking, hovering, or screenshot positioning |
| `resolveElementObjectId` | CDP object ID | Further CDP operations on the resolved element |

Both accept numeric refs (`@12` mapped via [`ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-map.ts)) or any supported selector string.

## Summary

- **Two-stage architecture**: Parsing (`parseLocator`) separates syntax classification from execution strategy
- **Dual execution paths**: DOM queries (JavaScript generation) for most selectors; accessibility tree traversal for `role:` selectors
- **Rich selector vocabulary**: 11 distinct kinds supporting CSS, XPath, text, attributes, roles, and scoped queries
- **Explicit ordinal control**: `nth` and `last` modifiers enable precise element targeting
- **Strict error semantics**: Transient errors for empty results, permanent errors for ambiguous matches

## Frequently Asked Questions

### What selector types does Ego-Browser support?

Ego-Browser supports **role**, **css**, **xpath**, **href**, **text**, **label**, **placeholder**, **alt**, **title**, **testid**, and generic **query** selectors. Each maps to a specific resolution strategy in the `parseLocator` classification system.

### How does role-based selection differ from CSS selection?

**Role-based selection** queries the browser's accessibility tree via `Accessibility.getFullAXTree`, matching ARIA roles and optional accessible names. **CSS selection** generates JavaScript using `document.querySelectorAll()` and executes it in the page context. Role queries are more resilient to DOM changes but slower due to the additional CDP round-trip.

### Can I select elements by their visible text content?

Yes. Use `text:exact:Your Text` for exact matches or `text:Your Text` for substring matches. The resolver normalizes whitespace to match browser rendering. For precise control, combine with `nth` modifiers when multiple elements share the same text.

### What happens when multiple elements match my selector?

Ego-Browser throws a permanent `ElementResolutionError` unless you specify an ordinal. Add `internal:nth=0` for the first match, `internal:nth=2` for the third, or `internal:last;` for the final match. This design prevents flaky automation from implicit first-match behavior.