What Selector Formats Does the Ego-Lite Element Resolver Support? A Complete Guide
TLDR: The ego-lite element resolver in citrolabs/ego-lite supports a unified selector language covering CSS, XPath, ARIA role, text, href, attribute-based lookups, and snapshot references — all parsed by parseLocator in package/ego-browser/src/element-resolver.ts and resolved via CDP calls or in-page JavaScript.
The citrolabs/ego-lite repository implements an intelligent browser automation layer where the core element-resolver.ts module translates selector strings into concrete DOM elements or their geometric coordinates (centers). It provides agents with a single, declarative API that abstracts away raw Chrome DevTools Protocol (CDP) calls, iframe handling, and accessibility tree queries. Instead of maintaining multiple resolution mechanisms, every selector format routes through one parser and one set of resolution strategies, making it easy to switch between CSS and role-based queries without rewriting test code.
Understanding the Selector Resolution Pipeline
The resolver follows a two-phase flow. First, it checks if the input is a snapshot reference (starting with @). If not, it parses the string into a structured locator object and then dispatches to the appropriate resolution implementation. Both paths funnel into the same geometry or object-id outputs.
The complete logic lives in [element-resolver.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts), with supporting query-generation code in [locator-query.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/locator-query.ts) and user-facing helpers in [helpers.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts).
All Supported Selector Formats in Ego-Lite
The parseLocator function (lines 147–188 in element-resolver.ts) recognizes these prefixes and maps them to an internal locator record with a kind field:
| Format | Prefix | Locator Kind | Example |
|---|---|---|---|
| CSS selector | css: |
"css" |
css:.submit-button |
| XPath expression | xpath: |
"xpath" |
xpath://div[@id='main']/button |
| Hyperlink path | href: |
"href" |
href:/settings/account |
| Text match | text: or text= |
"text" (with optional exact) |
text=Login |
| Label / ARIA label | label: |
text-like | label:Username |
| Placeholder | placeholder: |
text-like | placeholder:Enter email |
| Alt text | alt: |
text-like | alt:Product logo |
| Title attribute | title: |
text-like | title:Close dialog |
| Test ID | testid: |
text-like | testid:user-menu |
| ARIA role | role: |
"role" (with optional name= and nth) |
role:button[name="Close"] |
| Plain CSS (fallback) | none | "css" |
.card button |
| Snapshot reference | @ |
ref lookup | @42 |
The loc= Prefix Group
All locator prefixes (except the bare @ ref) can be explicitly wrapped in loc=. So loc=css:.submit-button is equivalent to css:.submit-button. The parser strips loc=, then re-parses the remainder using the rules above. If no known prefix is present, the string is treated as a raw CSS selector.
Inside the Locator Parser
For a css-only string like css:.submit-button, output is { kind: "css", selector: ".submit-button" }. When you append nth=2, the parser keeps it in the locator object for later index selection. XPath similarly produces { kind: "xpath", xpath: "//div" }. The text collect family (text, label, placeholder, alt, title, testid) all collapse into separate kinds internally but share the same DOM-scanning logic, which walks elements and matches by their visible text or attribute values.
ARIA role locators accept an optional name filter and index, for example role:button[navbar] or role:button;nth=1. The name filter is not a CSS attribute selector but a separate key that is passed to the accessibility-tree query.
How Each Selector Kind Is Resolved to a DOM Element
Once the parser produces a structured locator, the resolver branches into one of four resolution pathways:
- Role-based locators –
findBackendNodeIdByRoleNamequeries the CDP Accessibility tree usingAccessibility.getFullAXTreeto find the element by role and optional name. This keeps the DOM structure changes from breaking the resolution, at the cost of an extra protocol round-trip. - CSS / XPath / href – The resolver has a SSR JavaScript snippet (
queryAllExpression) that is executed in the page. CSS selection normalizes the selector text and picks the target by index (nthorlast). XPath usesdocument.evaluatedirectly. Href selectors filter<a>elements using an attribute comparison against the supplied path or full URL. - Text-based selectors –
buildLocatorAllJsbuilds a script that recursively scans the DOM for elements whoseinnerText,label,placeholder,alt,title, ordata-testidmatches the queried text. ThentextMatchJsapplies exact or fuzzy matching. - Snapshot references – If the ref maps to a known node, the resolver tries to return the stored
backendNodeId; if not, it falls back to a role/name query on the DOM.
Example Resolutions in Code
// Click a button by CSS
await click('loc=css:.submit-button');
// Click the 3rd occurrence of a text match (zero-based)
await click('text=Next;nth=2');
// Use an ARIA role selector with a name filter
await click('role:button[name="Close"]');
// Click a link pointing to a specific path
await click('href:/settings/account');
// Use a snapshot reference from a prior snapshot
await click('@12'); // where 12 is the snapshot ref
These helpers (click, type, await waitForSelector) internally call resolveElementCenter or resolveElementObjectId, which delegate to all of these resolution strategies.
Geometry and Object-ID Extraction for Every Kind
After a concrete element or AX-node is found, ego-lite must generate either a screen coordinate or a CDP objectId:
Center extraction (resolveElementCenter) uses DOM.getBoxModel for refs and role-based nodes. For CSS/XPath/text locators, it injects a JS snippet that calls getBoundingClientRect() on the matched element and returns the center point {x, y} along with the correct sessionId (which is frame-aware).
Object ID extraction (resolveElementObjectId) likewise either calls DOM.resolveNode when a backend node is known, or returns the direct element reference from the page-side finder JS.
Because every selector format returns the same two output types, conditionals in the higher-level helper routines remain uniform — you can swap css: for role: and the only thing that changes is how the DOM element is found.
Error Classification for Unresolvable Selectors
Unsuccessful lookups can fail with either a transient error or a permanent error, both wrapped in ElementResolutionError. The matchCountKind helper (lines 46–50) infers this by counting how many matches the query returned: zero matches usually means a transient failure (the page may still be loading), while multiple matches or explicit structural problems are considered permanent. This distinction is used by the retry logic in the agent loop.
Key Source Files
| File | Role |
|---|---|
element -resolver.ts |
Parsing and resolution logic for all selector formats, plus AX tree/JS fallback. |
[locator-query.ts](file:///locator-query.ts) |
Builds the queryAllExpression snippet used for CSS, role, text, and href locators. |
[ref-map.ts](file:///ref-map.ts) |
Stores snapshot refs returned by snapshot() so @N can be resolved later. |
Summary
- Ego-lite supports 11 selector formats:
css,xpath,role,text,label,placeholder,alt,title,testid,href, and snapshot refs (@N). - A single parser —
parseLocator— maps every string to a structured locator kind, and a uniform resolution layer then dispatches to CSS, XPath, text-scanning, or AX-tree lookups. - Center and objectId extraction is shared across all kinds, so swapping between CSS and role-based commands never changes your automation logic.
- Error classification (
transientvspermanent) provides retry semantics at the agent layer.
Frequently Asked Questions
Does ego-lite support CSS selectors and XPath in one entry point?
Yes — the loc string format accepts css: and xpath: and also bare selectors. For example, css:.card and loc=css:.card both work, while xpath://form/button is parsed as an XPath kind. The resolver generates the correct in-page JavaScript (querySelector vs document.evaluate) per kind.
How does ego-lite resolve a bare string without any prefix?
If no recognized prefix (like css: or role:) appears and the string is not a @ reference, the parser treats the input as a raw CSS selector. This means click('.submit') works directly.
What is the nth parameter for with text selectors?
The nth parameter is a zero-based index that selects a specific matching element when multiple ones match. It works with CSS, text, role, and href locators. For example, text=Next Next;nth=2` selects the third element whose text matches "Next".
Can role locators be combined with a name filter?
Yes, role selectors accept a name= filter. role:button[name="Close"] means: find a button whose accessible name is "Close" (as exposed in the browser accessibility tree). The naster tree filter is applied at the AX node level, not via DOM attributes, which helps stable resolution even if the DOM structure changes.
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 →