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

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. It delegates to roleSelector(), which produces internal selector strings like:

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, this builds selector strings such as:

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) invokes filterSelector() to construct nested selector strings. In 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) 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:

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 and 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

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

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

// 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 Defines createLocator(), façade methods (getByRole, getByText, filter), and selector-building helpers (roleSelector, textSelector, filterSelector)
src/locator-query.ts Parses internal selector strings and builds DOM query expressions, including filterCondition handling
src/element-resolver.ts Low-level functions that evaluate selectors against page DOM and return matching elements
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 and 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, 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). For precise targeting, add more specific text or role constraints.

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 →