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

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 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 with the parseLocator function (source).

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 provides the queryAllExpression function (source) to generate executable JavaScript.

CSS Selectors (css:)

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

// 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.

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.

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 (source):

  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
// 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 (source) 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) 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 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) 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.

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 →