How to Use `page.locator` with Playwright-Style Methods (`first`, `nth`, `last`, `filter`) in ego-browser
Ego-browser exposes a Playwright-like page facade that enables chainable element selection through first(), nth(), last(), and filter() methods, which compose internal selector strings before delegating execution to the Chrome DevTools Protocol (CDP) bridge.
The citrolabs/ego-lite repository provides a lightweight browser automation framework that implements strict, auto-waiting locators compatible with Playwright conventions. Understanding how to use page.locator with Playwright-style methods allows you to write precise element queries that resolve against the DOM through an internal selector composition system.
Understanding the Locator Architecture
The Page Facade Entry Point
In src/helpers.ts, the createPageFacade() function injects the page.locator method that returns a locator object. This facade wraps the raw CDP connection with a high-level API familiar to Playwright users. Each call to page.locator(selector) initializes a chainable object via createLocator(selector), establishing the foundation for method chaining.
Internal Selector Composition
Rather than immediately querying the DOM, locator methods build internal selector strings that encode the selection logic. The createLocator function in src/helpers.ts returns an object whose methods append prefixes to the selector string. These strings remain opaque until the final action triggers query resolution in the driver layer.
Chainable Locator Methods Deep Dive
Targeting First and Last Elements
The first() method invokes nthSelector(selector, 0) to generate an internal:nth=0; prefix, guaranteeing selection of the first matching element in DOM order. Conversely, last() applies the internal:last; prefix to target the final occurrence. Both methods return new locator instances with composed selector strings awaiting resolution.
Zero-Based Indexing with nth()
When you need a specific position, the nth(index) method validates the non-negative integer and produces internal:nth={index};. This supports precise targeting of elements in lists or grids where positional access is required. The zero-based indexing aligns with Playwright conventions, ensuring predictable behavior for developers familiar with that ecosystem.
Refining Selections with filter()
The filter(options) method constructs a JSON description of filtering criteria and encodes it with the internal:filter prefix. You can filter by text content using hasText or by descendant presence using has, which accepts nested locators. This enables complex queries such as locating a parent element that contains a specific child button or visible text pattern.
From Internal Strings to DOM Execution
Query Resolution in locator-query.ts
Before any click or text extraction occurs, the internal selector must expand into executable DOM queries. The src/locator-query.ts module parses these internal prefixes, interpreting internal:nth, internal:last, and internal:filter to build concrete query expressions. This layer handles the translation from the Playwright-like API to actual CSS selectors, XPath expressions, or ARIA role lookups that the browser understands.
Driver Execution via CDP
Once resolved, the concrete selector passes to src/driver/locator.ts, where functions like click, textContent, and evaluateLocator perform the actual Chrome DevTools Protocol operations. The driver resolves ARIA roles through src/element-resolver.ts and executes queryRoleBackendNodeIds or buildQueryAllExpression to interact with the browser's accessibility tree and DOM.
Practical Code Examples
The following patterns demonstrate how to chain page.locator methods in citrolabs/ego-lite:
// Click the first matching button
await page.locator('css=button').first().click();
// Click the third button (zero-based index)
await page.locator('css=button').nth(2).click();
// Click the last matching link
await page.locator('css=a').last().click();
Filter methods enable complex conditional selection:
// Filter elements that contain the text "Submit"
await page
.locator('css=form')
.filter({ hasText: 'Submit' })
.click();
// Combine filters with role selectors
await page
.locator('css=section')
.filter({ has: page.getByRole('button', { name: /confirm/i }) })
.click();
Advanced chaining combines multiple strategies:
// Use getByRole and then pick the first matching element
await page
.getByRole('button', { name: 'Save' })
.first()
.click();
// Chain locator methods for complex hierarchical queries
await page
.locator('css=ul.todo-list')
.filter({ has: page.getByText('Urgent') })
.nth(1) // second matching list item
.click();
Summary
- Internal selector composition: Methods like
first(),nth(),last(), andfilter()insrc/helpers.tsgenerate prefixed selector strings (e.g.,internal:nth=0;) rather than querying the DOM immediately. - Playwright compatibility: The API mirrors Playwright's ergonomics using zero-based indexing and filter objects with
hasTextorhasproperties. - CDP-based execution: Final actions delegate to
src/driver/locator.ts, which resolves selectors throughsrc/locator-query.tsand executes Chrome DevTools Protocol commands. - Strict auto-waiting: The locator architecture automatically waits for elements to match the composed selector criteria before performing actions.
Frequently Asked Questions
How does ego-browser's locator differ from actual Playwright?
While the API surface mimics Playwright's chainable locator pattern, ego-browser operates through a CDP bridge that composes internal selector strings before execution. According to the source code in citrolabs/ego-lite, the actual DOM queries occur in locator-query.ts and driver/locator.ts rather than through Playwright's native browser bindings.
Can I combine multiple filter conditions in a single locator chain?
Yes, you can chain .filter() calls or combine them with positional methods like .nth(). The filter method builds a JSON description encoded with internal:filter prefixes, and locator-query.ts parses these to construct the final query expression that respects all conditions.
What happens if nth() receives a negative index or invalid input?
The nth(index) method in src/helpers.ts validates that the provided index is a non-negative integer. Invalid inputs are rejected during the selector composition phase before any CDP communication occurs, ensuring type safety in the internal selector generation.
Does the last() method support filtering before selecting the final element?
Yes, because last() returns a new locator instance with the internal:last; prefix, you can precede it with .filter() in the chain. The filter applies first, narrowing the candidate set, and then last() selects the final element from those filtered results.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →