feat: add chainable text and style locators (#149)
## Summary
Adds lazy, reusable locators to Rust, Python, and JavaScript. The APIs
are `getByText` / `getByStyle` in JavaScript and `get_by_text` /
`get_by_style` in Python and Rust. Text and contiguous per-row style-run
stages can be chained with `within`, `after`, or `before`. Repeated
parent matches divide relative regions into separate segments.
Locators support literal and regex matching, viewport and
full-scrollback searches, whitespace normalization, and match locations.
Occurrences are selected only by chaining `any`, `unique`, `first`,
`last`, or `nth`. Every read or action resolves against the current
terminal. Clicks target the middle matched cell, and highlights appear
in the live monitor and SVG screenshots. Bare `wait`, `highlight`, and
`expect` accept any match; `location` and `click` require one match; and
`unique().expect()` requires exactly one.
Style expectations use nested `getByStyle` / `get_by_style` stages. In
the default `within` direction, the style stage keeps a parent match
only when all its visible cells satisfy the style.
The CLI exposes `find|expect|click|highlight text "T"` with the same
selector and optional style flags. This removes `wait text`, redundant
`--no-strict`, and the one-shot `findText` / `find_text`, `waitText` /
`wait_text`, and `expectText` / `expect_text` APIs.
N-API and PyO3 receive typed locator stage arrays and action parameters
without locator JSON serialization. Dense regex match offsets are
converted in one forward pass. Occurrences are selected before match
cells are materialized unless a style filter must run first.
Full-scrollback locations use 32-bit row coordinates, preserving rows
beyond 65,535.
Tests cover `any` / `unique` / `first` / `last` / `nth` expectations,
negation with zero, one, or multiple matches, exact counts, and partial
style filtering in all three libraries. The package version is the sole
daemon compatibility version.
Text matching and actions use the generic locator engine. The four
cleanup revisions remove 3,081 lines and add 1,978, for a net reduction
of 1,103 lines.
## Examples
```js
const save = terminal
.getByText("Settings")
.getByText("Save", {
whitespace: "normalize",
direction: "after",
})
.unique();
await save.wait();
await save.expect();
await save.click();
await terminal
.getByText("Warning")
.getByStyle({ bold: true })
.unique()
.expect();
```
```python
from tui_test import TextStyle
save = (
terminal
.get_by_text("Settings")
.get_by_text(
"Save",
whitespace="normalize",
direction="after",
)
.unique()
)
await save.wait()
await save.expect()
await save.click()
await (
terminal
.get_by_text("Warning")
.get_by_style(TextStyle(bold=True))
.unique()
.expect()
)
```
```rust
use tui_test::LocatorDirection;
let save = terminal
.get_by_text("Settings")
.get_by_text_relative("Save", LocatorDirection::After)
.unique();
save.wait()?;
save.expect()?;
save.click()?;
```
```sh
tui-test find text "Add to cart" --fg green
tui-test expect text "Add to cart" --fg green --timeout 5000
tui-test expect text "Add to cart" --match unique --timeout 5000
tui-test click text "Add to cart" --fg green --timeout 5000
tui-test highlight text "Add to cart" --fg green
```
## Test plan
- `cargo fmt --all -- --check`
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
- `cargo clippy -p tui-test-rs --all-targets --no-default-features -- -D
warnings`
- `cargo build`
- `cargo test --workspace -- --test-threads=1`
- `python -m unittest discover -s bindings/python/tests -v`
- `python bindings/python/scripts/generate_stubs.py --check`
- `npm run test:node --prefix bindings/js`
Replaces #150, #151, #152, and #154.
---------
Signed-off-by: cpendery <cpendery@vt.edu> C
cpendery committed
436f61b7f3d6cdfe922793b33f54b15e8e23503f
Parent: 5bdcac8
Committed by GitHub <noreply@github.com>
on 8/29/2026, 5:38:20 PM