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 byhasTextor visibility constraintslocator(child)— creates scoped child locatorsgetByRole(role, options)— role-based selection with name filteringgetByText(text)— text content matchinggetByLabel(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.waitForSelectorand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →