use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use gwm::tui::keymap::{KeyStroke, Source};
use gwm::tui::modal_keymap::{KeyContext, ModalAction, ModalKeymap};
fn stroke(code: KeyCode) -> KeyStroke {
KeyStroke::new(code, KeyModifiers::empty())
}
fn ch(c: char) -> KeyStroke {
KeyStroke::new(KeyCode::Char(c), KeyModifiers::empty())
}
#[test]
fn config_path_roundtrips_through_from_config_path() {
for ctx in KeyContext::all() {
assert_eq!(
KeyContext::from_config_path(ctx.config_path()),
Some(*ctx),
"config_path {:?} must roundtrip",
ctx.config_path()
);
}
}
#[test]
fn link_stages_use_dotted_config_paths() {
assert_eq!(KeyContext::LinkChooseTarget.config_path(), "link.choose_target");
assert_eq!(KeyContext::LinkInputNumber.config_path(), "link.input_number");
assert_eq!(KeyContext::ConfigEdit.config_path(), "config.edit");
}
#[test]
fn from_context_verb_resolves_known_verbs_and_rejects_unknown() {
assert_eq!(
ModalAction::from_context_verb(KeyContext::Confirm, "confirm"),
Some(ModalAction::ConfirmConfirm)
);
assert_eq!(
ModalAction::from_context_verb(KeyContext::Create, "submit"),
Some(ModalAction::CreateSubmit)
);
assert_eq!(ModalAction::from_context_verb(KeyContext::Confirm, "submit"), None);
assert_eq!(ModalAction::from_context_verb(KeyContext::Create, "confirm"), None);
}
#[test]
fn every_action_reports_its_owning_context() {
for action in ModalAction::all() {
let ctx = action.context();
assert_eq!(
ModalAction::from_context_verb(ctx, action.verb()),
Some(action),
"{:?} must resolve from its own (context, verb)",
action
);
}
}
#[test]
fn defaults_resolve_the_historical_keys() {
let km = ModalKeymap::defaults();
assert_eq!(
km.resolve(KeyContext::Confirm, &ch('y')),
Some(ModalAction::ConfirmConfirm)
);
assert_eq!(
km.resolve(KeyContext::Confirm, &stroke(KeyCode::Enter)),
Some(ModalAction::ConfirmActivate)
);
assert_eq!(
km.resolve(KeyContext::Confirm, &ch('n')),
Some(ModalAction::ConfirmCancel)
);
assert_eq!(
km.resolve(KeyContext::Confirm, &stroke(KeyCode::Esc)),
Some(ModalAction::ConfirmCancel)
);
assert_eq!(
km.resolve(KeyContext::Confirm, &ch('h')),
Some(ModalAction::ConfirmFocusConfirm)
);
assert_eq!(
km.resolve(KeyContext::Confirm, &stroke(KeyCode::Tab)),
Some(ModalAction::ConfirmToggleFocus)
);
assert_eq!(
km.resolve(KeyContext::Create, &stroke(KeyCode::Tab)),
Some(ModalAction::CreateNextField)
);
assert_eq!(
km.resolve(KeyContext::Create, &stroke(KeyCode::BackTab)),
Some(ModalAction::CreatePrevField)
);
assert_eq!(
km.resolve(KeyContext::Create, &ch('l')),
Some(ModalAction::CreateNextType)
);
assert_eq!(
km.resolve(KeyContext::LinkChooseTarget, &ch('i')),
Some(ModalAction::LinkChooseIssue)
);
assert_eq!(
km.resolve(KeyContext::LinkInputNumber, &stroke(KeyCode::Enter)),
Some(ModalAction::LinkInputSubmit)
);
}
#[test]
fn unbound_stroke_resolves_to_none() {
let km = ModalKeymap::defaults();
assert_eq!(km.resolve(KeyContext::Confirm, &ch('z')), None);
assert_eq!(km.resolve(KeyContext::LinkInputNumber, &ch('4')), None);
}
#[test]
fn same_key_means_different_verbs_in_different_contexts() {
let km = ModalKeymap::defaults();
assert_eq!(
km.resolve(KeyContext::Create, &stroke(KeyCode::Enter)),
Some(ModalAction::CreateSubmit)
);
assert_eq!(
km.resolve(KeyContext::Confirm, &stroke(KeyCode::Enter)),
Some(ModalAction::ConfirmActivate)
);
assert_eq!(
km.resolve(KeyContext::LinkChooseTarget, &ch('i')),
Some(ModalAction::LinkChooseIssue)
);
assert_eq!(
km.resolve(KeyContext::OpenMenu, &ch('i')),
Some(ModalAction::OpenMenuIssue)
);
}
#[test]
fn override_replaces_keys_for_a_verb() {
let mut km = ModalKeymap::defaults();
km.apply_override(ModalAction::ConfirmConfirm, vec![ch('o')]).unwrap();
assert_eq!(
km.resolve(KeyContext::Confirm, &ch('o')),
Some(ModalAction::ConfirmConfirm)
);
assert_eq!(km.resolve(KeyContext::Confirm, &ch('y')), None);
}
#[test]
fn override_can_unbind_with_empty_vec() {
let mut km = ModalKeymap::defaults();
km.apply_override(ModalAction::ConfirmConfirm, vec![]).unwrap();
assert_eq!(km.resolve(KeyContext::Confirm, &ch('y')), None);
}
#[test]
fn user_override_wins_over_a_default_in_the_same_context() {
let mut km = ModalKeymap::defaults();
km.apply_override(ModalAction::ConfirmCancel, vec![ch('y'), stroke(KeyCode::Esc)])
.unwrap();
assert_eq!(
km.resolve(KeyContext::Confirm, &ch('y')),
Some(ModalAction::ConfirmCancel)
);
}
#[test]
fn user_vs_user_conflict_in_same_context_is_rejected() {
let mut km = ModalKeymap::defaults();
km.apply_override(ModalAction::ConfirmConfirm, vec![ch('x')]).unwrap();
let err = km
.apply_override(ModalAction::ConfirmCancel, vec![ch('x')])
.expect_err("same key for two verbs in one context must conflict");
let msg = err.to_string();
assert!(msg.contains("conflict"), "message should mention conflict: {msg}");
assert!(msg.contains("confirm"), "message should name the context: {msg}");
}
#[test]
fn cross_context_reuse_is_not_a_conflict() {
let mut km = ModalKeymap::defaults();
km.apply_override(ModalAction::ConfirmConfirm, vec![ch('x')]).unwrap();
km.apply_override(ModalAction::CreateNextType, vec![ch('x')]).unwrap();
assert_eq!(
km.resolve(KeyContext::Confirm, &ch('x')),
Some(ModalAction::ConfirmConfirm)
);
assert_eq!(
km.resolve(KeyContext::Create, &ch('x')),
Some(ModalAction::CreateNextType)
);
}
#[test]
fn toggle_mode_refuses_a_bare_printable_the_text_fields_would_swallow() {
let mut km = ModalKeymap::defaults();
let err = km
.apply_override(ModalAction::CreateToggleMode, vec![ch('x')])
.expect_err("a bare printable must be refused for toggle_mode");
assert!(
err.to_string().contains("reserved for typing"),
"the message must explain why: {err}"
);
km.apply_override(
ModalAction::CreateToggleMode,
vec![KeyStroke::new(KeyCode::Char('y'), KeyModifiers::CONTROL)],
)
.expect("a Ctrl-modified stroke is not typing input");
}
#[test]
fn override_marks_source_as_user_config() {
let mut km = ModalKeymap::defaults();
km.apply_override(ModalAction::ConfirmConfirm, vec![ch('o')]).unwrap();
let binding = km
.list()
.iter()
.find(|b| b.action == ModalAction::ConfirmConfirm)
.unwrap();
assert_eq!(binding.source, Source::UserConfig);
let other = km
.list()
.iter()
.find(|b| b.action == ModalAction::ConfirmCancel)
.unwrap();
assert_eq!(other.source, Source::Default);
}
#[test]
fn parse_single_accepts_one_stroke_rejects_chords() {
use gwm::tui::modal_keymap::parse_single;
assert!(parse_single("Enter").is_ok());
assert!(parse_single("Ctrl+s").is_ok());
let err = parse_single("g g").expect_err("multi-stroke chord must be rejected for modals");
assert!(
err.to_string().contains("single keystroke"),
"message should explain the single-stroke rule: {err}"
);
}
#[test]
fn parse_single_rejects_the_reserved_ctrl_c() {
use gwm::tui::modal_keymap::parse_single;
for s in ["Ctrl+c", "Ctrl+C"] {
let err = parse_single(s).expect_err("Ctrl+C must be rejected as reserved");
assert!(
err.to_string().contains("Ctrl+C") || err.to_string().to_lowercase().contains("reserved"),
"message should explain the reserved key: {err}"
);
}
}
#[test]
fn bindings_for_returns_only_that_contexts_verbs() {
let km = ModalKeymap::defaults();
let confirm = km.bindings_for(KeyContext::Confirm);
assert!(confirm.iter().all(|b| b.action.context() == KeyContext::Confirm));
assert_eq!(confirm.len(), 6);
}
#[test]
fn backtab_with_shift_modifier_resolves_like_plain_backtab() {
let km = ModalKeymap::defaults();
let shifted = KeyStroke::from_event(&KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT));
assert_eq!(
km.resolve(KeyContext::Create, &shifted),
Some(ModalAction::CreatePrevField)
);
assert_eq!(
km.resolve(KeyContext::Config, &shifted),
Some(ModalAction::ConfigPrevTab)
);
}