use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use pretty_assertions::assert_eq;
use super::{inline_choice_frame, InlineChoice, InlineChoiceKeyOutcome, InlineChoiceOption};
use crate::tui::composer_pointer::ComposerChoice;
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn navigation_skips_unavailable_options() {
let mut choice = InlineChoice::new(
"choose",
"details",
vec![
InlineChoiceOption::unavailable("first", '1', "first", "unavailable"),
InlineChoiceOption::available("second", '2', "second", "available"),
InlineChoiceOption::available("third", '3', "third", "available"),
],
)
.unwrap();
assert_eq!(choice.selected_value(), "second");
assert_eq!(
choice.handle_key(key(KeyCode::Down)),
InlineChoiceKeyOutcome::Handled
);
assert_eq!(choice.selected_value(), "third");
assert_eq!(
choice.handle_key(key(KeyCode::Up)),
InlineChoiceKeyOutcome::Handled
);
assert_eq!(choice.selected_value(), "second");
}
#[test]
fn shortcut_selects_and_submits_option() {
for (case, modifiers) in [
("plain", KeyModifiers::NONE),
("shift", KeyModifiers::SHIFT),
] {
let mut choice = InlineChoice::new(
"choose",
"details",
vec![
InlineChoiceOption::available("compact", '1', "compact", "first"),
InlineChoiceOption::available("direct", '2', "direct", "second"),
],
)
.unwrap();
assert_eq!(
choice.handle_key(KeyEvent::new(KeyCode::Char('2'), modifiers)),
InlineChoiceKeyOutcome::Selected("direct".into()),
"{case}"
);
assert_eq!(choice.selected_value(), "direct", "{case}");
assert_eq!(
choice.handle_key(key(KeyCode::Esc)),
InlineChoiceKeyOutcome::Cancelled,
"{case}"
);
}
}
#[test]
fn option_hits_tile_each_available_option_group() {
let options = vec![
InlineChoiceOption::available("keep", '1', "Keep", "Leave everything as it is"),
InlineChoiceOption::unavailable("locked", '2', "Locked option", "Needs a login first"),
InlineChoiceOption::available(
"delete",
'3',
"Delete every saved session in this directory",
"Permanently removes transcripts and cached web content",
),
InlineChoiceOption::available("later", '4', "Decide later", ""),
];
let choice = InlineChoice::new("Delete sessions?", "", options.clone()).unwrap();
let markers = ['→', '·', ' '];
let option_at = |row: &str| {
let row = row.trim_start_matches(markers);
options
.iter()
.position(|option| row.starts_with(&format!("{} ", option.shortcut)))
};
for (width, wraps) in [(80, false), (24, true)] {
let frame = inline_choice_frame(&choice, width, false);
let rows: Vec<String> = frame
.lines
.iter()
.map(|line| {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
})
.collect();
let hits: Vec<_> = frame
.choice_hits
.iter()
.map(|hit| match hit.target {
ComposerChoice::InlineChoice(index) => (index, hit.lines.clone()),
ComposerChoice::Questionnaire(_)
| ComposerChoice::Approval(_)
| ComposerChoice::PickerRow(_) => {
panic!("width {width}: foreign target {:?}", hit.target)
}
})
.collect();
assert_eq!(
hits.iter().map(|(index, _)| *index).collect::<Vec<_>>(),
vec![0, 2, 3],
"width {width}"
);
assert_eq!(
hits.iter().any(|(_, lines)| lines.len() > 2),
wraps,
"width {width}: fixture wrap expectation"
);
for (index, lines) in &hits {
let option = &options[*index];
assert_eq!(
option_at(&rows[lines.start]),
Some(*index),
"width {width}: option {index} starts off its marker row"
);
assert!(
rows[lines.clone()].iter().all(|row| !row.trim().is_empty()),
"width {width}: option {index} span includes a blank separator"
);
let painted: Vec<&str> = rows[lines.clone()]
.iter()
.enumerate()
.flat_map(|(offset, row)| {
let text = if offset == 0 {
row.trim_start_matches(markers)
.trim_start_matches(option.shortcut)
} else {
row.as_str()
};
text.split_whitespace()
})
.collect();
let expected: Vec<&str> = option
.label
.split_whitespace()
.chain(option.detail.split_whitespace())
.collect();
assert_eq!(painted, expected, "width {width}: option {index}");
}
}
}