Locator Facade and Selector Transformation System in ego-browser: Architecture Deep Dive
The ego-browser locator system provides a Playwright-style facade that parses CSS, XPath, text, role, and snapshot-ref selectors, transforms them into executable JavaScript queries, and resolves them via Chrome DevTools Protocol (CDP) for auto-waiting, resilient element interactions.
This article analyzes the architecture of citrolabs/ego-lite's browser automation layer, focusing on how the locator facade abstracts raw selector complexity and how the selector transformation system converts high-level queries into safe, evaluated browser code.
Locator Parsing and Normalization
All selector strings enter through parseLocator() in package/ego-browser/src/helpers.ts. This parser recognizes six distinct kinds of locators:
| Kind | Syntax | Purpose |
|---|---|---|
css |
"div > span" |
Standard CSS selector |
xpath |
"//button" |
XPath 1.0 expression |
text |
"text=Submit" |
Visible text matching |
role |
"role=button[name=Close]" |
Accessibility role with optional name |
ref |
"@42" |
Stable backend node reference (snapshot ID) |
loc= |
"loc=css:.item" |
Explicit locator type hint |
The parser returns a structured object that downstream components consume uniformly. For example, role=button[name=Submit] parses to { kind: 'role', role: 'button', name: 'Submit' }.
Selector Transformation Pipeline
The selector-to-JavaScript transformation layer lives in package/ego-browser/src/locator-query.ts. Here, parsed locators become executable browser expressions through specialized generators:
textElementsExpression– Constructsdocument.querySelectorAllfilters withexactflag handling for substring vs. full-text matchinglabelElementsExpression– Resolves<label>elements to their associated form controls viahtmlForand implicit association rulesroleElementsExpression– Generates accessibility tree queries usingroleNameConditionfor combined role + accessible name matching
Each generator outputs a string like:
Array.from(document.querySelectorAll('button')).filter(el =>
el.textContent.trim() === "Submit"
)
The resulting expression is evaluated via cdp.evaluate() in package/ego-browser/src/driver/locator.ts, returning object IDs or backendNodeIds for matched elements.
Facade Construction and Method Chaining
The facade factory in package/ego-browser/src/helpers.ts creates chainable locator objects:
function createLocator(selector) {
return {
nth: (i) => createLocator(scopedSelector(selector, locatorSelector({ nth: i }))),
first: () => createLocator(scopedSelector(selector, locatorSelector({ nth: 0 }))),
last: () => createLocator(scopedSelector(selector, locatorSelector({ nth: -1 }))),
filter: (has) => createLocator(scopedSelector(selector, locatorSelector({ has }))),
click: () => locator.click(selector),
fill: (value) => locator.fill(selector, value),
textContent: () => locator.textContent(selector),
innerText: () => locator.innerText(selector),
isVisible: () => locator.isVisible(selector),
isEnabled: () => locator.isEnabled(selector),
};
}
The scopedSelector helper composes base selectors with qualifiers (has, hasNot, nth, hasText). This enables complex queries like:
// Scoped: button inside a specific form, nth match, with text filter
page.locator('form#login').locator('button').nth(0).filter({ hasText: 'Sign' })
End-to-End Interaction Flow
When an agent executes:
await page.locator('role=button[name=Submit]').click();
The system executes this sequence:
- Parse –
parseLocator()identifiesrolekind, extracts{role: 'button', name: 'Submit'} - Transform –
roleElementsExpression()builds accessibility query withroleNameCondition - Evaluate –
locator.evaluateLocator()runs via CDP, returning matching element's object ID - Action –
locator.click()issuesDOM.focus→Input.dispatchMouseEventCDP commands
Each facade method performs a fresh resolve, eliminating stale element references through automatic re-evaluation.
Stable vs. Transient Selectors
The selector transformation system distinguishes between two resolution strategies:
| Type | Characteristic | Re-resolution Behavior |
|---|---|---|
| Stable selectors | CSS, XPath, text, role | Recomputed on every call; resilient to DOM changes |
| Transient refs | Snapshot refs (@42) |
Triggers automatic re-snapshot when ref map expires |
Transient reference handling resides in package/ego-browser/src/ref-map.ts. When a snapshot ref like @23 is used, the system checks the ref-to-node mapping. If missing, it automatically captures a new DOM snapshot to restore continuity—critical for multi-round CLI interactions where page state may have changed.
Code Examples: Selector Patterns in Practice
// 1. CSS locator with method chaining
const saveButton = page.locator('button[type="submit"].primary');
await saveButton.click();
// 2. Text locator with exact matching
const welcomeMessage = page.locator('text=Welcome back, Alice');
console.log(await welcomeMessage.innerText());
// 3. Role-based locator with accessible name
await page.locator('role=navigation[name="Main"]').isVisible();
// 4. Complex scoped locator
const firstUnread = page.locator('.message-list')
.locator('.message.unread')
.nth(0);
await firstUnread.hover();
// 5. Snapshot reference for stable automation
// Automatically re-snapshots if @23 expires
await page.locator('@23').fill('updated value');
Key Source Files and Responsibilities
| Component | File Path | Responsibility |
|---|---|---|
| Locator parsing & facade factory | package/ego-browser/src/helpers.ts |
parseLocator(), createLocator(), scopedSelector() |
| Selector-to-JS transformation | package/ego-browser/src/locator-query.ts |
textElementsExpression(), roleElementsExpression(), labelElementsExpression() |
| CDP driver actions | package/ego-browser/src/driver/locator.ts |
evaluateLocator(), click(), fill(), DOM interaction primitives |
| Element resolution utilities | package/ego-browser/src/element-resolver.ts |
Accessibility tree traversal, label resolution |
| Snapshot reference management | package/ego-browser/src/ref-map.ts |
Ref-to-node mapping, automatic re-snapshot triggers |
| Public API documentation | package/ego-browser/src/format.ts |
JSDoc signatures, help text for getByRole, getByText, et al. |
Summary
- The locator facade in ego-browser exposes Playwright-compatible methods through
page.locator(), hiding CDP complexity behind a declarative API. - Selector transformation converts six locator kinds into safe, evaluated JavaScript via
package/ego-browser/src/locator-query.tsgenerators. - Fresh resolution on every action eliminates stale element references without manual retry logic.
- Scoped selectors and method chaining enable precise element targeting without fragile XPath construction.
- Automatic re-snapshotting of transient refs (
@N) maintains automation continuity across CLI rounds.
Frequently Asked Questions
What selector types does ego-browser's locator facade support?
The facade supports CSS selectors, XPath expressions, text matching, accessibility role queries, snapshot references (@N), and explicit loc= prefixed selectors. Each type is parsed by parseLocator() in package/ego-browser/src/helpers.ts and routed through appropriate transformation generators in package/ego-browser/src/locator-query.ts.
How does the selector transformation system handle text matching?
The textElementsExpression() function in package/ego-browser/src/locator-query.ts builds a JavaScript filter expression applied to document.querySelectorAll('*') results. It respects the exact flag to distinguish between substring and full-text equality matching, then evaluates the generated code via CDP's runtime protocol.
Why do locator methods re-resolve elements on every call?
Fresh resolution prevents stale element reference errors common in dynamic web applications. Instead of caching DOM node pointers, each facade method rebuilds and re-evaluates the selector expression through locator.evaluateLocator(), guaranteeing the matched element exists in the current DOM state before interaction.
What happens when a snapshot reference like @42 becomes invalid?
The ref-map system in package/ego-browser/src/ref-map.ts detects missing mappings and triggers automatic re-snapshotting. This captures the current DOM state, regenerates stable references, and allows the automation to continue without manual intervention—essential for long-running agent sessions where pages mutate between commands.
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 →