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

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 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 through createLocator(selector):

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

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

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

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

// 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
boundingBox() driver/observe.ts
screenshot() driver/observe.ts
count() driver/locator.ts
allInnerTexts() driver/locator.ts

Code Examples: Identical Usage Patterns

The following examples work unchanged across both APIs:

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

// 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 createLocator and page façade L520-L715
src/element-resolver.ts Internal selector → CDP object ID resolution Full file
src/format.ts Public API signature declarations L85-L101
src/driver/locator.ts Low-level CDP element operations Full file
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 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.

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 →