use super::*;
fn approval(p: ChoicePrompt) -> ChoicePrompt {
p.option_key("approve", "Approve", 'a')
.option_key("all", "Approve all", 'A')
.option_key("deny", "Deny", 'd')
.danger("deny")
}
#[test]
fn option_letters_commit_single_and_are_case_sensitive() {
let mut r = rig_sized(Size::new(56, 16));
let (outcomes, _h) = open_on(&r, approval);
let text = r.modal_text();
assert!(text.contains("(a)"), "letter renders in the row: {text}");
assert!(
text.contains("a/A/d pick"),
"hint names the letters (F6/K2): {text}"
);
r.key(Key::Char('A'));
assert_eq!(
outcomes.borrow().as_slice(),
[ChoiceOutcome::Answered(ChoiceAnswer {
selected: vec!["all".into()],
other: None,
})],
"uppercase letter is its own key (case-sensitive)"
);
assert!(!r.is_open(), "letter commit closed the gate");
let (outcomes, _h) = open_on(&r, approval);
r.key(Key::Char('a'));
assert_eq!(
outcomes.borrow().as_slice(),
[ChoiceOutcome::Answered(ChoiceAnswer {
selected: vec!["approve".into()],
other: None,
})]
);
}
#[test]
fn option_letters_toggle_in_multiple_and_declared_key_beats_digit() {
let mut r = rig();
let (outcomes, _h) = open_on(&r, |p| approval(p).allow_multiple(true));
r.key(Key::Char('a'));
r.key(Key::Char('d'));
let text = r.modal_text();
assert!(
text.contains("☑ Approve") && text.contains("☑ Deny"),
"letters jump-toggle in multiple mode: {text}"
);
assert!(outcomes.borrow().is_empty(), "toggles never resolve");
r.key(Key::Char('d')); assert!(r.modal_text().contains("☐ Deny"));
r.key(Key::Enter);
assert_eq!(
outcomes.borrow().as_slice(),
[ChoiceOutcome::Answered(ChoiceAnswer {
selected: vec!["approve".into()],
other: None,
})]
);
let mut r = rig();
let (outcomes, _h) = open_on(&r, |p| {
p.option("a", "Alpha")
.option_key("b", "Beta", '2')
.option("c", "Gamma")
});
r.key(Key::Char('2'));
assert_eq!(
outcomes.borrow().as_slice(),
[ChoiceOutcome::Answered(ChoiceAnswer {
selected: vec!["b".into()],
other: None,
})],
"declared keys outrank the movement-only digit jump"
);
}
#[test]
fn kitty_shift_spelling_folds_to_the_uppercase_option_key() {
let mut r = rig_sized(Size::new(56, 16));
let (outcomes, _h) = open_on(&r, approval);
r.send(&UiEvent::Key(KeyEvent::new(
Key::Char('a'),
crate::ui::Mods::SHIFT,
)));
assert_eq!(
outcomes.borrow().as_slice(),
[ChoiceOutcome::Answered(ChoiceAnswer {
selected: vec!["all".into()],
other: None,
})],
"Char('a')+SHIFT is the kitty spelling of the 'A' key (0288)"
);
let (outcomes, _h) = open_on(&r, |p| {
p.option_key("approve", "Approve", 'a')
.option("hold", "Hold")
});
r.send(&UiEvent::Key(KeyEvent::new(
Key::Char('a'),
crate::ui::Mods::SHIFT,
)));
assert!(
outcomes.borrow().is_empty(),
"Shift+letter must never fire a lowercase-declared key"
);
r.key(Key::Char('a')); assert_eq!(
outcomes.borrow().as_slice(),
[ChoiceOutcome::Answered(ChoiceAnswer {
selected: vec!["approve".into()],
other: None,
})]
);
}
#[test]
fn option_letters_type_into_a_focused_other_editor() {
let mut r = rig();
let (outcomes, _h) = open_on(&r, |p| approval(p).allow_other("Other…"));
r.key(Key::End); r.type_str("ad"); assert!(outcomes.borrow().is_empty(), "no activation while editing");
assert!(
r.modal_text().contains("ad"),
"letters landed in the draft: {}",
r.modal_text()
);
}
#[test]
fn non_dismissable_esc_refuses_visibly_and_clears_on_action() {
let mut r = rig();
let (outcomes, handle) = open_on(&r, |p| basic(p).dismissable(false));
let text = r.modal_text();
assert!(!text.contains("Cancel"), "no Cancel button: {text}");
assert!(
!text.contains("Esc cancels"),
"no advertised Esc — a dead key would lie: {text}"
);
r.key(Key::Escape);
assert!(outcomes.borrow().is_empty(), "Esc refused — no resolution");
assert!(r.is_open(), "gate still open");
assert!(
r.modal_text().contains("an answer is required"),
"refusal is VISIBLE (charter G3): {}",
r.modal_text()
);
r.key(Key::Down); assert!(
!r.modal_text().contains("an answer is required"),
"note clears once the user acts: {}",
r.modal_text()
);
handle.cancel();
flush_effects();
assert_eq!(outcomes.borrow().as_slice(), [ChoiceOutcome::Cancelled]);
assert!(!r.is_open());
}
#[test]
fn esc_in_other_retreats_first_keeps_draft_then_second_esc_cancels() {
let mut r = rig();
let (outcomes, _h) = open_on(&r, |p| basic(p).allow_other("Other…"));
r.key(Key::End); r.type_str("half an answer");
assert!(
r.modal_text().contains("Esc back to the list"),
"hint tells the layered-Esc truth while editing: {}",
r.modal_text()
);
r.key(Key::Escape);
assert!(outcomes.borrow().is_empty(), "first Esc never cancels (F4)");
assert!(r.is_open());
let text = r.modal_text();
assert!(text.contains("half an answer"), "draft survives: {text}");
assert!(text.contains("● Other…"), "still the candidate: {text}");
assert!(
!text.contains("Esc back to the list"),
"editing hint gone after retreat: {text}"
);
r.key(Key::Escape);
assert_eq!(outcomes.borrow().as_slice(), [ChoiceOutcome::Cancelled]);
assert!(!r.is_open());
}
#[test]
fn esc_in_other_retreats_then_refuses_on_a_must_choose_gate() {
let mut r = rig();
let (outcomes, _h) = open_on(&r, |p| basic(p).allow_other("Other…").dismissable(false));
r.key(Key::End);
r.type_str("draft");
r.key(Key::Escape); assert!(outcomes.borrow().is_empty());
assert!(r.is_open());
r.key(Key::Escape); assert!(outcomes.borrow().is_empty(), "must-choose never cancels");
assert!(
r.modal_text().contains("an answer is required"),
"refusal visible: {}",
r.modal_text()
);
}
#[test]
fn danger_option_wears_error_ink_except_under_the_selection_pair() {
let t = crate::theme::default_theme().tokens;
let mut r = rig();
let (_outcomes, _h) = open_on(&r, |p| {
p.option("keep", "Keep everything")
.option("del", "Delete them")
.danger("del")
});
let (fg, bg) = r.ink_at("Delete them").expect("danger row");
assert_eq!(fg, t.error, "danger label rides the Error token (T3)");
assert_eq!(bg, t.overlay);
r.key(Key::Down);
let (fg, bg) = r.ink_at("Delete them").expect("danger row");
assert_eq!((fg, bg), (t.selection_fg, t.selection_bg));
}
#[test]
fn a11y_tree_names_question_options_and_selection_state() {
let mut r = rig();
let (_outcomes, _h) = open_on(&r, |p| basic(p).allow_other("Other…"));
let snap = r.a11y();
assert!(
snap.contains("heading \"Proceed how?\""),
"the question is in the tree (A1):\n{snap}"
);
assert!(
snap.contains("menu \"options\" = \"Alpha\""),
"region names the current choice (A2):\n{snap}"
);
assert!(
snap.contains("menuitem \"Alpha\" = \"selected\""),
"selected option carries its state (A2):\n{snap}"
);
assert!(
snap.contains("menuitem \"Beta\"") && snap.contains("menuitem \"Gamma\""),
"every option label is enumerable (A2/A3):\n{snap}"
);
assert!(
!snap.contains("input"),
"no phantom Other editor before engagement (O1/A4):\n{snap}"
);
r.key(Key::Down);
let snap = r.a11y();
assert!(snap.contains("menuitem \"Beta\" = \"selected\""), "{snap}");
r.key(Key::End);
let snap = r.a11y();
assert!(
snap.contains("input") && snap.contains("[focused]"),
"revealed editor is an Input with focus truth (A4):\n{snap}"
);
}
#[test]
fn a11y_unchanged_for_question_and_options_with_a_body() {
fn semantic_lines(snap: &str) -> Vec<String> {
snap.lines()
.map(|l| match l.find(" @ ") {
Some(cut) => l[..cut].to_string(),
None => l.to_string(),
})
.collect()
}
let r = rig();
let (_o, _h) = open_on(&r, basic);
let bare = semantic_lines(&r.a11y());
let r = rig();
let (_o, _h) = open_on(&r, |p| {
basic(p).body(|_| crate::ui::text("three tool calls, tier: safe"))
});
let snap = r.a11y();
let with_body = semantic_lines(&snap);
for line in &bare {
assert!(
with_body.contains(line),
"bare-gate a11y entry {line:?} must survive a body:\n{snap}"
);
}
for line in &with_body {
assert!(
bare.contains(line) || line.trim_start().starts_with("text "),
"a body adds only display text entries, got {line:?}:\n{snap}"
);
}
assert!(
snap.contains("menu \"options\" = \"Alpha\" [focused]"),
"focus stays anchored on the options with a body present:\n{snap}"
);
assert!(
!snap.contains("input"),
"no phantom editor from the body:\n{snap}"
);
}
#[test]
fn a11y_multiple_mode_reports_checkbox_state() {
let mut r = rig();
let (_outcomes, _h) = open_on(&r, |p| basic(p).allow_multiple(true));
let snap = r.a11y();
assert!(
snap.contains("checkbox \"Alpha\" = \"off\""),
"unchecked state readable (A2):\n{snap}"
);
r.key(Key::Char(' '));
let snap = r.a11y();
assert!(
snap.contains("checkbox \"Alpha\" = \"on\""),
"checked state tracks toggles (A2):\n{snap}"
);
}
#[test]
fn region_focus_affordance_visible_and_unfocused_highlight_distinct() {
let t = crate::theme::default_theme().tokens;
let mut r = rig();
let (_outcomes, _h) = open_on(&r, basic);
let (fg, bg) = r.ink_at("Alpha").expect("row");
assert_eq!((fg, bg), (t.selection_fg, t.selection_bg));
let mut tree = r.modal_tree().expect("modal tree");
assert!(
crate::ui::focus_affordance_visible(&mut tree),
"focused region must change pixels (charter A5)"
);
r.key(Key::Tab);
let (fg, bg) = r.ink_at("Alpha").expect("row");
assert_eq!(
(fg, bg),
(t.accent, t.overlay),
"unfocused highlight is accent ink (the RadioGroup precedent)"
);
let (plain_fg, _) = r.ink_at("Beta").expect("plain row");
assert_ne!(fg, plain_fg, "highlight still distinct unfocused (T2)");
let mut tree = r.modal_tree().expect("modal tree");
assert!(
crate::ui::focus_affordance_visible(&mut tree),
"focused button shows its affordance (charter A5)"
);
}