use std::time::Duration;
use abstracttui::app::{App, Driver, RunConfig};
use abstracttui::base::Size;
use abstracttui::layout::{Dimension, Style};
use abstracttui::term::{Capabilities, Terminal};
use abstracttui::testing::CaptureTerm;
use abstracttui::ui::{dyn_view, text, Element};
fn fixed_caps() -> Capabilities {
Capabilities::with(|c| {
c.truecolor = true;
c.colors_256 = true;
c.sync_output_2026 = true;
c.hyperlinks = true;
})
}
fn config() -> RunConfig {
RunConfig {
caps: Some(fixed_caps()),
enter: None,
probe: false,
}
}
fn settle(driver: &mut Driver, app: &mut App, term: &mut CaptureTerm) -> usize {
let mut renders = 0;
for _ in 0..64 {
let turn = driver.turn(app, term).expect("turn");
if turn.rendered {
renders += 1;
}
if turn.idle {
return renders;
}
}
panic!("loop failed to settle within 64 turns");
}
#[test]
fn idle_app_emits_zero_bytes_across_idle_turns() {
let mut term = CaptureTerm::new(Size::new(30, 6));
let mut app = App::new(Size::new(30, 6));
app.mount(|_cx| {
Element::new()
.style(
Style::default()
.width(Dimension::Cells(10))
.height(Dimension::Cells(1)),
)
.child(text("steady"))
.build()
})
.expect("mount");
let mut driver = Driver::new(&mut app, &mut term, config()).expect("enter");
let renders = settle(&mut driver, &mut app, &mut term);
assert!(renders >= 1, "the mount frame must render");
assert!(!term.bytes().is_empty(), "first frame must have painted");
let baseline = term.take_bytes().len();
let flushes = term.flush_count();
for i in 0..16 {
let turn = driver.turn(&mut app, &mut term).expect("turn");
assert!(turn.idle, "turn {i} must report idle");
assert!(!turn.rendered, "turn {i} rendered with no damage");
}
assert_eq!(
term.bytes().len(),
0,
"idle turns wrote bytes (baseline {baseline})"
);
assert_eq!(term.flush_count(), flushes, "idle turns flushed");
}
#[test]
fn cross_thread_post_lands_exactly_one_frame_later() {
let mut term = CaptureTerm::new(Size::new(30, 4));
let mut app = App::new(Size::new(30, 4));
let mut counter_handle = None;
app.mount(|cx| {
let counter = cx.signal(0u32);
counter_handle = Some(counter);
Element::new()
.style(
Style::default()
.width(Dimension::Cells(20))
.height(Dimension::Cells(1)),
)
.child(dyn_view(Style::default(), move || {
text(format!("count {}", counter.get()))
}))
.build()
})
.expect("mount");
let counter = counter_handle.expect("signal handle");
let mut driver = Driver::new(&mut app, &mut term, config()).expect("enter");
settle(&mut driver, &mut app, &mut term);
assert!(term.screen().to_text().contains("count 0"));
let _ = term.take_bytes();
let wake = app.wake_handle();
let t = std::thread::spawn(move || {
wake.post(move || counter.set(1));
});
t.join().expect("poster thread");
let turn = driver.turn(&mut app, &mut term).expect("turn");
assert!(
turn.rendered,
"the posted write must repaint on the next turn"
);
assert!(
term.screen().to_text().contains("count 1"),
"damage was lost: {}",
term.screen().to_text()
);
let turn = driver.turn(&mut app, &mut term).expect("turn");
assert!(!turn.rendered, "posted damage painted twice");
assert!(turn.idle);
}
#[test]
fn resize_between_keys_relayouts_and_repaints() {
let mut term = CaptureTerm::new(Size::new(24, 4));
let mut app = App::new(Size::new(24, 4));
app.mount(|_cx| {
Element::new()
.style(
Style::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Cells(1)),
)
.child(text("resizable content"))
.build()
})
.expect("mount");
let mut driver = Driver::new(&mut app, &mut term, config()).expect("enter");
settle(&mut driver, &mut app, &mut term);
let _ = term.take_bytes();
term.push_input(b"a");
term.push_resize(Size::new(40, 8));
term.push_input(b"b");
let turn = driver.turn(&mut app, &mut term).expect("turn");
assert_eq!(turn.events, 3, "key + resize + key all dispatched");
assert!(turn.rendered, "resize must force a repaint");
assert!(!term.bytes().is_empty(), "the repaint reached the terminal");
assert_eq!(term.size().expect("size"), Size::new(40, 8));
let turn = driver.turn(&mut app, &mut term).expect("turn");
assert!(turn.idle || turn.rendered);
}
#[test]
fn app_viewport_tracks_driver_resize() {
let mut term = CaptureTerm::new(Size::new(24, 4));
let mut app = App::new(Size::new(24, 4));
app.mount(|_cx| Element::new().build()).expect("mount");
let mut driver = Driver::new(&mut app, &mut term, config()).expect("enter");
settle(&mut driver, &mut app, &mut term);
term.push_resize(Size::new(40, 8));
let _ = driver.turn(&mut app, &mut term).expect("turn");
assert_eq!(
app.viewport(),
Size::new(40, 8),
"App::viewport must not lie after a driver-handled resize"
);
}
#[test]
fn driver_session_enter_leave_balance_via_model() {
let mut term = CaptureTerm::new(Size::new(20, 4));
let mut app = App::new(Size::new(20, 4));
app.mount(|_cx| {
Element::new()
.style(
Style::default()
.width(Dimension::Cells(5))
.height(Dimension::Cells(1)),
)
.child(text("hi"))
.build()
})
.expect("mount");
let mut driver = Driver::new(&mut app, &mut term, config()).expect("enter");
assert!(
term.screen().modes().alt_screen(),
"enter must switch to alt screen"
);
settle(&mut driver, &mut app, &mut term);
driver.finish(&mut term).expect("leave");
let screen = term.screen();
assert!(
!screen.modes().alt_screen(),
"leave must restore the main screen"
);
assert!(
screen.modes().cursor_visible(),
"leave must restore the cursor"
);
assert_eq!(
screen.counters().kitty_push_depth,
0,
"kitty flags balanced"
);
assert!(
screen.counters().sync_ends >= screen.counters().sync_begins,
"a 2026 bracket was left open"
);
assert!(
!screen.modes().synchronized_output(),
"2026 must be off after leave"
);
assert_eq!(
screen.unknown_seq_count(),
0,
"the whole session was modeled traffic"
);
}
#[test]
fn ctrl_c_quits_by_default() {
let mut term = CaptureTerm::new(Size::new(20, 4));
let mut app = App::new(Size::new(20, 4));
app.mount(|_cx| {
Element::new()
.style(
Style::default()
.width(Dimension::Cells(5))
.height(Dimension::Cells(1)),
)
.child(text("hi"))
.build()
})
.expect("mount");
let mut driver = Driver::new(&mut app, &mut term, config()).expect("enter");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x03"); let turn = driver.turn(&mut app, &mut term).expect("turn");
assert!(
turn.quit,
"Ctrl+C must request quit (ISIG is off in raw mode)"
);
}
#[test]
fn spawned_worker_panic_surfaces_as_app_error() {
let mut term = CaptureTerm::new(Size::new(20, 4));
let mut app = App::new(Size::new(20, 4));
app.mount(|_cx| {
Element::new()
.style(
Style::default()
.width(Dimension::Cells(5))
.height(Dimension::Cells(1)),
)
.child(text("hi"))
.build()
})
.expect("mount");
let mut driver = Driver::new(&mut app, &mut term, config()).expect("enter");
settle(&mut driver, &mut app, &mut term);
let handle = abstracttui::reactive::spawn_worker("doomed", || {
panic!("worker exploded");
});
handle.join().ok(); for _ in 0..1000 {
match driver.turn(&mut app, &mut term) {
Ok(_) => std::thread::yield_now(),
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("doomed") || msg.contains("worker") || msg.contains("exploded"),
"worker death must be named in the error: {msg}"
);
return;
}
}
}
panic!("worker panic never surfaced as an app error");
}
#[test]
fn probe_replies_upgrade_caps_after_first_paint() {
let mut term = CaptureTerm::new(Size::new(20, 4));
let mut app = App::new(Size::new(20, 4));
app.mount(|_cx| {
Element::new()
.style(
Style::default()
.width(Dimension::Cells(5))
.height(Dimension::Cells(1)),
)
.child(text("hi"))
.build()
})
.expect("mount");
let cfg = RunConfig {
caps: Some(Capabilities::default()),
enter: None,
probe: true,
};
let mut driver = Driver::new(&mut app, &mut term, cfg).expect("enter");
let renders = settle(&mut driver, &mut app, &mut term);
assert!(renders >= 1, "first paint must not wait for the probe");
assert!(!driver.caps().sixel);
term.push_input(b"\x1b[?62;4c"); let _ = driver.turn(&mut app, &mut term).expect("turn");
assert!(
driver.caps().sixel,
"probe reply must upgrade caps mid-session"
);
settle(&mut driver, &mut app, &mut term);
}
#[test]
fn turn_is_truly_non_blocking() {
let mut term = CaptureTerm::new(Size::new(20, 4));
let mut app = App::new(Size::new(20, 4));
app.mount(|_cx| Element::new().build()).expect("mount");
let mut driver = Driver::new(&mut app, &mut term, config()).expect("enter");
for _ in 0..1000 {
let _ = driver.turn(&mut app, &mut term).expect("turn");
}
let _ = Duration::ZERO; }