# How page.getByRole, page.getByText, and page.locator.filter Work Together in ego-browser

> Understand how Playwright-style locators like getByRole, getByText, and filter work together in ego-browser. Learn to chain these methods for powerful DOM queries and efficient testing.

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

---

**ego-browser uses a Playwright-style locator façade where `getByRole`, `getByText`, and `filter` are chainable methods that build internal selector strings, later resolved to DOM queries at action time.**

ego-browser implements a unified locator system that helps AI agents interact with web pages using semantic, accessibility-first queries. The three core methods—`page.getByRole`, `page.getByText`, and `page.locator.filter`—share a common architecture that enables composable, chainable element selection without brittle CSS selectors.

## Core Locator Methods Explained

Each method constructs a selector string that gets accumulated and resolved only when an action (like `click()`) is invoked.

### `page.getByRole(role, options?)`

**`getByRole`** initiates a locator matching elements by ARIA role, optionally filtered by accessible name.

According to the ego-browser source code, this method is attached to locator objects via `createLocator()` in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts). It delegates to `roleSelector()`, which produces internal selector strings like:

```javascript
loc=role:button[name="Submit"]

```

This selector format encodes both the role and any name constraints for later resolution.

### `page.getByText(text, options?)`

**`getByText`** creates a locator matching elements by visible text or `aria-label`, supporting exact or partial matching.

Implemented via `textSelector()` in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts), this builds selector strings such as:

```javascript
loc=text:"Save"

```

The `exact` option controls whether the match uses strict equality or substring matching during DOM resolution.

### `page.locator(selector).filter(options?)`

**`filter`** narrows an existing locator using conditions: `has`, `hasNot`, `hasText`, or `hasNotText`.

The `filter()` method (lines 58–60 in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts)) invokes `filterSelector()` to construct nested selector strings. In [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts), this data is transformed into JavaScript expressions that execute in the page context to filter element collections.

## How the Three Methods Compose

The locator system follows a **deferred resolution pattern**: methods build selector strings, and the actual DOM query happens at the final action step.

### Step 1: Locator Creation via `createLocator()`

When you call `page.locator('css=button')`, `createLocator()` (lines 20–31 in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts)) returns a plain JavaScript object exposing chainable methods. Each method returns a **new locator** with an accumulated selector string.

### Step 2: Chaining Builds Nested Selectors

Because every method returns a fresh locator, complex queries compose naturally:

```javascript
const btn = page
  .locator('body')
  .filter({ has: page.getByRole('button') })
  .getByText('Submit')

```

Internally, this produces a nested structure:
- Base: `body`
- Filter: `has: "loc=role:button"`
- Final: `loc=text:"Submit"`

Encoded as: `internal:filter:{base:"body",has:"loc=role:button",...}` → `loc=text:"Submit"`

### Step 3: Resolution at Action Time

When `click()` or another action fires, the accumulated selector string passes to `locator.evaluate...` or `pointer.click`. The runtime—implemented in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) and [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts) (lines 40–44, 139–167)—parses the selector, builds a DOM query using `querySelectorAll`, ARIA role heuristics, and text matching, then applies any `filter` clauses.

## Practical Code Examples

### Basic Role and Text Lookups

```javascript
// Click the first "Submit" button by role
await page.getByRole('button', { name: 'Submit' }).click();

// Find exact text match
await page.getByText('Save', { exact: true }).click();

```

### Combining filter with Role and Text

```javascript
// Find a button within a specific container, then filter by text
const saveBtn = page
  .locator('body')
  .filter({ has: page.getByRole('button') })
  .getByText('Save', { exact: true });

await saveBtn.click();

```

### Complex Descendant Filtering

```javascript
// Require a descendant with specific test ID, then find heading
await page
  .locator('section')
  .filter({ has: page.getByTestId('profile-pic') })
  .getByRole('heading', { name: /profile/i })
  .click();

```

## Key Source Files

| File | Contribution |
|------|--------------|
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Defines `createLocator()`, façade methods (`getByRole`, `getByText`, `filter`), and selector-building helpers (`roleSelector`, `textSelector`, `filterSelector`) |
| [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts) | Parses internal selector strings and builds DOM query expressions, including `filterCondition` handling |
| [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) | Low-level functions that evaluate selectors against page DOM and return matching elements |
| [`src/format.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/format.ts) | Documents public API signatures for `page.getByRole`, `page.getByText`, and `page.locator.filter` |

## Summary

- **`createLocator()`** provides the foundation: a factory that builds chainable locator objects with accumulated selector strings
- **`getByRole`** and **`getByText`** encode accessibility-focused queries into portable selector formats
- **`filter`** adds conditional narrowing using descendant or text constraints
- **Deferred resolution** ensures efficient, single-pass DOM evaluation only when actions execute
- All three methods share the same resolution pipeline in [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts) and [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts), guaranteeing consistent behavior

## Frequently Asked Questions

### How does ego-browser's locator system differ from raw Playwright?

ego-browser implements a **façade pattern** that mirrors Playwright's API but builds internal selector strings rather than native Playwright locators. These strings are resolved through custom DOM evaluation logic in [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts), allowing ego-browser to operate within its agent execution environment while maintaining familiar syntax.

### Can I chain multiple filter() calls together?

Yes. Because `filter()` returns a new locator instance, you can chain multiple filters. Each call wraps the previous selector in a new `internal:filter:` structure. The resolver processes these nested conditions sequentially during DOM evaluation.

### Why use getByRole instead of CSS selectors?

`getByRole` targets **ARIA semantics** rather than implementation details, making tests resilient to DOM structure changes. As implemented in ego-browser's `roleSelector()`, it also supports accessible name matching—something pure CSS cannot express. This aligns with modern accessibility testing practices.

### What happens if a filter condition matches multiple elements?

The filter narrows the parent locator's candidate set to only those elements satisfying the condition. If multiple elements remain, subsequent methods like `click()` typically operate on the **first match** (depending on the action implementation in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts)). For precise targeting, add more specific text or role constraints.