use abstracttui::app::{App, Driver, RunConfig};
use abstracttui::base::{Point, Rect, Rgba, Size};
use abstracttui::gfx::bitmap::Bitmap;
use abstracttui::gfx::{ExternalSink, ImageSession, SyncOutcome};
use abstracttui::prelude::*;
use abstracttui::term::caps::GraphicsCaps;
use abstracttui::term::Capabilities;
use abstracttui::testing::{CaptureTerm, KittyModel, VtScreen};
use abstracttui::widgets::{Button, List};
fn run_one_app(seed: u64) -> String {
let size = Size::new(48, 10);
let mut term = CaptureTerm::new(size);
let mut app = App::new(size);
app.mount(|cx| {
let count = cx.signal(seed as i64);
let items = vec![format!("row-{seed}-a"), format!("row-{seed}-b")];
let sel = cx.signal(0usize);
let t = ¤t_theme().tokens;
Element::new()
.style(LayoutStyle::column())
.child(dyn_view(LayoutStyle::line(1), move || {
text(format!("count {}", count.get()))
}))
.child(
Button::new("+1")
.on_click(move || count.update(|c| *c += 1))
.element(cx, t)
.build(),
)
.child(List::new(items).selection(sel).element(cx, t).build())
.build()
})
.expect("mount");
let cfg = RunConfig {
caps: Some(Capabilities::default()),
enter: None,
probe: false,
..RunConfig::default()
};
let mut driver = Driver::new(&mut app, &mut term, cfg).expect("enter");
let mut vt = VtScreen::new(size);
for _ in 0..8 {
let idle = driver.turn(&mut app, &mut term).expect("turn").idle;
vt.feed(&term.take_bytes());
if idle {
break;
}
}
term.push_input(b"\t\r"); for _ in 0..6 {
let idle = driver.turn(&mut app, &mut term).expect("turn").idle;
vt.feed(&term.take_bytes());
if idle {
break;
}
}
drop(driver); vt.feed(&term.take_bytes());
vt.to_text()
}
#[test]
fn two_full_apps_sequentially_are_independent() {
let first = run_one_app(10);
let second = run_one_app(20);
let third = run_one_app(10);
assert!(
first.contains("count 1") && first.contains("row-10-a"),
"run 1 wrong:\n{first}"
);
assert!(
second.contains("count 2") && second.contains("row-20-a"),
"run 2 wrong:\n{second}"
);
assert_eq!(
first, third,
"same-seed app differs after intervening runs — leaked global state"
);
}
#[test]
fn ten_full_apps_do_not_drift_or_panic() {
let baseline = run_one_app(7);
for i in 0..10 {
let out = run_one_app(7);
assert_eq!(
out, baseline,
"app instance {i} drifted from baseline — teardown leak"
);
}
}
struct ModelSink(KittyModel);
impl ExternalSink for ModelSink {
fn external_write(&mut self, bytes: &[u8], _at: Point) {
self.0.feed(bytes);
}
}
fn kitty_caps() -> GraphicsCaps {
GraphicsCaps::with(|g| g.kitty_graphics = true)
}
#[test]
fn image_session_reuse_after_drop_is_clean() {
let caps = kitty_caps();
let img = Bitmap::new(4, 4, Rgba::rgb(1, 2, 3));
let ids_first = {
let mut sink = ModelSink(KittyModel::new());
let mut session = ImageSession::new();
session.sync(&mut sink, 1, 1, &img, Rect::new(0, 0, 4, 2), &caps);
session.sync(&mut sink, 2, 1, &img, Rect::new(5, 0, 4, 2), &caps);
session.release_all(&mut sink, &caps);
assert!(
sink.0.live_data_ids().is_empty(),
"session 1 leaked on release_all"
);
session.live_slots()
};
assert_eq!(
ids_first, 0,
"session 1 still tracked slots after release_all"
);
let mut sink = ModelSink(KittyModel::new());
let mut session2 = ImageSession::new();
let out = session2.sync(&mut sink, 1, 1, &img, Rect::new(0, 0, 4, 2), &caps);
assert!(
matches!(out, SyncOutcome::Emitted(_)),
"fresh session must emit a transmit"
);
assert_eq!(
sink.0.live_data_ids().len(),
1,
"fresh session accounting polluted by the dropped one"
);
assert!(
sink.0.violations.is_empty(),
"protocol violations after reuse: {:?}",
sink.0.violations
);
session2.release_all(&mut sink, &caps);
assert!(sink.0.live_data_ids().is_empty());
}
#[test]
fn overlay_world_reuse_across_apps() {
for seed in [1u64, 2, 3] {
let size = Size::new(30, 8);
let mut term = CaptureTerm::new(size);
let mut app = App::new(size);
let overlays = app.overlays();
let _ = &overlays;
app.mount(move |cx| {
let _ = cx;
let _ = seed;
Element::new().child(text("base")).build()
})
.expect("mount");
let cfg = RunConfig {
caps: Some(Capabilities::default()),
enter: None,
probe: false,
..RunConfig::default()
};
let mut driver = Driver::new(&mut app, &mut term, cfg).expect("enter");
for _ in 0..4 {
if driver.turn(&mut app, &mut term).expect("turn").idle {
break;
}
}
drop(driver);
}
}