# Locator Facade and Selector Transformation System in ego-browser: Architecture Deep Dive

> Explore the ego-browser locator facade architecture. Learn how CSS, XPath, text, and role selectors are transformed into JavaScript queries for resilient element interactions via CDP.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: architecture
- Published: 2026-08-06

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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:

```javascript
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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) creates chainable locator objects:

```javascript
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:

```javascript
// 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:

```javascript
await page.locator('role=button[name=Submit]').click();

```

The system executes this sequence:

1. **Parse** – `parseLocator()` identifies `role` kind, extracts `{role: 'button', name: 'Submit'}`
2. **Transform** – `roleElementsExpression()` builds accessibility query with `roleNameCondition`
3. **Evaluate** – `locator.evaluateLocator()` runs via CDP, returning matching element's object ID
4. **Action** – `locator.click()` issues `DOM.focus` → `Input.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`](https://github.com/citrolabs/ego-lite/blob/main/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

```javascript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) | `parseLocator()`, `createLocator()`, `scopedSelector()` |
| Selector-to-JS transformation | [`package/ego-browser/src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/locator-query.ts) | `textElementsExpression()`, `roleElementsExpression()`, `labelElementsExpression()` |
| CDP driver actions | [`package/ego-browser/src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/locator.ts) | `evaluateLocator()`, `click()`, `fill()`, DOM interaction primitives |
| Element resolution utilities | [`package/ego-browser/src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts) | Accessibility tree traversal, label resolution |
| Snapshot reference management | [`package/ego-browser/src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-map.ts) | Ref-to-node mapping, automatic re-snapshot triggers |
| Public API documentation | [`package/ego-browser/src/format.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) and routed through appropriate transformation generators in [`package/ego-browser/src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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.