use lv_tui::prelude::*;
use lv_tui::widgets::*;
fn has_text(buf: &Buffer, text: &str) -> bool {
let cells: String = buf.cells.iter().map(|c| c.symbol.as_str()).collect();
cells.contains(text)
}
#[test]
fn checkbox_space_toggles() {
let mut pilot = Pilot::new(Checkbox::new("Option"), 20, 3);
pilot.focus_first();
assert!(!has_text(pilot.frame(), "✓"), "should start unchecked");
pilot.press(Key::Char(' ')).unwrap();
assert!(has_text(pilot.frame(), "✓"), "should be checked after Space");
pilot.press(Key::Char(' ')).unwrap();
assert!(!has_text(pilot.frame(), "✓"), "should be unchecked after second Space");
}
#[test]
fn checkbox_enter_toggles() {
let mut pilot = Pilot::new(Checkbox::new("Opt"), 20, 3);
pilot.focus_first();
pilot.press(Key::Enter).unwrap();
assert!(has_text(pilot.frame(), "✓"), "Enter should toggle checkbox");
}
#[test]
fn checkbox_starts_checked() {
let mut pilot = Pilot::new(Checkbox::new("Opt").checked(), 20, 3);
pilot.focus_first();
assert!(has_text(pilot.frame(), "✓"), "checked() should start checked");
}
#[test]
fn radiogroup_down_selects_next() {
let mut pilot = Pilot::new(RadioGroup::new(vec!["A", "B", "C"]), 20, 5);
pilot.focus_first();
let frame0: String = pilot.frame().cells.iter().map(|c| c.symbol.as_str()).collect();
assert!(frame0.contains("•"), "should have selection marker");
pilot.press(Key::Down).unwrap();
assert!(has_text(pilot.frame(), "B"), "should show option B");
}
#[test]
fn radiogroup_up_wraps() {
let mut pilot = Pilot::new(RadioGroup::new(vec!["X", "Y"]), 20, 3);
pilot.focus_first();
pilot.press(Key::Up).unwrap();
assert!(has_text(pilot.frame(), "Y"), "should wrap to last option");
}
#[test]
fn radiogroup_ignores_event_when_not_target_phase() {
let mut pilot = Pilot::new(RadioGroup::new(vec!["A", "B"]), 20, 3);
pilot.press(Key::Down).unwrap();
assert!(has_text(pilot.frame(), "A"), "A should still be selected without focus");
}
#[test]
fn select_expands_on_space() {
let mut pilot = Pilot::new(Select::new().options(vec!["Alpha", "Beta", "Gamma"]), 20, 10);
pilot.focus_first();
assert!(has_text(pilot.frame(), "Alpha"), "should show selected option");
assert!(has_text(pilot.frame(), "▼"), "should show collapsed indicator");
pilot.press(Key::Char(' ')).unwrap();
assert!(has_text(pilot.frame(), "▲"), "should show expanded indicator");
assert!(has_text(pilot.frame(), "Beta"), "expanded should show all options");
}
#[test]
fn select_collapses_on_esc() {
let mut pilot = Pilot::new(Select::new().options(vec!["A", "B"]), 20, 8);
pilot.focus_first();
pilot.press(Key::Char(' ')).unwrap(); assert!(has_text(pilot.frame(), "▲"));
pilot.press(Key::Esc).unwrap(); assert!(!has_text(pilot.frame(), "▲"));
}
#[test]
fn select_enter_toggles() {
let mut pilot = Pilot::new(Select::new().options(vec!["One", "Two"]), 20, 8);
pilot.focus_first();
pilot.press(Key::Enter).unwrap(); assert!(has_text(pilot.frame(), "▲"));
pilot.press(Key::Enter).unwrap(); assert!(!has_text(pilot.frame(), "▲"));
}
#[test]
fn select_navigation_in_expanded_mode() {
let mut pilot = Pilot::new(Select::new().options(vec!["First", "Second", "Third"]), 20, 10);
pilot.focus_first();
pilot.press(Key::Char(' ')).unwrap();
pilot.press(Key::Down).unwrap();
assert!(has_text(pilot.frame(), "Second"), "navigated list should show Second");
}
#[test]
fn input_types_characters() {
let mut pilot = Pilot::new(Input::new(), 30, 3);
pilot.focus_first();
pilot.press(Key::Char('h')).unwrap();
pilot.press(Key::Char('i')).unwrap();
assert!(has_text(pilot.frame(), "hi"), "should show typed characters");
}
#[test]
fn input_backspace_removes_char() {
let mut pilot = Pilot::new(Input::new(), 30, 3);
pilot.focus_first();
pilot.press(Key::Char('x')).unwrap();
pilot.press(Key::Backspace).unwrap();
assert!(!has_text(pilot.frame(), "x"), "backspace should remove character");
}
#[test]
fn input_shows_placeholder() {
let pilot = Pilot::new(Input::new().placeholder("Enter name"), 30, 3);
assert!(has_text(pilot.frame(), "Enter name"), "should show placeholder when empty and unfocused");
}
#[test]
fn input_enter_triggers_submit() {
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
let mut pilot = Pilot::new(
Input::new().placeholder("test").on_submit(move |text| {
let _ = tx.send(text.to_string());
}),
30, 3,
);
pilot.focus_first();
pilot.press(Key::Char('y')).unwrap();
pilot.press(Key::Enter).unwrap();
let submitted = rx.try_recv().ok();
assert_eq!(submitted, Some("y".to_string()), "submit callback should receive text");
}
#[test]
fn input_ignores_ctrl_key_combos() {
let mut pilot = Pilot::new(Input::new(), 30, 3);
pilot.focus_first();
pilot.press(Key::Char('a')).unwrap();
assert!(has_text(pilot.frame(), "a"), "regular chars should still work");
}
#[test]
fn dialog_enter_confirms() {
let mut pilot = Pilot::new(
Dialog::new(Label::new("content")).border(Border::Rounded),
30, 10,
);
pilot.focus_first();
pilot.press(Key::Enter).unwrap();
assert!(has_text(pilot.frame(), "Enter"), "footer should be visible");
}
#[test]
fn dialog_esc_cancels() {
let mut pilot = Pilot::new(
Dialog::new(Label::new("test")).border(Border::Rounded),
30, 10,
);
pilot.focus_first();
pilot.press(Key::Esc).unwrap();
assert!(has_text(pilot.frame(), "test"), "dialog content should still render");
}
#[test]
fn spinner_advances_on_tick() {
let mut pilot = Pilot::new(Spinner::new("loading"), 20, 3);
let frame0: String = pilot.frame().cells.iter().map(|c| c.symbol.as_str()).collect();
pilot.send_event(Event::Tick).unwrap();
let frame1: String = pilot.frame().cells.iter().map(|c| c.symbol.as_str()).collect();
assert_ne!(frame0, frame1, "spinner frame should change on Tick");
}
#[test]
fn progressbar_shows_ratio() {
let pilot = Pilot::new(ProgressBar::new().ratio(0.5).width(20).label(true), 22, 3);
assert!(has_text(pilot.frame(), "50%"), "should show percentage when label enabled");
}
#[test]
fn progressbar_without_label() {
let pilot = Pilot::new(ProgressBar::new().ratio(0.75).width(10), 15, 3);
assert!(has_text(pilot.frame(), "█"), "should render filled blocks");
}
#[test]
fn virtuallist_renders_items() {
let items: Vec<String> = (0..5).map(|i| format!("item-{}", i)).collect();
let pilot = Pilot::new(VirtualList::new(items), 20, 5);
assert!(has_text(pilot.frame(), "item-0"), "should render first item");
assert!(has_text(pilot.frame(), "item-4"), "should render last item within viewport");
}
#[test]
fn virtuallist_down_selects_next() {
let items: Vec<String> = (0..10).map(|i| format!("row-{}", i)).collect();
let mut pilot = Pilot::new(
VirtualList::new(items).highlight_symbol("> "),
20, 10,
);
assert!(!has_text(pilot.frame(), "> "), "no highlight when nothing selected");
pilot.press(Key::Down).unwrap();
assert!(has_text(pilot.frame(), "> "), "highlight should appear after Down");
}
#[test]
fn scroll_content_is_clipped() {
let label = Label::new("line1\nline2\nline3\nline4\nline5\nline6");
let mut pilot = Pilot::new(Scroll::new(label), 20, 3);
pilot.press(Key::Down).unwrap();
pilot.press(Key::Down).unwrap();
assert!(!has_text(pilot.frame(), "line1"), "scrolled content should be clipped");
}
#[test]
fn scroll_pagedown_moves_by_viewport() {
let lines: Vec<String> = (0..20).map(|i| format!("L{:02}", i)).collect();
let text = lines.join("\n");
let mut pilot = Pilot::new(Scroll::new(Label::new(text)), 10, 5);
pilot.press(Key::PageDown).unwrap();
assert!(!has_text(pilot.frame(), "L00"), "L00 scrolled off after PageDown");
}
#[test]
fn table_renders_headers() {
let t = Table::new()
.columns(vec![
TableColumn { title: Text::from("Name"), width: ColumnWidth::Flex(1), align: TextAlign::Left },
TableColumn { title: Text::from("Size"), width: ColumnWidth::Fixed(6), align: TextAlign::Right },
])
.rows_simple(vec![
vec!["file.rs", "1.2KB"],
vec!["main.rs", "3.4KB"],
]);
let pilot = Pilot::new(t, 30, 8);
assert!(has_text(pilot.frame(), "Name"), "should show header");
assert!(has_text(pilot.frame(), "file.rs"), "should show row data");
}
#[test]
fn table_down_selects_row() {
let t = Table::new()
.columns(vec![
TableColumn { title: Text::from("Col"), width: ColumnWidth::Flex(1), align: TextAlign::Left },
])
.rows_simple(vec![vec!["A"], vec!["B"], vec!["C"]]);
let mut pilot = Pilot::new(t, 20, 8);
pilot.press(Key::Down).unwrap();
assert!(has_text(pilot.frame(), "Col"), "table should still render after navigation");
}
#[test]
fn diffview_parses_and_renders() {
let diff = "diff --git a/file b/file\n--- a/file\n+++ b/file\n@@ -1,3 +1,3 @@\n-old\n+new\n ctx";
let pilot = Pilot::new(DiffView::new(diff), 40, 10);
assert!(has_text(pilot.frame(), "old"), "should show removed lines");
assert!(has_text(pilot.frame(), "new"), "should show added lines");
}
#[test]
fn diffview_scrolls_down() {
let lines: Vec<String> = (0..20).map(|i| format!(" line_{}", i)).collect();
let diff = format!("--- a\n+++ b\n@@ -1,20 +1,20 @@\n{}", lines.join("\n"));
let mut pilot = Pilot::new(DiffView::new(&diff), 40, 8);
for _ in 0..5 {
pilot.press(Key::Down).unwrap();
}
assert!(!has_text(pilot.frame(), "line_0"), "first line scrolled off");
}
#[test]
fn splitpane_renders_both_children() {
let mut pilot = Pilot::new(
SplitPane::new()
.first(Label::new("LEFT"))
.second(Label::new("RIGHT"))
.ratio(50),
40, 10,
);
pilot.focus_first();
assert!(has_text(pilot.frame(), "LEFT"), "should show first child");
assert!(has_text(pilot.frame(), "RIGHT"), "should show second child");
}
#[test]
fn splitpane_ratio_change() {
let mut pilot = Pilot::new(
SplitPane::new()
.first(Label::new("AAA"))
.second(Label::new("BBB"))
.direction(SplitDirection::Horizontal)
.ratio(50),
50, 10,
);
pilot.press_with_modifiers(Key::Left, Modifiers { ctrl: true, alt: false, shift: false }).unwrap();
assert!(has_text(pilot.frame(), "AAA"), "first child still visible after resize");
assert!(has_text(pilot.frame(), "BBB"), "second child still visible after resize");
}
#[test]
fn block_renders_border_and_title() {
let pilot = Pilot::new(
Block::new(Label::new("inner"))
.border(Border::Rounded)
.title("My Title"),
30, 8,
);
assert!(has_text(pilot.frame(), "My Title"), "should render title");
assert!(has_text(pilot.frame(), "inner"), "should render child");
}
#[test]
fn overlay_shows_foreground_when_active() {
let mut pilot = Pilot::new(
Overlay::new(
Label::new("background"),
Label::new("POPUP"),
),
30, 10,
);
pilot.focus_first();
assert!(has_text(pilot.frame(), "background"), "should show background initially");
}
#[test]
fn label_renders_multiline() {
let pilot = Pilot::new(Label::new("line1\nline2\nline3"), 20, 5);
assert!(has_text(pilot.frame(), "line1"), "should render first line");
assert!(has_text(pilot.frame(), "line3"), "should render third line");
}
#[test]
fn label_renders_styled_text() {
let text = Text::from(Line::from("hello".red().bold()));
let pilot = Pilot::new(Label::new(text), 20, 3);
assert!(has_text(pilot.frame(), "hello"), "should render styled label text");
}
#[test]
fn column_stacks_children() {
let pilot = Pilot::new(
Column::new()
.child(Label::new("TOP"))
.child(Label::new("BOTTOM"))
.gap(0),
20, 5,
);
let cells: String = pilot.frame().cells.iter().map(|c| c.symbol.as_str()).collect();
let top_pos = cells.find("TOP").unwrap();
let bottom_pos = cells.find("BOTTOM").unwrap();
assert!(top_pos < bottom_pos, "TOP should be above BOTTOM");
}
#[test]
fn row_arranges_horizontally() {
let pilot = Pilot::new(
Row::new()
.child(Label::new("LEFT"))
.child(Label::new("RIGHT"))
.gap(1),
30, 3,
);
let cells: String = pilot.frame().cells.iter().map(|c| c.symbol.as_str()).collect();
let left_pos = cells.find("LEFT").unwrap();
let right_pos = cells.find("RIGHT").unwrap();
assert!(left_pos < right_pos, "LEFT should be before RIGHT");
}