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

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.

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, enables reliable element querying across CSS, XPath, and ARIA role selectors.

How page.locator() Builds Immutable Locators

In 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).

  • last() – Targets the final match using the internal prefix internal:last;${selector} (source).

  • nth(index) – Selects the zero-based index-th element via nthSelector(selector, index) (source).

  • locator(child) – Scopes a child selector under the current Locator through scopedSelector(selector, locatorSelector(child)) (source).

  • getByRole(role, options) – Queries elements by ARIA role, optionally filtered by accessible name, using roleSelector(role, options) (source).

  • getByText(text, options) – Matches visible text through textSelector("text", text, options) (source).

  • getByLabel(text, options) – Finds form controls associated with a label element via textSelector("label", text, options) (source).

  • getByPlaceholder(text, options) – Matches input or textarea placeholder attributes using textSelector("placeholder", text, options) (source).

  • getByAltText(text, options) – Targets img alt attributes with textSelector("alt", text, options) (source).

  • getByTitle(text, options) – Matches elements carrying a title attribute through textSelector("title", text, options) (source).

  • getByTestId(testId) – Performs an exact match on custom test identifiers using testIdSelector(testId) (source).

  • filter(options) – Narrows an existing Locator with additional constraints by invoking filterSelector(selector, options) (source).

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). 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).

  • 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).

  • 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:

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

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, while execution depends on driver/locator.ts and locator-query.ts.

Frequently Asked Questions

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

The createLocator factory in 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.

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 →