How Ego-Lite Resolves Elements Using Different Selector Types

Ego-Lite resolves elements through a unified parseLocator function in element-resolver.ts that normalizes selector strings into typed locator objects, then routes them to specialized resolution strategies including CSS/XPath evaluation, accessibility tree traversal, and attribute-based DOM walking.

The citrolabs/ego-lite repository provides a browser-automation harness that abstracts element location behind a single string argument. This article examines how the system parses these strings and dispatches them to the appropriate resolution mechanism based on the selector type.

The Core Resolution Pipeline

The resolution flow follows a strict pipeline: raw selector → parseLocator → selector-specific JS builder → CDP evaluation → element center/objectId.

All resolution logic resides in package/ego-browser/src/element-resolver.ts. The entry point, parseLocator(input) (lines 1514–1610), examines the raw string and returns a normalized locator object containing the selector's kind (css, xpath, role, text, label, placeholder, alt, title, testid, href, query) and optional nth index modifiers (internal:nth= or internal:last;).

Selector Parsing with parseLocator

When you pass a selector like role:button[name=Confirm] or css:button.submit, the parseLocator function identifies the prefix and payload. It handles:

  • Explicit indices: The internal:nth=n syntax selects the nth matching element, while internal:last selects the final match.
  • Exact matching: Flags like exact: trigger strict equality checks rather than substring matching.
  • Ref lookups: Strings starting with @N trigger reference resolution against the internal ref map.

The parsed locator object serves as the routing token for the rest of the resolution pipeline.

Role-Based Resolution via the Accessibility Tree

Role-based selectors leverage Chrome's Accessibility tree rather than the DOM. Ego-Lite uses the CDP command Accessibility.getFullAXTree to retrieve the full accessibility tree, then passes it to findBackendNodeIdsByRoleName (lines 2424–2505).

This helper walks the AX nodes, matching both the requested role and name (accessible name). If the locator specifies an nth index, it selects the corresponding match from the results; otherwise, it requires a unique match and throws a ElementResolutionError if multiple elements qualify.

CSS and XPath Selector Evaluation

For CSS selectors and XPath expressions, Ego-Lite delegates to the generic DOM query helper queryAllExpression from locator-query.ts.

  • CSS: The buildLocatorFindJs function (lines 1515–1520) prefixes the string with loc=css: and evaluates it within the browser context.
  • XPath: The XPath branch (lines 1531–1535) generates a native document.evaluate call targeting XPathResult.ORDERED_NODE_SNAPSHOT_TYPE.

Both strategies return a list of matching elements that subsequent filtering logic can narrow by index.

Text and Attribute-Based Selectors

Selectors like text, label, placeholder, alt, title, and testid generate specialized JavaScript helpers that execute in the browser context:

  • textElementsJs: Walks the DOM to extract inner text, normalizes whitespace, and performs exact or "contains" matching.
  • labelElementsJs: Targets <label> elements and their associated form controls.
  • attributeElementsJs: Generic helper for HTML attributes like alt, title, and data-testid.

These functions (lines 1451–1484) filter results to avoid nested matches—preventing a parent container from shadowing its child text elements.

Specialized Selectors: Href and Raw Queries

Href selectors collect all <a> elements with an href attribute and compare the resolved URL (including path, query, and hash) against the supplied value. The hrefElementsJs helper (lines 1657–1665) handles this comparison.

Raw query selectors bypass the standard parsing logic. Strings prefixed with internal:scope: or internal:filter: route directly to queryAllExpression, allowing complex chained queries against specific DOM scopes.

Resolving to Element Centers and Object IDs

Once a locator identifies candidate elements, the system resolves them to actionable references through two public helpers:

  • resolveElementCenter: Calls DOM.getBoxModel via Chrome DevTools Protocol to retrieve the element's bounding box, then calculates the center coordinates {x, y}.
  • resolveElementObjectId: Uses DOM.resolveNode to obtain a CDP object ID for further interactions like click or type.

If the locator specifies a ref (@N), the resolver first checks the ref map in ref-map.ts. If the backend node is stale, it falls back to role/name lookup.

Error Handling and Retry Semantics

Throughout the resolution flow, functions throw ElementResolutionError with a kind classification:

  • Transient: Indicates the element may appear with a retry (e.g., element not yet rendered). Powers the waitForSelector helper.
  • Permanent: Indicates a selector logic problem (e.g., multiple matches found when uniqueness is required).

This distinction allows higher-level automation scripts to distinguish between timing issues and selector defects.

Code Examples

Resolving a CSS Selector (Center Coordinates)

// Returns {x, y, sessionId} for the center of the submit button
const { x, y } = await js('css:button.submit');

Behind the scenes: parseLocator identifies kind: "css"buildLocatorCenterJsqueryAllExpressionDOM.getBoxModel.

Resolving a Role-Based Element (Object ID)

// Find the first button with role="button" and accessible name "Confirm"
const { objectId } = await js('role:button[name=Confirm]');

Behind the scenes: parseLocator yields {kind:"role", role:"button", name:"Confirm"}findBackendNodeIdByRoleNameDOM.resolveNode.

Using a Text Selector with Exact Match

// Click the element whose visible text exactly matches "Log In"
await click('text:exact:Log In');

Behind the scenes: parseLocator creates {kind:"text", text:"Log In", exact:true}textElementsJs → filtered element list → first element's objectId.

Selecting by Href (Last Match)

// Retrieve centre of the last link pointing to "/settings"
const pos = await js('href:/settings;internal:last');

Behind the scenes: parseLocator{kind:"href", href:"/settings", nth:"last"}hrefElementsJs → selects the final matching <a> element.

Summary

  • Unified entry point: All selectors pass through parseLocator in package/ego-browser/src/element-resolver.ts for normalization.
  • Multiple resolution strategies: The system routes to accessibility tree traversal, DOM querying, or attribute walking based on the selector kind.
  • Index support: The internal:nth= and internal:last modifiers allow precise element selection from matched sets.
  • CDP integration: Resolution ultimately yields either center coordinates via DOM.getBoxModel or object references via DOM.resolveNode.
  • Robust error handling: ElementResolutionError distinguishes between transient (retryable) and permanent (selector logic) failures.

Frequently Asked Questions

How does Ego-Lite handle ambiguous selectors that match multiple elements?

According to the source code in element-resolver.ts, Ego-Lite throws a permanent ElementResolutionError when a selector matches multiple elements but no nth index is specified. To select a specific match, append internal:nth=n (0-indexed) or internal:last to the selector string.

What is the difference between resolveElementCenter and resolveElementObjectId?

resolveElementCenter returns the {x, y} coordinates of an element's center by calling CDP's DOM.getBoxModel, suitable for mouse interactions. resolveElementObjectId returns a CDP RemoteObject ID via DOM.resolveNode, required for invoking element methods or accessing properties directly.

Can Ego-Lite resolve elements using ARIA roles instead of CSS classes?

Yes. Role-based selectors use the accessibility tree via Accessibility.getFullAXTree and findBackendNodeIdsByRoleName. Use the syntax role:button[name=Submit] to match elements by their computed role and accessible name, which is more resilient than CSS selectors when DOM structure changes.

Where does Ego-Lite store references to previously resolved elements?

The system maintains an internal ref map in package/ego-browser/src/ref-map.ts. When you use a ref selector like @1, the resolver looks up the backend node ID in this map before falling back to fresh resolution if the reference has become stale.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →