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 – Constructs document.querySelectorAll filters with exact flag handling for substring vs. full-text matching
  • labelElementsExpression – Resolves <label> elements to their associated form controls via htmlFor and implicit association rules
  • roleElementsExpression – Generates accessibility tree queries using roleNameCondition for 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:

  1. ParseparseLocator() identifies role kind, extracts {role: 'button', name: 'Submit'}
  2. TransformroleElementsExpression() builds accessibility query with roleNameCondition
  3. Evaluatelocator.evaluateLocator() runs via CDP, returning matching element's object ID
  4. Actionlocator.click() issues DOM.focusInput.dispatchMouseEvent CDP 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.ts generators.
  • 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:

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 →