# page.locator() Chainable API and Filtering Capabilities in Ego Browser

> Explore Ego Browser's page locator chainable API and filtering. Discover Playwright-style locators with auto-wait and advanced element narrowing for efficient web automation.

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

---

**The `page.locator()` chainable API and filtering capabilities in Ego Browser provide an immutable, Playwright-style Locator object that auto-waits for DOM attachment and supports advanced element narrowing through methods defined in [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts).**

The `page.locator()` chainable API and filtering capabilities serve as the primary DOM facade in Ego Browser, an open-source automation project maintained by citrolabs. Calling `page.locator(selector)` returns a strict Locator instance that wraps selectors with internal scope or filter rules, ensuring every chainable operation produces a new object without mutating its parent. This design pattern, implemented in [`package/ego-browser/src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts), enables reliable element querying across CSS, XPath, and ARIA role selectors.

## How page.locator() Builds Immutable Locators

In [`package/ego-browser/src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts), the `createLocator` factory (lines 20–62) instantiates every Locator returned by `page.locator()`. Each Locator is **strict**, meaning it automatically waits for the target element to attach to the DOM before resolving actions.

Because every chainable helper invokes an internal wrapper—such as `scopedSelector`, `nthSelector`, or `filterSelector`—the original Locator remains unchanged. This immutability guarantees that intermediate selectors can be safely reused across multiple test branches without side effects.

## Core Chainable Methods in helpers.ts

The Locator API exposes multiple chainable helpers that refine element selection. Each method generates a new selector string and returns a fresh Locator instance:

- **`first()`** – Resolves to the first matching element by wrapping the selector with `nthSelector(selector, 0)` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L23‑L24)).

- **`last()`** – Targets the final match using the internal prefix `internal:last;${selector}` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L24‑L25)).

- **`nth(index)`** – Selects the zero-based index-th element via `nthSelector(selector, index)` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L26‑L31)).

- **`locator(child)`** – Scopes a child selector under the current Locator through `scopedSelector(selector, locatorSelector(child))` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L32‑L34)).

- **`getByRole(role, options)`** – Queries elements by ARIA role, optionally filtered by accessible name, using `roleSelector(role, options)` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L35‑L36)).

- **`getByText(text, options)`** – Matches visible text through `textSelector("text", text, options)` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L37‑L39)).

- **`getByLabel(text, options)`** – Finds form controls associated with a `label` element via `textSelector("label", text, options)` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L40‑L42)).

- **`getByPlaceholder(text, options)`** – Matches `input` or `textarea` placeholder attributes using `textSelector("placeholder", text, options)` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L44‑L46)).

- **`getByAltText(text, options)`** – Targets `img` alt attributes with `textSelector("alt", text, options)` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L48‑L50)).

- **`getByTitle(text, options)`** – Matches elements carrying a `title` attribute through `textSelector("title", text, options)` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L52‑L55)).

- **`getByTestId(testId)`** – Performs an exact match on custom test identifiers using `testIdSelector(testId)` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L56‑L57)).

- **`filter(options)`** – Narrows an existing Locator with additional constraints by invoking `filterSelector(selector, options)` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L58‑L60)).

## Filtering Capabilities with filter()

The `filter(options)` method augments a Locator with predicates that are evaluated when the selector resolves. Internally, the filter data is encoded as an internal selector string of the form `internal:filter:{…}` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L67‑L68)). This encoding allows the same filtering logic to work uniformly for CSS selectors, XPath expressions, and AX-role queries.

### Supported Filter Options

- **`has` / `hasNot`** – Requires or excludes the presence of a descendant that matches a provided Locator or raw selector. The engine converts these values through `locatorSelector` ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L62‑L66)).

- **`hasText` / `hasNotText`** – Asserts or rejects visible text content. String values and regular expressions are transformed into `textMatcher` objects—either `{text, exact:false}` or `{regex,flags}`—depending on the input type ([source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L55‑L60)).

- **Combined predicates** – You can mix `has`, `hasNot`, `hasText`, and `hasNotText` in a single call to express complex constraints, such as requiring a button containing the exact text **Submit** while excluding any subtree that contains an error message.

When the Locator eventually resolves via the driver’s `readQueryAll` or `queryRoleBackendNodeIds`, `filterSelector` applies these constraints on the client side.

## Full Chain Example

The following example demonstrates how to combine scoping, role selection, filtering, and ordinal targeting in one fluent chain:

```javascript
// Find the last visible button inside a dialog that:
//   • has role="button"
//   • contains the exact text "Confirm"
//   • does NOT contain any descendant with text "Error"
await page
  .locator('dialog')
  .getByRole('button')
  .filter({
    hasText: 'Confirm',
    hasNotText: /Error/,
    has: page.locator('svg.icon')   // require an SVG icon child
  })
  .last()
  .click();

```

**Step-by-step resolution:**

1. `page.locator('dialog')` establishes the base selector.
2. `.getByRole('button')` narrows the scope to button descendants using `roleSelector`.
3. `.filter({...})` injects text-matching and child-presence rules via `filterSelector`.
4. `.last()` selects the final matched element with the `internal:last` prefix.
5. `.click()` resolves the entire chain and executes the action.

Each step returns a distinct Locator, so the intermediate instances remain available for reuse elsewhere in your script.

## Key Source Files

The `page.locator()` chainable API and filtering capabilities are defined, documented, and executed across four primary files in the citrolabs/ego-lite repository:

- **[`package/ego-browser/src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts)** – Implements the `createLocator` factory, all chainable methods, and the `filter` encoding logic.
- **[`package/ego-browser/src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/locator.ts)** – Provides low-level CDP helpers that evaluate selectors, including `count` and `evaluateAll`.
- **[`package/ego-browser/src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/locator-query.ts)** – Generates JavaScript expressions for CSS and XPath queries that underpin the filtering engine.
- **[`package/ego-browser/src/format.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/format.ts)** – Documents public API signatures for `page.locator` and its sub-methods, consumed by the built-in `help()` command.

## Summary

- **`page.locator()`** returns a strict, immutable Locator that auto-waits for DOM attachment.
- **Chainable methods** such as `first()`, `last()`, `nth()`, `locator()`, and `getBy*` helpers build new selectors without mutating the original Locator.
- **`filter(options)`** encodes constraints as `internal:filter:{…}` and supports `has`, `hasNot`, `hasText`, and `hasNotText` predicates.
- Filtering logic is applied client-side during resolution via `filterSelector`, ensuring compatibility with CSS, XPath, and ARIA role queries.
- All Locator factory logic resides in [`package/ego-browser/src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts), while execution depends on [`driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/locator.ts) and [`locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/locator-query.ts).

## Frequently Asked Questions

### What makes `page.locator()` chainable in Ego Browser?

The `createLocator` factory in [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts) ensures that every chainable method—such as `filter()` or `getByRole()`—returns a brand-new Locator instance wrapping the previous selector. Because the original object is never mutated, multiple refinement branches can originate from the same base locator safely.

### How does the `filter()` method handle regular expressions?

When `hasText` or `hasNotText` receives a RegExp, the source code converts it into a `textMatcher` object containing `regex` and `flags` properties. This object is then serialized into the internal filter selector so the client-side engine can match visible text against the pattern during resolution.

### Can I reuse a Locator after calling chainable methods like `last()` or `filter()`?

Yes. Immutability is a core design principle of the Ego Browser Locator API. Calling `last()`, `first()`, `nth()`, or `filter()` produces a derived Locator while leaving the parent instance unchanged, enabling safe reuse across different test scenarios.

### Where is the filtering logic executed when a Locator resolves?

According to the citrolabs/ego-lite source code, the `internal:filter:{…}` selector generated by `filterSelector` is interpreted on the client side when the driver invokes `readQueryAll` or `queryRoleBackendNodeIds`. This guarantees consistent behavior regardless of whether the underlying query uses CSS, XPath, or AX-role selectors.