page.locator() Chainable API and Filtering Capabilities in Ego Browser
The page.locator() chainable API and filtering capabilities in Ego Browser provide an immutable, Playwright-style Locator object that auto-waits for DOM attachment and supports advanced element narrowing through methods defined in helpers.ts.
The page.locator() chainable API and filtering capabilities serve as the primary DOM facade in Ego Browser, an open-source automation project maintained by citrolabs. Calling page.locator(selector) returns a strict Locator instance that wraps selectors with internal scope or filter rules, ensuring every chainable operation produces a new object without mutating its parent. This design pattern, implemented in package/ego-browser/src/helpers.ts, enables reliable element querying across CSS, XPath, and ARIA role selectors.
How page.locator() Builds Immutable Locators
In package/ego-browser/src/helpers.ts, the createLocator factory (lines 20–62) instantiates every Locator returned by page.locator(). Each Locator is strict, meaning it automatically waits for the target element to attach to the DOM before resolving actions.
Because every chainable helper invokes an internal wrapper—such as scopedSelector, nthSelector, or filterSelector—the original Locator remains unchanged. This immutability guarantees that intermediate selectors can be safely reused across multiple test branches without side effects.
Core Chainable Methods in helpers.ts
The Locator API exposes multiple chainable helpers that refine element selection. Each method generates a new selector string and returns a fresh Locator instance:
-
first()– Resolves to the first matching element by wrapping the selector withnthSelector(selector, 0)(source). -
last()– Targets the final match using the internal prefixinternal:last;${selector}(source). -
nth(index)– Selects the zero-based index-th element vianthSelector(selector, index)(source). -
locator(child)– Scopes a child selector under the current Locator throughscopedSelector(selector, locatorSelector(child))(source). -
getByRole(role, options)– Queries elements by ARIA role, optionally filtered by accessible name, usingroleSelector(role, options)(source). -
getByText(text, options)– Matches visible text throughtextSelector("text", text, options)(source). -
getByLabel(text, options)– Finds form controls associated with alabelelement viatextSelector("label", text, options)(source). -
getByPlaceholder(text, options)– Matchesinputortextareaplaceholder attributes usingtextSelector("placeholder", text, options)(source). -
getByAltText(text, options)– Targetsimgalt attributes withtextSelector("alt", text, options)(source). -
getByTitle(text, options)– Matches elements carrying atitleattribute throughtextSelector("title", text, options)(source). -
getByTestId(testId)– Performs an exact match on custom test identifiers usingtestIdSelector(testId)(source). -
filter(options)– Narrows an existing Locator with additional constraints by invokingfilterSelector(selector, options)(source).
Filtering Capabilities with filter()
The filter(options) method augments a Locator with predicates that are evaluated when the selector resolves. Internally, the filter data is encoded as an internal selector string of the form internal:filter:{…} (source). This encoding allows the same filtering logic to work uniformly for CSS selectors, XPath expressions, and AX-role queries.
Supported Filter Options
-
has/hasNot– Requires or excludes the presence of a descendant that matches a provided Locator or raw selector. The engine converts these values throughlocatorSelector(source). -
hasText/hasNotText– Asserts or rejects visible text content. String values and regular expressions are transformed intotextMatcherobjects—either{text, exact:false}or{regex,flags}—depending on the input type (source). -
Combined predicates – You can mix
has,hasNot,hasText, andhasNotTextin a single call to express complex constraints, such as requiring a button containing the exact text Submit while excluding any subtree that contains an error message.
When the Locator eventually resolves via the driver’s readQueryAll or queryRoleBackendNodeIds, filterSelector applies these constraints on the client side.
Full Chain Example
The following example demonstrates how to combine scoping, role selection, filtering, and ordinal targeting in one fluent chain:
// Find the last visible button inside a dialog that:
// • has role="button"
// • contains the exact text "Confirm"
// • does NOT contain any descendant with text "Error"
await page
.locator('dialog')
.getByRole('button')
.filter({
hasText: 'Confirm',
hasNotText: /Error/,
has: page.locator('svg.icon') // require an SVG icon child
})
.last()
.click();
Step-by-step resolution:
page.locator('dialog')establishes the base selector..getByRole('button')narrows the scope to button descendants usingroleSelector..filter({...})injects text-matching and child-presence rules viafilterSelector..last()selects the final matched element with theinternal:lastprefix..click()resolves the entire chain and executes the action.
Each step returns a distinct Locator, so the intermediate instances remain available for reuse elsewhere in your script.
Key Source Files
The page.locator() chainable API and filtering capabilities are defined, documented, and executed across four primary files in the citrolabs/ego-lite repository:
package/ego-browser/src/helpers.ts– Implements thecreateLocatorfactory, all chainable methods, and thefilterencoding logic.package/ego-browser/src/driver/locator.ts– Provides low-level CDP helpers that evaluate selectors, includingcountandevaluateAll.package/ego-browser/src/locator-query.ts– Generates JavaScript expressions for CSS and XPath queries that underpin the filtering engine.package/ego-browser/src/format.ts– Documents public API signatures forpage.locatorand its sub-methods, consumed by the built-inhelp()command.
Summary
page.locator()returns a strict, immutable Locator that auto-waits for DOM attachment.- Chainable methods such as
first(),last(),nth(),locator(), andgetBy*helpers build new selectors without mutating the original Locator. filter(options)encodes constraints asinternal:filter:{…}and supportshas,hasNot,hasText, andhasNotTextpredicates.- Filtering logic is applied client-side during resolution via
filterSelector, ensuring compatibility with CSS, XPath, and ARIA role queries. - All Locator factory logic resides in
package/ego-browser/src/helpers.ts, while execution depends ondriver/locator.tsandlocator-query.ts.
Frequently Asked Questions
What makes page.locator() chainable in Ego Browser?
The createLocator factory in helpers.ts ensures that every chainable method—such as filter() or getByRole()—returns a brand-new Locator instance wrapping the previous selector. Because the original object is never mutated, multiple refinement branches can originate from the same base locator safely.
How does the filter() method handle regular expressions?
When hasText or hasNotText receives a RegExp, the source code converts it into a textMatcher object containing regex and flags properties. This object is then serialized into the internal filter selector so the client-side engine can match visible text against the pattern during resolution.
Can I reuse a Locator after calling chainable methods like last() or filter()?
Yes. Immutability is a core design principle of the Ego Browser Locator API. Calling last(), first(), nth(), or filter() produces a derived Locator while leaving the parent instance unchanged, enabling safe reuse across different test scenarios.
Where is the filtering logic executed when a Locator resolves?
According to the citrolabs/ego-lite source code, the internal:filter:{…} selector generated by filterSelector is interpreted on the client side when the driver invokes readQueryAll or queryRoleBackendNodeIds. This guarantees consistent behavior regardless of whether the underlying query uses CSS, XPath, or AX-role selectors.
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 →