How Locator Filter Selectors Work in ego-browser: hasText, hasNotText, has, and hasNot Explained
Locator filter selectors in ego-browser parse selector strings beginning with internal:filter: to execute base queries and apply boolean conditions via the filterCondition function, enabling precise DOM targeting through text matching and descendant existence checks.
ego-browser (citrolabs/ego-lite) provides a sophisticated locator engine that processes complex selector strings through the queryAllExpression function in src/locator‑query.ts. These locator filter selectors allow developers to refine element selection by testing inner text content or verifying the presence of specific descendant elements, all evaluated dynamically within the browser context.
Filter Selector Architecture
The internal:filter Prefix and JSON Payload
When the parser encounters a selector starting with internal:filter:, it treats the subsequent content as a JSON-encoded configuration object. The parseInternalJson function (lines 19‑28 in src/locator‑query.ts) decodes this payload, which defines both the root elements to query and the filtering criteria to apply.
The JSON structure includes:
base: The selector string for root elements that will undergo filteringhasText/hasNotText: Configuration objects specifying text-matching rules against the element's inner texthas: A sub-selector string that must match at least one descendanthasNot: A sub-selector string that must match zero descendants
Dynamic Expression Generation
If a filter object is present, queryAllExpression constructs an immediately-invoked function expression (IIFE) that first queries base elements, then filters them using generated JavaScript conditions (lines 40‑46):
if (filter) {
return `(() => {
const elements = ${queryAllExpression(filter.base, rootExpression)};
return elements.filter((element) => ${filterCondition(filter, "element")});
})()`;
}
How Each Filter Condition Works
The filterCondition function (lines 39‑67 in src/locator‑query.ts) assembles individual test expressions and joins them with && operators, requiring all specified constraints to pass for an element to survive the filter.
Text Matching with hasText and hasNotText
For hasText, the engine invokes textMatcherExpression (lines 55‑64), which delegates to textMatchExpression (lines 66‑72) to generate either a RegExp test or a simple includes()/=== check. The hasNotText filter negates this same matcher by wrapping the expression in !(...), inverting the boolean result.
Descendant Existence Checks with has and hasNot
The has filter generates a condition requiring queryAllExpression(filter.has, elementExpression).length > 0, guaranteeing at least one descendant matches the sub-selector. Conversely, hasNot requires === 0, ensuring no descendants match. Because these filters re-use queryAllExpression, any supported selector type—including CSS, XPath, or role selectors—can be nested within has or hasNot clauses.
Practical Code Examples
The following patterns demonstrate valid locator filter selectors in ego-browser:
// Match buttons containing "Submit" text
await page.waitForSelector('css:button:has-text("Submit")');
// Exclude elements with exact "Loading" text
await page.waitForSelector('css:div:has-not-text("Loading")');
// Require at least one .icon descendant
await page.waitForSelector('css:section:has(.icon)');
// Ensure no .spinner descendants exist
await page.waitForSelector('css:article:has-not(.spinner)');
// Raw internal JSON format combining multiple filters
await page.waitForSelector(
'internal:filter:{"base":"css:ul","has":".active","hasNotText":{"text":"Disabled","exact":true}}'
);
Note: The Playwright-style :has-text() and :has() syntaxes are parsed by parsePlaywrightHasTextSelector (lines 83‑99) and translated into the internal internal:filter: representation before evaluation.
Key Implementation Files
| File | Role |
|---|---|
src/locator‑query.ts |
Core parser implementing queryAllExpression, filterCondition, and textMatcherExpression |
src/driver/locator.ts |
Public API exposing the locator helper that forwards selectors to the query engine |
src/driver/locator.test.mjs |
Test suite validating has, hasNot, hasText, and hasNotText behavior |
scripts/real-browser-e2e/cases/playwright‑locators.mjs |
End-to-end demonstrations of Playwright-style filter usage |
Summary
- Locator filter selectors use the
internal:filter:prefix to encode JSON configuration describing base selectors and filtering constraints - The
queryAllExpressionfunction insrc/locator‑query.tsorchestrates the base query and wraps results with dynamically generated filter functions hasTextandhasNotTextleveragetextMatcherExpressionto evaluate inner text content via regular expressions or string comparisonshasrequires matching descendants (.length > 0), whilehasNotrequires zero matches (.length === 0)- Filter conditions concatenate with
&&operators, meaning all specified criteria must be satisfied for an element to be selected
Frequently Asked Questions
What is the difference between has and hasText in ego-browser?
has verifies that at least one descendant element matches a specified sub-selector, testing DOM structure, whereas hasText checks if the element's own text content matches a given pattern. The former validates nested element existence, while the latter inspects text node values directly.
Can I combine multiple filter conditions in a single selector?
Yes. When the JSON payload specifies multiple filters, the filterCondition function concatenates individual test expressions with && operators. This means an element must simultaneously satisfy all text-matching and descendant requirements to be included in the final result set.
How does ego-browser handle Playwright-style :has-text syntax?
The parser includes parsePlaywrightHasTextSelector (lines 83‑99 in src/locator‑query.ts), which intercepts the shorthand :has-text() and :has-not-text() syntaxes and translates them into the standard internal:filter: JSON format before the main filtering logic processes them.
Why does hasNot require exactly zero matching descendants?
The hasNot filter explicitly checks that queryAllExpression(filter.hasNot, element).length === 0 to ensure no descendants match the sub-selector. This creates a strict guard clause that excludes elements containing unwanted nested structures, rather than selecting unrelated elements elsewhere in the DOM.
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 →