# How the Ego-Browser Locator Facade API Supports Chained Calls: Immutable Selector Composition Explained

> Discover how the ego-browser locator facade API enables chained calls via immutable selector composition. Learn to build dynamic selectors efficiently.

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

---

**The ego-browser locator facade supports chained calls through immutable selector composition, where every method returns a new façade instance with an updated selector string rather than mutating the original object.**

The **ego-lite** repository provides a lightweight browser automation framework with a Playwright-inspired API. Its **locator façade** enables expressive element selection through method chaining, making test scripts readable and maintainable. This article explains the architectural patterns that make this chaining possible, drawn directly from the citrolabs/ego-lite source code.

## Immutable Façade Design Pattern

The core mechanism resides in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts), where `createLocator` (lines 20-31) implements a factory that returns plain objects with chainable methods. Each method follows the same contract: **validate inputs, compose a new selector, return a new façade**.

```js
// From src/helpers.ts - simplified core pattern
function createLocator(selector) {
  return {
    // Every method returns a NEW createLocator call
    nth(index) {
      const newSelector = nthSelector(selector, index);
      return createLocator(newSelector);  // ← fresh instance
    },
    locator(subSelector) {
      const newSelector = scopedSelector(selector, subSelector);
      return createLocator(newSelector);  // ← fresh instance
    },
    // ... additional chainable methods
  };
}

```

This **pure functional approach** eliminates hidden state. The original `selector` string captured in closure remains untouched, while each chain step accumulates complexity in a new string passed to the next instance.

## Selector Composition Through Internal Pseudo-Selectors

The façade encodes chain operations as **internal pseudo-selectors** prefixed to the selector string. Helper functions in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) handle this encoding:

| Helper Function | Lines | Purpose |
|-----------------|-------|---------|
| `nthSelector` | 13-15 | Wraps selector with `internal:nth=N;` prefix |
| `scopedSelector` | 31-58 | Combines parent and child selectors with semicolon delimiter |

```js
// Selector evolution through a chain
'#container'                          // initial locator
'internal:nth=2;#container'           // after .nth(2)
'internal:nth=2;#container;button'    // after .locator('button')

```

The semicolon-delimited format allows the query parser in [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts) to split and interpret each segment, executing the operations in order against the DOM.

## Chainable Methods in the Façade API

The ego-browser locator façade mirrors Playwright's **locator API**, exposing methods that fall into three categories:

**Navigation methods** (return new façades):
- `first()` → prefixes with `internal:last;` (interpreted as first match)
- `nth(index)` → validates with `Number.isInteger` and non-negative check
- `locator(selector)` → scopes to descendant elements
- `filter(options)` → applies attribute or structural filters
- `getByText(text)`, `getByRole(role, options)` → semantic selectors

**Action methods** (terminal, execute via driver):
- `click()`, `fill(value)`, `evaluate(fn)`, `textContent()`, etc.

**Property methods** (terminal, return values):
- `count()`, `isVisible()`, `boundingBox()`, etc.

Early validation occurs before façade creation. For example, `nth()` rejects non-integers immediately rather than deferring errors to execution time.

## Driver Resolution of Composed Selectors

The accumulated selector string ultimately reaches [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts), which coordinates with [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts) to execute queries. The flow:

1. **Parse** the semicolon-delimited selector into operation segments
2. **Translate** pseudo-selectors (`internal:*`) to executable JavaScript
3. **Execute** against Chrome DevTools Protocol (CDP) for element resolution
4. **Apply** the terminal action or return the requested property

Because the façade has already composed the full selector by chain's end, the driver treats the query as a **single deterministic operation** rather than multiple round-trip selections.

## Practical Chaining Examples

```js
// Example 1: Index-based access within a container
await page
  .locator('#product-list')
  .nth(2)                    // third item (zero-indexed)
  .locator('.add-to-cart')
  .click();

// Example 2: Filtering before interaction
await page
  .locator('button')
  .filter({ has: { attribute: 'data-testid', value: 'submit' } })
  .first()
  .click();

// Example 3: Deep semantic chaining
await page
  .getByRole('navigation')
  .getByRole('link', { name: 'Settings' })
  .click();

// Example 4: Complex multi-step selection
await page
  .locator('form#login')
  .locator('input')
  .filter({ has: { attribute: 'type', value: 'password' } })
  .fill('securePassword123');

```

Each intermediate façade is discarded after its successor is created. The garbage collector reclaims these short-lived objects, while only the final façade participates in the driver call.

## Comparison with Mutative Alternatives

| Approach | Pattern | Drawback Addressed by Ego-Browser |
|----------|---------|-----------------------------------|
| **Mutative faсade** | `loc.first().nth(2)` modifies internal state | Race conditions in async code, unexpected shared state |
| **Builder with `build()`** | `new Locator().first().nth(2).build()` | Verbose API, easy to forget terminal call |
| **Ego-browser's immutable façade** | Each step returns ready-to-use instance | Clean API, safe reassignment, predictable behavior |

The immutable approach enables **reusable base locators**:

```js
// Define once
const formLocator = page.locator('#user-form');

// Branch to different fields without interference
await formLocator.locator('input[name="email"]').fill('test@example.com');
await formLocator.locator('input[name="phone"]').fill('555-0123');
// formLocator remains unmodified

```

## Source File Reference Map

| File | Responsibility |
|------|--------------|
| [`package/ego-browser/src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) | `createLocator` factory and selector helpers (lines 20-31) |
| [`package/ego-browser/src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/locator.ts) | CDP driver for selector resolution |
| [`package/ego-browser/src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/locator-query.ts) | Query generation for CSS/XPath/Accessibility Tree |
| [`package/ego-browser/src/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/index.ts) | Public API export (`page.locator`) |
| `package/ego-browser/src/helpers.test.mjs` | Test coverage for chaining behavior |

## Summary

- The **ego-browser locator facade API** enables chained calls through **immutable selector composition**, where every method returns a new façade instance.
- The `createLocator` factory in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) implements this pattern using closure-captured selector strings and fresh object returns.
- **Internal pseudo-selectors** (`internal:nth=`, `internal:last;`) encode chain operations in a parseable string format.
- Early validation in methods like `nth()` provides immediate feedback on incorrect arguments.
- Terminal methods delegate to [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts) and [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts) for CDP-based element resolution and action execution.
- This design yields a **pure, stateless API** compatible with Playwright-style automation patterns.

## Frequently Asked Questions

### What makes the ego-browser locator façade immutable?

Each public method creates a **new façade instance** via `createLocator()` rather than modifying the original object's properties. The selector string is captured in closure and never reassigned; instead, helper functions generate updated strings for the next instance. This immutability prevents side effects when locators are reused or shared across async contexts.

### How does the chaining syntax avoid mutating the original locator?

The `createLocator` factory returns a plain object whose methods immediately call `createLocator` again with modified arguments. For example, calling `.nth(2)` invokes `nthSelector()` to build a prefixed string, then `createLocator(newSelector)` produces a fresh object. The original object and its selector remain unchanged in memory, though no longer referenced in the chain.

### What happens if I call `.nth()` with an invalid argument?

The façade validates numeric arguments before creating any new instance. In `nth()` and similar methods, `Number.isInteger(index)` and non-negative checks execute immediately; if validation fails, the method throws without invoking `createLocator`. This **fail-fast behavior** surfaces errors at call site rather than during later driver execution.

### Can I reuse a base locator after chaining from it?

Yes—this is a key benefit of immutability. Since chained methods never modify the source façade, you can declare a base locator and branch multiple independent queries from it. Each branch receives its own selector string and façade instance, while the original remains available for additional chains or reuse in subsequent test steps.