use super::*;
fn state() -> PickerState {
PickerState::new(
["CSV", "Parquet", "JSON", "NDJSON", "Arrow", "Avro"]
.iter()
.map(|s| s.to_string())
.collect(),
)
}
#[test]
fn typing_narrows_and_backspace_widens() {
let mut s = state();
s.type_char('a');
let names: Vec<&str> = s.filtered().iter().map(|(_, n)| *n).collect();
assert_eq!(names, ["Arrow", "Avro", "Parquet"], "starts first");
s.type_char('r');
let names: Vec<&str> = s.filtered().iter().map(|(_, n)| *n).collect();
assert_eq!(
names,
["Arrow", "Parquet"],
"matches anywhere, ignoring case"
);
s.backspace();
s.backspace();
assert_eq!(s.filtered().len(), 6);
}
#[test]
fn narrowing_keeps_the_cursor_on_its_item_when_it_survives() {
let mut s = state();
s.select_original(4); s.type_char('r');
assert_eq!(
s.selected_original(),
Some(4),
"Arrow matches 'r' and keeps the cursor"
);
s.type_char('q');
assert_eq!(
s.selected_original(),
Some(1),
"'rq' filters Arrow away, so the cursor lands on the first match"
);
}
#[test]
fn movement_walks_the_visible_items_and_wraps() {
let mut s = state();
s.type_char('a'); s.move_down();
assert_eq!(s.selected_original(), Some(5));
s.move_down();
assert_eq!(s.selected_original(), Some(1));
s.move_down();
assert_eq!(s.selected_original(), Some(4), "wraps to the top");
s.move_up();
assert_eq!(s.selected_original(), Some(1), "and back around");
}
#[test]
fn a_name_typed_whole_ranks_first_and_takes_the_cursor() {
let mut s = PickerState::new(
["time_hour", "dep_delay", "Hour", "hours"]
.iter()
.map(|s| s.to_string())
.collect(),
);
s.select_original(0);
for c in "hour".chars() {
s.type_char(c);
}
let names: Vec<&str> = s.filtered().iter().map(|(_, n)| *n).collect();
assert_eq!(names, ["Hour", "hours", "time_hour"]);
assert_eq!(s.selected_original(), Some(2));
s.backspace();
assert_eq!(
s.selected_original(),
Some(2),
"kept while it still matches"
);
}
#[test]
fn a_filter_that_admits_nothing_chooses_nothing_and_never_panics() {
let mut s = state();
for c in "zzz".chars() {
s.type_char(c);
}
assert_eq!(s.selected_original(), None);
s.move_down();
s.move_up();
assert_eq!(s.selected_original(), None);
}
fn render_rows(picker: &Picker, width: u16, height: u16) -> Vec<String> {
let ctx = RenderContext::for_test();
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
picker.render(area, &mut buf, &ctx);
(0..height)
.map(|y| {
(0..width)
.map(|x| buf[(x, y)].symbol().to_string())
.collect::<String>()
})
.collect()
}
#[test]
fn the_selection_carries_the_rail_and_only_the_selection() {
let g = crate::glyphs::get();
let picker = Picker::new(vec!["CSV", "Parquet", "JSON"], Some(1), true);
let rows = render_rows(&picker, 20, 3);
assert!(
rows[1].starts_with(&format!("{}Parquet", g.rail)),
"got {rows:?}"
);
assert!(rows[0].starts_with(" CSV"), "got {rows:?}");
assert!(rows[2].starts_with(" JSON"), "got {rows:?}");
}
#[test]
fn an_unfocused_selection_stays_visible_without_the_rail() {
let g = crate::glyphs::get();
let picker = Picker::new(vec!["CSV", "Parquet"], Some(0), false);
let rows = render_rows(&picker, 20, 2);
assert!(
rows[0].starts_with(&format!("{}CSV", g.middot)),
"the choice keeps a glyph without focus: {rows:?}"
);
assert!(
!rows[0].starts_with(g.rail),
"but never the rail, which means focus: {rows:?}"
);
let ctx = RenderContext::for_test();
let area = Rect::new(0, 0, 20, 2);
let mut buf = Buffer::empty(area);
picker.render(area, &mut buf, &ctx);
assert_ne!(
buf[(1, 0)].fg,
buf[(1, 1)].fg,
"the chosen item still reads apart from the rest"
);
}
#[test]
fn overflow_is_counted_on_the_last_row() {
let picker = Picker::new(vec!["a", "b", "c", "d", "e"], Some(0), true);
let rows = render_rows(&picker, 20, 3);
assert!(rows[2].contains("3 more"), "got {rows:?}");
}
#[test]
fn marks_draw_a_checkbox_on_every_item() {
let g = crate::glyphs::get();
let picker = Picker::new(vec!["dept", "region"], Some(0), true).marks(vec![true, false]);
let rows = render_rows(&picker, 20, 2);
assert!(
rows[0].contains(&format!("{} dept", g.checkbox_on)),
"got {rows:?}"
);
assert!(
rows[1].contains(&format!("{} region", g.checkbox_off)),
"got {rows:?}"
);
}
#[test]
fn scrolling_keeps_the_selection_in_view() {
let picker = Picker::new(vec!["a", "b", "c", "d", "e"], Some(4), true);
let g = crate::glyphs::get();
let rows = render_rows(&picker, 20, 3);
assert!(
rows[2].starts_with(&format!("{}e", g.rail)),
"the selected last item is drawn, not the overflow count: {rows:?}"
);
}
#[test]
fn the_filter_keeps_readline_chords_out_of_the_text() {
use crossterm::event::KeyModifiers;
let mut p = PickerState::new(vec!["first_name".to_string(), "start date".to_string()]);
for c in "start d".chars() {
p.filter_key(c, KeyModifiers::NONE);
}
assert_eq!(p.filter, "start d");
p.filter_key('w', KeyModifiers::CONTROL);
assert_eq!(p.filter, "start ", "Ctrl+W drops the word, not types w");
p.filter_key('u', KeyModifiers::CONTROL);
assert_eq!(p.filter, "", "Ctrl+U clears, not types u");
p.filter_key('x', KeyModifiers::ALT);
assert_eq!(p.filter, "", "an Alt chord is not a letter");
}