# How the `page.locator()` Facade Compares to Playwright's Locator API: A Complete Technical Breakdown

> Compare page.locator() facade to Playwright's Locator API. ego-lite uses a custom CDP-based implementation for robust automation. Learn the technical differences.

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

---

**The `page.locator()` facade in ego-lite mirrors Playwright's Locator API ergonomically while routing all operations through a custom CDP-based implementation using internal selector strings.**

The `ego-browser` package from [citrolabs/ego-lite](https://github.com/citrolabs/ego-lite) provides a Playwright-compatible page façade designed for sandboxed browser automation. This article examines how `page.locator()` compares to Playwright's native Locator API, with direct reference to the source implementation.

## Core Architecture Comparison

Both APIs present similar developer-facing interfaces, but differ fundamentally in their execution backends.

### Locator Creation and Selector Storage

**Playwright** creates locator objects via `page.locator(selector)`, storing the original selector for lazy evaluation.

**ego-lite** implements equivalent behavior in [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts) through `createLocator(selector)`:

```typescript
// helpers.ts L520-L610
const locator = page.locator('.submit-button');

```

The returned object holds the raw selector string and builds **internal selector strings** for chainable operations. These internal selectors use prefixes like `internal:nth=`, `internal:scope`, and `loc:role:` to encode targeting logic.

### Auto-Waiting Behavior

Playwright's locators automatically wait for elements to be **stable, visible, and enabled** before executing actions.

ego-lite achieves identical semantics through `waits.waitForSelector` in [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts#L609-L610):

```typescript
// Every action ends with this wait helper
await waits.waitForSelector(internalSelector, { visible: true });

```

This guarantees the same auto-wait behavior before any CDP operation executes.

### Strictness and Disambiguation

| Feature | Playwright | ego-lite Implementation |
|---------|-----------|------------------------|
| Default behavior | Strict (single match required) | Strict matching enforced |
| First match | `.first()` | `first()` prepends `internal:nth=0` |
| Last match | `.last()` | `last()` prepends `internal:last` |
| Indexed match | `.nth(n)` | `nth(index)` prepends `internal:nth=${index}` |

The disambiguation helpers are defined at [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts#L522-L531):

```typescript
// helpers.ts L522-L531
first: () => createLocator(`internal:nth=0,${selector}`),
last: () => createLocator(`internal:last,${selector}`),
nth: (index: number) => createLocator(`internal:nth=${index},${selector}`)

```

## Filtering and Chaining Methods

Both APIs support identical chaining patterns for refined element targeting.

### Available Chainable Operations

ego-lite mirrors Playwright's filtering API through these [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts) implementations (L532-L547):

- **`filter(options)`** — narrows by `hasText` or visibility constraints
- **`locator(child)`** — creates scoped child locators
- **`getByRole(role, options)`** — role-based selection with name filtering
- **`getByText(text)`** — text content matching
- **`getByLabel(text)`** — form label association

These build upon internal selector language functions: `scopedSelector`, `textSelector`, and `roleSelector`.

### Convenience Shortcuts on Page

Playwright exposes `page.getByText()`, `page.getByRole()` as wrappers around `page.locator()`.

ego-lite provides identical shortcuts directly on the page façade at [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts#L708-L715):

```typescript
// helpers.ts L708-L715
page.getByRole = (role, options) => createLocator(roleSelector(role, options));
page.getByText = (text, options) => createLocator(textSelector(text, options));
page.getByLabel = (text) => createLocator(labelSelector(text));

```

## Evaluation and Utility Methods

### In-Browser Script Execution

**Playwright**: `locator.evaluate(pageFn)` and `locator.evaluateAll(pageFn)`

**ego-lite**: `locator.evaluateLocator` and `locator.evaluateAll` at [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts#L605-L608):

```typescript
// helpers.ts L605-L608
evaluateLocator: async (pageFunction, arg) => {
  return cdpEvaluator.evaluateOnSelector(internalSelector, pageFunction, arg);
}

```

### Additional Helper Methods

ego-lite implements the full utility suite found in Playwright by delegating to **driver/locator.ts** and **driver/observe.ts**:

| Method | CDP Implementation Target |
|--------|--------------------------|
| `innerHTML()` | [`driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/locator.ts) |
| `boundingBox()` | [`driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/observe.ts) |
| `screenshot()` | [`driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/observe.ts) |
| `count()` | [`driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/locator.ts) |
| `allInnerTexts()` | [`driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/locator.ts) |

## Code Examples: Identical Usage Patterns

The following examples work unchanged across both APIs:

```typescript
// Basic click action with auto-wait
await page.locator('button[type=submit]').click();

// Role-based locator with regex name matching
await page.getByRole('button', { name: /Submit/i }).click();

// Disambiguating multiple matches
await page.locator('li.item').nth(2).hover();
await page.locator('ul > li').first().click();

// Scoped child selection
const card = page.locator('.card');
await card.locator('.title').innerText();

// Text-based filtering
await page.locator('.message').filter({ hasText: 'Error' }).isVisible();

// Custom browser-side evaluation
const rect = await page.locator('#banner').evaluate((el) => el.getBoundingClientRect());

// Element screenshot
await page.locator('#logo').screenshot({ path: 'logo.png' });

```

## API Signature Documentation

Playwright generates documentation from JSDoc annotations.

ego-lite defines canonical signatures in **format.ts** for knowledge-base generation:

```typescript
// format.ts L85-L101
"page.locator(selector) => Locator",
"locator.click(options?) => Promise<void>",
"locator.fill(value, options?) => Promise<void>",
"locator.evaluate(pageFunction, arg?) => Promise<Serializable>",

```

These signatures drive the built-in `help()` output and agent-facing documentation.

## Implementation Files Reference

| File | Responsibility | Key Lines |
|------|--------------|-----------|
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | `createLocator` and page façade | L520-L715 |
| [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) | Internal selector → CDP object ID resolution | Full file |
| [`src/format.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/format.ts) | Public API signature declarations | L85-L101 |
| [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts) | Low-level CDP element operations | Full file |
| [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts) | Screenshot, bounding box, visibility checks | Full file |

## Summary

- **API surface**: ego-lite's `page.locator()` facade replicates Playwright's Locator API with identical method names, signatures, and chaining patterns
- **Implementation**: All operations translate to **internal selector strings** resolved through CDP rather than Playwright's protocol
- **Behavioral parity**: Auto-waiting, strictness, and disambiguation work identically via `waits.waitForSelector` and internal selector prefixes
- **Extensibility**: The internal selector language (`internal:`, `loc:`, `role:`) enables custom functionality while preserving familiar ergonomics

## Frequently Asked Questions

### Is the `page.locator()` facade a full Playwright replacement?

No. It implements the most-used Locator API methods for sandboxed browser automation, but does not include Playwright's full feature set (network interception, multiple contexts, mobile emulation). The design prioritizes API familiarity for agents already trained on Playwright patterns.

### How does internal selector resolution actually work?

The [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts) module parses internal selectors like `internal:nth=2,.item` or `loc:role:button,name=Submit` and translates them to CDP Runtime or DOM calls. This happens transparently; developers interact only with the Playwright-style façade.

### Can I mix Playwright and ego-lite code in the same project?

Only at the orchestration level. The APIs are compatible in usage patterns but not interoperable—Playwright locators cannot target ego-lite browser instances and vice versa. Choose one based on deployment requirements: full Playwright for local/CI environments, ego-lite for sandboxed remote execution.

### What happens when a selector matches multiple elements?

By default, both APIs throw a strict mode violation. Use `.first()`, `.last()`, or `.nth(index)` to disambiguate, or `.filter()` to narrow the match set. ego-lite enforces this at the internal selector level before any CDP call executes.