# How to Use `page.locator` with Playwright-Style Methods (`first`, `nth`, `last`, `filter`) in ego-browser

> Master ego-browser's page.locator with Playwright-style methods like first, nth, last, and filter for efficient DOM element selection and testing.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: how-to-guide
- Published: 2026-07-26

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/locator-query.ts)

Before any click or text extraction occurs, the internal selector must expand into executable DOM queries. The [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`:

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

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

```javascript
// 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()`, and `filter()` in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) generate 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 `hasText` or `has` properties.
- **CDP-based execution**: Final actions delegate to [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts), which resolves selectors through [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts) and 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`](https://github.com/citrolabs/ego-lite/blob/main/locator-query.ts) and [`driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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.