# How Locator Filter Selectors Work in ego-browser: hasText, hasNotText, has, and hasNot Explained

> Master ego browser locator filter selectors like hasText, hasNotText, has, and hasNot. Learn how to precisely target DOM elements using text matching and descendant checks for efficient web scraping.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: how-to-guide
- Published: 2026-07-26

---

**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 filtering
- **`hasText`** / **`hasNotText`**: Configuration objects specifying text-matching rules against the element's inner text
- **`has`**: A sub-selector string that must match at least one descendant
- **`hasNot`**: 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):

```typescript
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:

```javascript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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 `queryAllExpression` function in `src/locator‑query.ts` orchestrates the base query and wraps results with dynamically generated filter functions
- **`hasText`** and **`hasNotText`** leverage `textMatcherExpression` to evaluate inner text content via regular expressions or string comparisons
- **`has`** requires matching descendants (`.length > 0`), while **`hasNot`** requires 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.