use super::*;
use insta::assert_snapshot;
use ratatui::{backend::TestBackend, Terminal};
fn render_stateful(name: &str, popup: ChoosePopup<'_>, state: &mut ChoosePopupState) {
let backend = TestBackend::new(60, 20);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| f.render_stateful_widget(popup, f.area(), state))
.unwrap();
assert_snapshot!(name, terminal.backend());
}
fn make_items() -> Vec<(String, Style)> {
vec![
("Manzana".to_string(), Style::default()),
("Banana".to_string(), Style::default()),
("Cereza".to_string(), Style::default()),
("Durazno".to_string(), Style::default()),
]
}
#[test]
fn default_state_cursor_is_zero() {
let state = ChoosePopupState::default();
assert_eq!(state.cursor, 0);
}
#[test]
fn default_state_chosen_indices_empty() {
let state = ChoosePopupState::default();
assert!(state.chosen_indices.is_empty());
}
#[test]
fn default_state_max_selected_none() {
let state = ChoosePopupState::default();
assert!(state.max_selected.is_none());
}
#[test]
fn default_state_show_filter_false() {
let state = ChoosePopupState::default();
assert!(!state.show_filter);
}
#[test]
fn toggle_adds_new_index() {
let mut state = ChoosePopupState::default();
state.toggle(2);
assert_eq!(state.chosen_indices.len(), 1);
assert!(state.chosen_indices.contains(&2));
}
#[test]
fn toggle_removes_existing_index() {
let mut state = ChoosePopupState::default();
state.toggle(1);
state.toggle(1);
assert!(state.chosen_indices.is_empty());
}
#[test]
fn toggle_respects_max_selected() {
let mut state = ChoosePopupState { max_selected: Some(1), ..Default::default() };
state.toggle(0);
assert!(state.chosen_indices.contains(&0));
state.toggle(1);
assert!(!state.chosen_indices.contains(&1));
state.toggle(0);
assert!(!state.chosen_indices.contains(&0));
}
#[test]
fn toggle_cursor_toggles_current_cursor() {
let mut state = ChoosePopupState { cursor: 3, ..Default::default() };
state.toggle_cursor();
assert!(state.chosen_indices.contains(&3));
state.toggle_cursor();
assert!(!state.chosen_indices.contains(&3));
}
#[test]
fn toggle_selected_uses_original_index() {
let mut state = ChoosePopupState { cursor: 1, ..Default::default() };
let items = make_items();
state.toggle_selected(&items);
assert!(state.chosen_indices.contains(&1));
}
#[test]
fn toggle_selected_with_filter_maps_to_original() {
let mut state = ChoosePopupState {
cursor: 0,
show_filter: true,
text_input: TextInputState { content: "z".to_string(), cursor: 0 },
..Default::default()
};
let items = vec![
("alfa".to_string(), Style::default()),
("bravo".to_string(), Style::default()),
("zulu".to_string(), Style::default()),
];
state.toggle_selected(&items);
assert!(state.chosen_indices.contains(&2));
}
#[test]
fn next_advances_cursor() {
let mut state = ChoosePopupState { cursor: 0, ..Default::default() };
state.next(3);
assert_eq!(state.cursor, 1);
}
#[test]
fn next_stays_at_last() {
let mut state = ChoosePopupState { cursor: 2, ..Default::default() };
state.next(3);
assert_eq!(state.cursor, 2);
}
#[test]
fn next_empty_items_does_nothing() {
let mut state = ChoosePopupState { cursor: 0, ..Default::default() };
state.next(0);
assert_eq!(state.cursor, 0);
}
#[test]
fn previous_decrements_cursor() {
let mut state = ChoosePopupState { cursor: 2, ..Default::default() };
state.previous();
assert_eq!(state.cursor, 1);
}
#[test]
fn previous_clamps_at_zero() {
let mut state = ChoosePopupState { cursor: 0, ..Default::default() };
state.previous();
assert_eq!(state.cursor, 0);
}
#[test]
fn first_sets_cursor_to_zero() {
let mut state = ChoosePopupState { cursor: 5, ..Default::default() };
state.first();
assert_eq!(state.cursor, 0);
}
#[test]
fn last_sets_cursor_to_last_index() {
let mut state = ChoosePopupState::default();
state.last(3);
assert_eq!(state.cursor, 2);
}
#[test]
fn last_with_zero_items_does_nothing() {
let mut state = ChoosePopupState::default();
state.last(0);
assert_eq!(state.cursor, 0);
}
#[test]
fn filtered_indices_matches_substring_case_insensitive() {
let state = ChoosePopupState {
text_input: TextInputState { content: "ba".to_string(), cursor: 0 },
..Default::default()
};
let items = vec![
("Banana".to_string(), Style::default()),
("Cereza".to_string(), Style::default()),
("BAT".to_string(), Style::default()),
];
assert_eq!(state.filtered_indices(&items), vec![0, 2]);
}
#[test]
fn filtered_indices_returns_empty_when_no_match() {
let state = ChoosePopupState {
text_input: TextInputState { content: "xyz".to_string(), cursor: 0 },
..Default::default()
};
assert!(state.filtered_indices(&make_items()).is_empty());
}
#[test]
fn visible_count_uses_all_when_no_filter() {
let items = make_items();
let state = ChoosePopupState { show_filter: true, ..Default::default() };
assert_eq!(state.visible_count(&items), 4);
}
#[test]
fn visible_count_uses_filtered_when_filter_active() {
let state = ChoosePopupState {
show_filter: true,
text_input: TextInputState { content: "an".to_string(), cursor: 0 },
..Default::default()
};
assert_eq!(state.visible_count(&make_items()), 2);
}
#[test]
fn original_index_passes_through_when_no_filter() {
let state = ChoosePopupState { cursor: 3, ..Default::default() };
assert_eq!(state.original_index(&make_items()), Some(3));
}
#[test]
fn original_index_maps_filtered_to_original() {
let state = ChoosePopupState {
cursor: 0,
show_filter: true,
text_input: TextInputState { content: "an".to_string(), cursor: 0 },
..Default::default()
};
assert_eq!(state.original_index(&make_items()), Some(0));
}
#[test]
fn original_index_returns_none_when_outside_filtered() {
let state = ChoosePopupState {
cursor: 99,
show_filter: true,
text_input: TextInputState { content: "an".to_string(), cursor: 0 },
..Default::default()
};
assert_eq!(state.original_index(&make_items()), None);
}
#[test]
fn insert_filter_char_appends_to_input() {
let mut state = ChoosePopupState::default();
state.insert_filter_char('x');
assert_eq!(state.text_input.content, "x");
}
#[test]
fn delete_before_filter_removes_last_char() {
let mut state = ChoosePopupState {
text_input: TextInputState { content: "ab".to_string(), cursor: 2 },
..Default::default()
};
state.delete_before_filter();
assert_eq!(state.text_input.content, "a");
}
#[test]
fn filter_cursor_left_moves_left() {
let mut state = ChoosePopupState {
text_input: TextInputState { content: "abc".to_string(), cursor: 2 },
..Default::default()
};
state.filter_cursor_left();
assert_eq!(state.text_input.cursor, 1);
}
#[test]
fn item_at_returns_none_above_content() {
let popup = ChoosePopup::new(make_items());
let state = ChoosePopupState::default();
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 0), None);
}
#[test]
fn item_at_returns_none_in_filter_area() {
let popup = ChoosePopup::new(make_items());
let state = ChoosePopupState { show_filter: true, ..Default::default() };
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 1), None);
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 2), None);
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 3), None);
}
#[test]
fn item_at_returns_index_in_list_area() {
let popup = ChoosePopup::new(make_items());
let state = ChoosePopupState::default();
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 1), Some(0));
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 2), Some(1));
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 3), Some(2));
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 4), Some(3));
}
#[test]
fn item_at_returns_none_outside_bounds() {
let popup = ChoosePopup::new(make_items());
let state = ChoosePopupState::default();
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 5), None);
}
#[test]
fn item_at_uses_zero_offset_by_default() {
let popup = ChoosePopup::new(make_items());
let state = ChoosePopupState::default();
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 1), Some(0));
}
#[test]
fn item_at_uses_filtered_when_filter_active() {
let popup = ChoosePopup::new(make_items());
let state = ChoosePopupState {
show_filter: true,
text_input: TextInputState { content: "an".to_string(), cursor: 0 },
..Default::default()
};
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 4), Some(0));
assert_eq!(popup.item_at(&state, Rect::new(0, 0, 60, 20), 5), Some(1));
}
#[test]
fn resolve_rect_delegates_to_popup() {
let popup = ChoosePopup::new(make_items())
.width(PopupSize::Fixed(30))
.height(PopupSize::Fixed(10));
let rect = popup.resolve_rect(Rect::new(0, 0, 100, 50));
assert_eq!(rect.width, 30);
assert_eq!(rect.height, 10);
}
#[test]
fn resolve_rect_clamps_when_larger_than_area() {
let popup = ChoosePopup::new(make_items())
.width(PopupSize::Fixed(100))
.height(PopupSize::Fixed(50));
let rect = popup.resolve_rect(Rect::new(0, 0, 50, 20));
assert_eq!(rect.width, 50);
assert_eq!(rect.height, 20);
}
#[test]
fn snapshot_default() {
let popup = ChoosePopup::new(make_items());
let mut state = ChoosePopupState::default();
render_stateful("choose_default", popup, &mut state);
}
#[test]
fn snapshot_with_title() {
let popup = ChoosePopup::new(make_items()).title("Seleccionar fruta");
let mut state = ChoosePopupState::default();
render_stateful("choose_with_title", popup, &mut state);
}
#[test]
fn snapshot_with_selection() {
let popup = ChoosePopup::new(make_items()).title("Frutas");
let mut state = ChoosePopupState {
chosen_indices: HashSet::from([0, 2]),
..Default::default()
};
render_stateful("choose_with_selection", popup, &mut state);
}
#[test]
fn snapshot_with_filter_active() {
let popup = ChoosePopup::new(make_items()).title("Filtrar fruta");
let mut state = ChoosePopupState {
show_filter: true,
text_input: TextInputState {
content: "an".to_string(),
cursor: 2,
},
..Default::default()
};
render_stateful("choose_with_filter", popup, &mut state);
}
#[test]
fn snapshot_max_selected_reached() {
let popup = ChoosePopup::new(make_items())
.title("Elegí una")
.max_selected(1);
let mut state = ChoosePopupState {
chosen_indices: HashSet::from([0]),
..Default::default()
};
render_stateful("choose_max_selected", popup, &mut state);
}
#[test]
fn snapshot_filtered_and_selected() {
let popup = ChoosePopup::new(make_items()).title("Buscar");
let mut state = ChoosePopupState {
show_filter: true,
chosen_indices: HashSet::from([0]),
text_input: TextInputState {
content: "a".to_string(),
cursor: 1,
},
..Default::default()
};
render_stateful("choose_filtered_and_selected", popup, &mut state);
}
#[test]
fn snapshot_border_color() {
let popup = ChoosePopup::new(make_items())
.title("Rojo")
.border_color(Color::Red);
let mut state = ChoosePopupState::default();
render_stateful("choose_border_color", popup, &mut state);
}
#[test]
fn snapshot_positioned() {
let popup = ChoosePopup::new(make_items())
.title("Posicionado")
.position(5, 5);
let mut state = ChoosePopupState::default();
let backend = TestBackend::new(60, 20);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
f.render_stateful_widget(popup, f.area(), &mut state);
})
.unwrap();
assert_snapshot!("choose_positioned", terminal.backend());
}
#[test]
fn snapshot_bg_color() {
let popup = ChoosePopup::new(make_items())
.title("Fondo azul")
.bg_color(Color::Blue);
let mut state = ChoosePopupState::default();
render_stateful("choose_bg_color", popup, &mut state);
}
#[test]
fn snapshot_bg_color_with_filter() {
let popup = ChoosePopup::new(make_items())
.title("Buscar")
.bg_color(Color::Blue);
let mut state = ChoosePopupState {
show_filter: true,
text_input: TextInputState {
content: "a".to_string(),
cursor: 1,
},
..Default::default()
};
render_stateful("choose_bg_color_with_filter", popup, &mut state);
}