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=nsyntax selects the nth matching element, whileinternal:lastselects the final match. - Exact matching: Flags like
exact:trigger strict equality checks rather than substring matching. - Ref lookups: Strings starting with
@Ntrigger 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
buildLocatorFindJsfunction (lines 1515–1520) prefixes the string withloc=css:and evaluates it within the browser context. - XPath: The XPath branch (lines 1531–1535) generates a native
document.evaluatecall targetingXPathResult.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 likealt,title, anddata-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: CallsDOM.getBoxModelvia Chrome DevTools Protocol to retrieve the element's bounding box, then calculates the center coordinates{x, y}.resolveElementObjectId: UsesDOM.resolveNodeto obtain a CDP object ID for further interactions likeclickortype.
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
waitForSelectorhelper. - 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" → buildLocatorCenterJs → queryAllExpression → DOM.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"} → findBackendNodeIdByRoleName → DOM.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
parseLocatorinpackage/ego-browser/src/element-resolver.tsfor 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=andinternal:lastmodifiers allow precise element selection from matched sets. - CDP integration: Resolution ultimately yields either center coordinates via
DOM.getBoxModelor object references viaDOM.resolveNode. - Robust error handling:
ElementResolutionErrordistinguishes 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →