ego-browser Element Locator Strategies: XPath, ARIA Roles, Text Matchers, and More

ego-browser supports over a dozen element locator strategies beyond CSS selectors, including XPath expressions, semantic text matchers, ARIA role queries, and internal scoping filters, all parsed in src/locator-query.ts.

The ego-browser library (part of the citrolabs/ego-lite ecosystem) provides a robust element resolution system that extends far beyond standard CSS selectors. When you call page.locator() or any getBy... helper, the runtime parses your query string using a sophisticated engine defined in src/locator-query.ts (lines 47–114) to determine which DOM traversal strategy to execute. Understanding these element locator strategies allows you to write more resilient and accessible browser automation scripts.

Core Locator Syntaxes

The parser in src/locator-query.ts inspects selector strings for specific prefixes to determine the resolution strategy. If no prefix matches, the engine falls back to standard querySelectorAll behavior at line 116.

XPath Expressions

Use the xpath= prefix to evaluate full XPath expressions via document.evaluate.

await page.locator('xpath=//button[contains(., "Submit")]').click();

Explicit CSS Prefixing

While raw CSS selectors work by default, you can explicitly prefix them with loc=css: for clarity or programmatic construction.

URL-Based Matching

The loc=href: strategy targets anchor elements whose resolved href attribute matches a specific path or URL.

await page.locator('loc=href:/products/123').click();

Semantic and Accessible Locators

These strategies prioritize user-facing attributes and accessibility properties over implementation details, reducing test fragility.

Text Content Matching

Match elements based on visible text content using either fuzzy or exact matching syntax.

Fuzzy matching uses the loc=text: prefix:

await page.locator('loc=text:Welcome').waitFor();

Exact matching uses the text= syntax:

await page.locator('text="Exact title"').click();

Label Association

Queries for <label> elements or associated form controls using accessible label text. The page.getByLabel() helper constructs a loc=label: prefixed string internally.

await page.getByLabel('Email address').fill('user@example.com');

Placeholder Attributes

Targets <input> or <textarea> elements by their placeholder text via the loc=placeholder: prefix.

await page.getByPlaceholder('Search…').type('ego-browser');

Alt Text and Title Attributes

Match images and interactive elements by descriptive attributes using loc=alt: and loc=title: prefixes.

await page.getByAltText('Company logo').click();
await page.getByTitle('Help').click();

Test ID Attributes

Locate elements by the data-testid attribute using the loc=testid: prefix, a common pattern for test automation stability.

await page.getByTestId('submit-button').click();

ARIA Roles

Select elements by their explicit or implicit ARIA role using loc=role:, optionally filtering by accessible name.

await page.getByRole('button', { name: /confirm/i }).click();

Advanced Composition Strategies

For complex DOM hierarchies, ego-browser provides internal scoping and filtering mechanisms defined in src/locator-query.ts that operate on intermediate result sets.

Internal Scoping

The internal:scope: compound selector first resolves a base selector, then searches for child elements within each base context. This is useful for nested component structures.

// Find all <section> elements, then within each, find the first <h2>
await page.locator('internal:scope:{"base":"section","child":"loc=css:h2"}').first().click();

Internal Filtering

Apply additional constraints such as hasText, hasNotText, has, or hasNot to refine a base selector without additional round-trips.

await page.locator('internal:filter:{"base":"div.item","hasText":{"text":"Active"}}').count();

Nth-Element Selection

Retrieve specific indices from a result set using internal:nth= or grab the final element with internal:last to target specific positions in a list.

Implementation Architecture

The locator system spans several key files in the citrolabs/ego-lite repository:

  • src/locator-query.ts: Core parser that translates prefixed strings into executable DOM query plans (lines 47–114).
  • src/helpers.ts: Exposes public helper methods including locator(), getByRole(), getByText(), and others that construct the prefixed selector strings internally.
  • src/driver/locator.ts: Low-level driver interface that receives resolved selector strings and coordinates execution.
  • src/element-resolver.ts: Executes the final query expressions against the DOM, handling the actual DOM traversal and element resolution.

Summary

  • ego-browser supports 12+ distinct locator strategies ranging from XPath to ARIA roles, all managed via src/locator-query.ts.
  • Accessibility-first selectors (label, placeholder, alt text, title) reduce reliance on fragile CSS classes or IDs.
  • Internal scoping and filtering enable complex hierarchical queries without multiple round-trips to the browser.
  • The parser falls back to standard querySelectorAll at line 116 when no special prefix is detected, ensuring backward compatibility with standard CSS selectors.

Frequently Asked Questions

How do I use XPath selectors in ego-browser?

Prefix your selector string with xpath= followed by a valid XPath expression. The engine uses document.evaluate to resolve these queries. For example: await page.locator('xpath=//div[@class="content"]').click();.

What is the difference between loc=text: and text= syntax?

The loc=text: prefix performs fuzzy text matching, finding elements that contain the specified text substring. The text= syntax (without the loc= prefix) performs exact string matching, requiring the element's full text content to match precisely, and is typically used with quoted values like text="Exact String".

Can I combine multiple locator strategies in a single query?

Yes, using the internal:scope: and internal:filter: compound selectors. The scoping strategy allows you to nest selectors (finding child elements within parent contexts), while the filtering strategy applies constraints like hasText or hasNot to refine results from a base selector.

What happens if my selector prefix is not recognized?

If the parser in src/locator-query.ts does not recognize a prefix (lines 47–114), the selector falls back to a standard querySelectorAll call at line 116. This means unprefixed strings are treated as standard CSS selectors, maintaining compatibility with traditional browser automation patterns.

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 →