use abstracttui::app::{App, Driver, Modal, RunConfig};
use abstracttui::base::Size;
use abstracttui::layout::{Dimension, LayoutStyle};
use abstracttui::reactive::{create_root, flush_effects, Scope};
use abstracttui::term::{Capabilities, EnterOptions, GraphicsCaps, KittyFlags, MouseMode};
use abstracttui::testing::CaptureTerm;
use abstracttui::ui::{
dyn_view_scoped, text, BufferCanvas, Element, Key, KeyChord, KeyEvent, UiEvent, UiTree,
};
use abstracttui::widgets::TextInput;
use std::cell::RefCell;
use std::rc::Rc;
fn test_config() -> RunConfig {
RunConfig {
caps: Some(Capabilities::with(|c| {
c.truecolor = true;
c.colors_256 = true;
c.unicode_ok = true;
})),
enter: Some(EnterOptions {
alternate_screen: true,
hide_cursor: true,
mouse: MouseMode::Off,
bracketed_paste: false,
focus_events: false,
kitty_keyboard: KittyFlags(0),
}),
probe: false,
}
}
fn app_with_scope(size: Size) -> (App, Rc<RefCell<Option<Scope>>>) {
let mut app = App::new(size);
let scope_holder: Rc<RefCell<Option<Scope>>> = Rc::new(RefCell::new(None));
let sh = scope_holder.clone();
app.mount(move |cx| {
*sh.borrow_mut() = Some(cx);
Element::new().child(text("underneath")).build()
})
.expect("mount");
(app, scope_holder)
}
#[test]
fn autofocus_inside_dyn_view_regeneration_mounts_and_focuses() {
let tokens = abstracttui::theme::default_theme().tokens;
let mut tree = UiTree::new(Size::new(30, 4));
let mut probe = None;
let (root, ()) = create_root(|cx| {
let theme_key = cx.signal(0u32);
probe = Some(theme_key);
let view = Element::new()
.style(LayoutStyle::column())
.child(dyn_view_scoped(
LayoutStyle::default().height(Dimension::Cells(1)),
move |scx| {
let _generation = theme_key.get(); TextInput::new()
.placeholder("composer")
.element(scx, &tokens)
.autofocus()
.build()
},
))
.build();
tree.mount(cx, view);
});
tree.layout();
let before = tree
.focused()
.expect("initial mount delivers dyn-subtree autofocus");
probe.expect("signal captured").set(1);
flush_effects();
tree.layout(); let after = tree.focused().expect("focus landed after regeneration");
assert_ne!(
before, after,
"focus points at the regenerated input, not the disposed one"
);
root.dispose();
}
#[test]
fn repeated_dyn_regenerations_keep_autofocus_deterministic() {
let tokens = abstracttui::theme::default_theme().tokens;
let mut tree = UiTree::new(Size::new(30, 4));
let mut probe = None;
let (root, ()) = create_root(|cx| {
let key = cx.signal(0u32);
probe = Some(key);
let view = Element::new()
.child(dyn_view_scoped(LayoutStyle::default(), move |scx| {
let _k = key.get();
TextInput::new().element(scx, &tokens).autofocus().build()
}))
.build();
tree.mount(cx, view);
});
let key = probe.expect("signal");
for generation in 1..=3u32 {
key.set(generation);
flush_effects();
tree.layout();
assert!(
tree.focused().is_some(),
"generation {generation} ends focused"
);
let consumed = tree.dispatch(&UiEvent::Key(KeyEvent::plain(Key::Char('x'))));
assert!(consumed, "generation {generation} input consumes keys");
}
root.dispose();
}
#[test]
fn modal_content_shortcut_fires_without_tab() {
let size = Size::new(30, 8);
let (mut app, scope_holder) = app_with_scope(size);
let overlays = app.overlays();
let mut term = CaptureTerm::new(size);
let mut driver = Driver::new(&mut app, &mut term, test_config()).expect("driver");
driver.turn(&mut app, &mut term).expect("frame 1");
let fired: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let f2 = fired.clone();
let cx = scope_holder.borrow().expect("scope");
let _modal = Modal::open(&overlays, cx, size, Size::new(20, 4), move |_| {
Element::new()
.shortcut(KeyChord::plain(Key::Char('a')), move |_| {
*f2.borrow_mut() += 1;
})
.child(text("approve? [a]"))
.build()
});
driver.turn(&mut app, &mut term).expect("modal frame");
term.push_input(b"a");
driver.turn(&mut app, &mut term).expect("chord turn");
assert_eq!(
*fired.borrow(),
1,
"content-root shortcut fired without Tab"
);
}
#[test]
fn modal_autofocus_wins_and_root_shortcuts_stay_reachable() {
let tokens = abstracttui::theme::default_theme().tokens;
let size = Size::new(30, 8);
let (app, scope_holder) = app_with_scope(size);
let overlays = app.overlays();
let escaped: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let e2 = escaped.clone();
let value_probe: Rc<RefCell<Option<abstracttui::reactive::Signal<String>>>> =
Rc::new(RefCell::new(None));
let v2 = value_probe.clone();
let cx = scope_holder.borrow().expect("scope");
let modal = Modal::open(&overlays, cx, size, Size::new(24, 5), move |mcx| {
let value = mcx.signal(String::new());
*v2.borrow_mut() = Some(value);
Element::new()
.style(LayoutStyle::column())
.shortcut(KeyChord::plain(Key::Escape), move |_| {
*e2.borrow_mut() += 1;
})
.child(text("name:"))
.child(
TextInput::new()
.value(value)
.element(mcx, &tokens)
.autofocus()
.build(),
)
.build()
});
let mut tree = modal.layer().tree().expect("modal layer holds a tree");
tree.dispatch(&UiEvent::Key(KeyEvent::plain(Key::Char('x'))));
let typed = value_probe
.borrow()
.expect("value signal captured")
.get_untracked();
assert_eq!(typed, "x", "typing reaches the autofocused input");
tree.dispatch(&UiEvent::Key(KeyEvent::plain(Key::Escape)));
assert_eq!(
*escaped.borrow(),
1,
"root shortcut reachable while the input is focused"
);
}
#[test]
fn modal_fixed_rows_survive_content_overflow() {
let size = Size::new(30, 8);
let (app, scope_holder) = app_with_scope(size);
let overlays = app.overlays();
let cx = scope_holder.borrow().expect("scope");
let modal = Modal::open(&overlays, cx, size, Size::new(30, 6), |_| {
let tall = (0..40).map(|i| format!("arg line {i}")).collect::<Vec<_>>();
let fixed_row = |label: &str| {
Element::new()
.style(LayoutStyle::default().height(Dimension::Cells(1)))
.child(text(label))
.build()
};
Element::new()
.style(LayoutStyle::column())
.child(fixed_row("Approve tool?"))
.child(
Element::new()
.style(LayoutStyle::default().grow(1.0))
.child(text(tall.join("\n")))
.build(),
)
.child(fixed_row("[a]pprove [d]eny"))
.child(fixed_row("esc closes"))
.build()
});
let mut tree = modal.layer().tree().expect("modal layer holds a tree");
let mut canvas = BufferCanvas::new(Size::new(30, 6));
tree.draw(&mut canvas);
let screen: Vec<String> = (0..6).map(|y| canvas.row_text(y)).collect();
let flat = screen.join("\n");
for needle in ["Approve tool?", "[a]pprove [d]eny", "esc closes"] {
assert!(
flat.contains(needle),
"fixed row {needle:?} must stay visible; screen:\n{flat}"
);
}
}
#[test]
fn modal_fixed_row_floor_respects_explicit_min() {
let size = Size::new(30, 8);
let (app, scope_holder) = app_with_scope(size);
let overlays = app.overlays();
let cx = scope_holder.borrow().expect("scope");
let modal = Modal::open(&overlays, cx, size, Size::new(30, 6), |_| {
let tall = (0..40).map(|i| format!("line {i}")).collect::<Vec<_>>();
Element::new()
.style(LayoutStyle::column())
.child(
Element::new()
.style(LayoutStyle::default().height(Dimension::Cells(1)).min_h(0))
.child(text("collapsible"))
.build(),
)
.child(
Element::new()
.style(LayoutStyle::default().grow(1.0))
.child(text(tall.join("\n")))
.build(),
)
.child(
Element::new()
.style(LayoutStyle::default().height(Dimension::Cells(1)))
.child(text("kept row"))
.build(),
)
.build()
});
let mut tree = modal.layer().tree().expect("modal tree");
let mut canvas = BufferCanvas::new(Size::new(30, 6));
tree.draw(&mut canvas);
let flat: String = (0..6).map(|y| canvas.row_text(y) + "\n").collect();
assert!(
!flat.contains("collapsible"),
"explicit min_h(0) keeps the author's collapse; screen:\n{flat}"
);
assert!(
flat.contains("kept row"),
"unopted fixed row stays visible; screen:\n{flat}"
);
}
#[test]
fn capabilities_construct_via_with_and_grow_without_breakage() {
let caps = Capabilities::with(|c| {
c.truecolor = true;
c.colors_256 = true;
c.unicode_ok = true;
});
assert!(caps.truecolor && caps.colors_256 && caps.unicode_ok);
assert!(caps.deferred_wrap, "unset fields keep their defaults");
let gfx = GraphicsCaps::with(|g| {
g.kitty_graphics = true;
});
assert!(gfx.kitty_graphics && !gfx.sixel);
let from_env = Capabilities::detect_env_with(&|_| None);
assert!(from_env.dumb, "empty environment is honestly dumb");
}
#[cfg(debug_assertions)]
#[test]
fn zero_collapse_notice_reaches_the_notices_lane() {
use abstracttui::app::use_startup_notices;
create_root(|_| {
let size = Size::new(20, 6);
let mut app = App::new(size);
type NoticeSignal = abstracttui::reactive::Signal<Vec<String>>;
let lane: Rc<RefCell<Option<NoticeSignal>>> = Rc::new(RefCell::new(None));
let lane_in = lane.clone();
app.mount(move |cx| {
*lane_in.borrow_mut() = Some(use_startup_notices(cx));
Element::new()
.style(LayoutStyle::column())
.child(
Element::new()
.style(LayoutStyle::default().h(600))
.child(text("filler"))
.build(),
)
.child(
Element::new()
.style(LayoutStyle::default().h(1))
.child(text("status row"))
.build(),
)
.build()
})
.expect("mount");
let mut term = CaptureTerm::new(size);
let mut driver = Driver::new(&mut app, &mut term, test_config()).expect("driver");
driver.turn(&mut app, &mut term).expect("turn 1");
driver.turn(&mut app, &mut term).expect("turn 2");
flush_effects();
let signal = lane.borrow().expect("notices signal captured");
let notes = signal.get();
let collapse: Vec<&String> = notes
.iter()
.filter(|n| n.contains("collapsed to 0"))
.collect();
assert_eq!(
collapse.len(),
1,
"exactly one collapse notice in the lane: {notes:?}"
);
driver.turn(&mut app, &mut term).expect("turn 3");
driver.turn(&mut app, &mut term).expect("turn 4");
flush_effects();
let notes2 = signal.get();
let collapse2: Vec<&String> = notes2
.iter()
.filter(|n| n.contains("collapsed to 0"))
.collect();
assert_eq!(collapse2.len(), 1, "no re-report across turns: {notes2:?}");
app.shutdown();
});
}