SKILL.md
tui-testing
How to write and run unit tests for Warp's headless TUI front-end (crates/warptui and the element library at crates/warpuicore/src/elements/tui). This complements rust-unit-tests (general Rust test conventions) and parallels gui-integration-test for the TUI.
TUI tests are plain, fast unit tests: they render an element tree to a fixed cell grid and assert on the resulting text lines. They do not use the GUI real-display / integration / computer-use framework (that's gui-integration-test / gui-integration-test-video, which are GUI-only).
Two test locations, two harnesses
TUI tests live in two crates, and which render helper you use depends on where the test is:
Element-library tests (in warpui_core)
Tests for the shared cell-grid elements live in crates/warpuicore/src/elements/tui/*tests.rs and use the crate-internal testsupport helpers from crates/warpuicore/src/elements/tui/mod.rs:
testsupport::rendertolines(element: &dyn TuiElement, size: TuiSize) -> Vec<String>— one-call harness: buildsarea = TuiRect::new(0, 0, size.width, size.height)andTuiBuffer::empty(area), callselement.render(area, &mut buffer, ctx)inside a paint context, and returnsbuffer.tolines(). It only callsrender, notlayout— fine for a leaf likeTuiText, but composite elements (e.g.TuiFlex) populate child sizes duringlayout, so lay the element out first (see thelayoutathelper inflextests.rs) or it renders empty/stale.testsupport::withpaint_context(|ctx| ...)— runs a closure with aTuiPaintContextover a fresh, empty view map. Use it when you need theTuiBufferafterward to assert on individualCells.
These helpers are pub(crate) to warpuicore, so they are only callable from that crate's own tests. Simplest leaf assertion (see texttests.rs, flex_tests.rs):
```rust path=null start=null asserteq!( renderto_lines(&TuiText::new("hello"), TuiSize::new(10, 1)), vec!["hello "], );
### View/screen tests (in `warp_tui`)
`warp_tui` tests (`crates/warp_tui/src/*_tests.rs`) can NOT use `test_support` — render directly instead, under an `App::test` read/update so an `AppContext` is available. `layout` must run before `render` so child sizes are populated. This local helper mirrors `render_element` in `transcript_view_tests.rs` and `render_lines` in `editor_element_tests.rs`:
```rust path=null start=null
fn render_lines(app_ctx: &AppContext, mut element: impl TuiElement, w: u16, h: u16) -> Vec<String> {
let mut rendered_views = EntityIdMap::default();
let mut lctx = TuiLayoutContext { rendered_views: &mut rendered_views };
let size = element.layout(TuiConstraint::loose(TuiSize::new(w, h)), &mut lctx, app_ctx);
let area = TuiRect::new(0, 0, size.width, size.height);
let mut buffer = TuiBuffer::empty(area);
let mut paint_ctx = TuiPaintContext::new(&mut rendered_views);
element.render(area, &mut buffer, &mut paint_ctx);
buffer.to_lines()
}
Views that resolve theme styles need an Appearance singleton (ctx.addsingletonmodel(|| Appearance::mock())). To exercise a whole view through the real draw path, drive the presenter: TuiPresenter::new(), presenter.invalidate(&invalidation, ctx, windowid), then presenter.present(ctx, &view, area) and assert on frame.buffer.tolines() (see transcriptview_tests.rs).
Asserting on styles, cursor, and events
- Styles/colors: paint into a buffer yourself and index cells.
to_lines()only carries glyphs, so style assertions readCellfields:
```rust path=null start=null let mut buffer = TuiBuffer::empty(TuiRect::new(0, 0, 1, 1)); withpaintcontext(|ctx| text.render(TuiRect::new(0, 0, 1, 1), &mut buffer, ctx)); let cell = &buffer[(0, 0)]; asserteq!(cell.symbol(), "a"); asserteq!(cell.fg, Color::Red); assert!(cell.modifier.contains(Modifier::BOLD));
(`with_paint_context` is the `warpui_core`-only helper; in a `warp_tui` test construct the paint context directly with `TuiPaintContext::new(&mut rendered_views)` as in the `render_lines` helper above.)
- **Cursor:** call `element.cursor_position(area, ctx)` and assert on the returned `Option<(u16, u16)>`.
- **Events:** build a `TuiEvent` (e.g. `TuiEvent::KeyDown { keystroke, chars, details, is_composing }` or `TuiEvent::ScrollWheel { .. }`), then call `element.dispatch_event(&event, area, &mut event_ctx, &mut layout_ctx, app_ctx)` and assert on the returned `bool` and on the re-rendered lines/cursor. Layout must run first. See `render_element` / `dispatch_event` / `dispatch_scroll` helpers in `crates/warp_tui/src/transcript_view_tests.rs`.
Keep test areas at a stable, small width/height so golden line vectors stay readable and deterministic; trailing padding is spaces (e.g. `"hello "`).
## Where tests live
Follow the repo convention: put tests in a sibling `*_tests.rs` file included at the end of the source module:
```rust path=null start=null
#[cfg(test)]
#[path = "foo_tests.rs"]
mod tests;
Real examples to model:
- Element library:
crates/warpuicore/src/elements/tui/texttests.rs,flextests.rs,containertests.rs,constrainedboxtests.rs,buffer_tests.rs. - Views/screens:
crates/warptui/src/transcriptviewtests.rs,crates/warptui/src/input/view_tests.rs.
Appearance in view tests
Views that resolve theme styles (via TuiUiBuilder::fromapp) need an Appearance singleton. Install the mock in the test with app.addsingletonmodel(|| Appearance::mock()); (as in transcriptviewtests.rs). Appearance::mock() comes from warp_core's test-util feature, wired as a dev-dependency of the TUI crates.
Process-level tests (no integration harness)
The TUI has no GUI-style integration harness: the real-display, synthetic-event framework in crates/integration (see gui-integration-test) is GUI-only and does not drive the TUI. Besides render-to-lines unit tests, binary-level behavior is covered by a process-level test that spawns the built binary and asserts on its output/exit — see crates/warptui/tests/workerdispatch.rs (it runs CARGOBINEXE_warp-tui-oss and checks that a worker invocation dispatches without launching the TUI frontend). Use that pattern for process/CLI-level behavior, and render-to-lines unit tests for element/screen rendering. There is no separate TUI integration-test skill because there is no such framework today.
Running
- Whole crates:
cargo nextest run -p warptuiandcargo nextest run -p warpuicore. - The TUI element library is behind the
tuifeature; if a test needs it explicitly, add--features tui. - A single test by substring:
cargo nextest run -p warp_tui -E 'test(<substring>)'. - Before opening a PR, run
./script/formatandcargo clippyper repo conventions.