use super::*;
use crate::editor::{Editor, Mode};
use strop_core::Buffer;
#[test]
fn coverage_shape() {
for section in SECTIONS {
assert!(
BINDINGS.iter().any(|b| b.section == *section),
"section {section} is empty"
);
}
for b in BINDINGS {
assert!(!b.keys.is_empty() && !b.desc.is_empty());
assert!(
!b.desc.contains("(soon)"),
"{}: '(soon)' belongs to live:false, not the desc",
b.keys
);
}
}
#[test]
fn keys_notation_expands() {
assert_eq!(expand("space f"), vec![vec!["space", "f"]]);
assert_eq!(expand("space /"), vec![vec!["space", "/"]]);
assert_eq!(
expand("space g u / s / p"),
vec![
vec!["space", "g", "u"],
vec!["space", "g", "s"],
vec!["space", "g", "p"]
]
);
assert_eq!(expand("/ ?"), vec![vec!["/"], vec!["?"]]);
assert_eq!(expand("h j k l").len(), 4);
assert_eq!(
expand("ctrl-w h / l"),
vec![vec!["ctrl-w", "h"], vec!["ctrl-w", "l"]]
);
for b in BINDINGS {
let seqs = expand(b.keys);
assert!(!seqs.is_empty(), "{}: no sequences", b.keys);
assert!(seqs.iter().all(|s| !s.is_empty()));
}
}
#[test]
fn which_key_children() {
for p in [" ", " g", "g", "[", "]", "m", "'", "`"] {
assert!(
!children_of(p, Mode::Normal).is_empty(),
"no hints for pending {p:?}"
);
}
let space = children_of(" ", Mode::Normal);
let g = space.iter().find(|h| h.key == "g").expect("space g hint");
assert_eq!(g.desc, "git…");
assert!(g.live);
assert!(
space.iter().any(|h| h.key == "j" && !h.live),
"space j renders as soon"
);
let git = children_of(" g", Mode::Normal);
assert_eq!(git.len(), 9); assert!(git.iter().any(|h| h.key == "u" && h.desc.contains("undo")));
assert_eq!(children_of("m", Mode::Normal)[0].key, "<a>");
assert_eq!(children_of("g", Mode::Normal).len(), 13); let v = children_of(" ", Mode::Visual);
assert_eq!(v.len(), 1);
assert_eq!(v[0].key, "y");
assert!(v[0].desc.contains("selection"));
}
const DISPATCHED: &[&str] = &[
"space f",
"space b",
"space /",
"space R",
"space ?",
"space d",
"space k",
"space y",
"space p",
"space P",
"space g u",
"space g s",
"space g S",
"space g p",
"space g l",
"space g h",
"space g b",
"space g y",
"space g o",
"space u",
"Q",
"space c",
"n",
"N",
"*",
"#",
";",
",",
"^",
"~",
"S",
"I",
":view",
"space |",
"gg",
"gd",
"gs",
"]c",
"[c",
"]f",
"[f",
"m<a>",
"'a",
"`a",
"r<c>",
"\"xy",
"\"xp",
"\"+y",
"\"+p",
"\"+P",
":w",
":q",
":q!",
":wq",
":e",
":help",
":e!",
":vs",
":vsplit",
":sp",
":split",
"ctrl-w h",
"ctrl-w l",
"ctrl-w j",
"ctrl-w k",
"ctrl-w w",
"ctrl-w v",
"ctrl-w s",
"ctrl-w q",
"up",
"down",
"left",
"right",
"tab",
"s-tab",
"ctrl-x",
"}",
"]",
")",
"enter",
"esc",
"backspace",
"q",
"S<c>",
"space y",
];
const EX_ALIASES: &[(&str, &str)] = &[(":vsplit", ":vs"), (":split", ":sp"), (":e!", ":e")];
#[test]
fn every_dispatched_sequence_has_a_row() {
for entry in DISPATCHED {
let canonical = EX_ALIASES
.iter()
.find(|(a, _)| a == entry)
.map(|(_, c)| *c)
.unwrap_or(*entry);
let concretized = canonical.replace("<a>", "a").replace("<c>", "x");
let mut toks: Vec<String> = Vec::new();
for tok in concretized.split(' ') {
if NAMED.contains(&tok) || tok.starts_with(':') {
toks.push(tok.to_string());
} else {
toks.extend(tok.chars().map(|c| c.to_string()));
}
}
assert!(
find_row(&toks).is_some(),
"{entry} dispatches but has no BINDINGS row (0003 §5.7)"
);
}
}
#[test]
fn live_rows_dispatch_through_the_table() {
for b in BINDINGS.iter().filter(|b| b.live) {
for seq in expand(b.keys) {
let keys = seq
.iter()
.map(|t| match *t {
"space" => " ".to_string(),
"<a>" => "a".into(),
"<c>" => "x".into(),
t if t.starts_with("ctrl-") || t == "up" || t == "down" || t == "tab" => {
String::new() }
t if t.contains('<') => {
let i = t.find('<').unwrap();
format!("{}x", &t[..i])
}
t => t.to_string(),
})
.collect::<String>();
if keys.is_empty() || keys.starts_with(':') || keys.starts_with('-') {
continue; }
if matches!(b.handler, Handler::Soon) {
continue; }
let mut e = Editor::new(Buffer::from_text("fn f(x) {\n let y = f(x);\n}\n"));
e.set_head(14);
e.feed_text(&keys);
assert!(
!e.message.starts_with("not an editor command"),
"{} (fed as {keys:?}) failed to dispatch — table drift",
b.keys
);
}
}
}
#[test]
fn live_leader_bindings_reach_dispatch() {
for b in BINDINGS
.iter()
.filter(|b| b.live && b.keys.starts_with("space"))
{
for seq in expand(b.keys) {
let keys = seq
.iter()
.map(|t| if *t == "space" { " " } else { t })
.collect::<String>();
let mut e = Editor::new(Buffer::from_text("x\n"));
e.feed_text(&keys);
if !dispatched_something(&e) {
panic!(
"{} (fed as {keys:?}) no-op: msg={:?} pending={:?} prefix={:?}",
b.keys,
e.message,
e.pending,
e.walker.prefix_display()
);
}
}
}
}
fn dispatched_something(e: &Editor) -> bool {
!e.message.is_empty()
|| !e.pending.is_empty()
|| e.picker_open()
|| !e.walker.prefix_display().is_empty()
|| e.clip_paste_pending.is_some()
|| e.osc52.is_some()
|| e.mode != Mode::Normal
|| e.docs.len() != 1
|| e.head() != 0
|| e.hover_card.is_some()
|| e.blame_card.is_some()
}
#[test]
fn parameterized_leaves_reach_dispatch() {
let mut e = Editor::new(Buffer::from_text("one two\n"));
e.feed_text("ma");
assert_eq!(e.message, "mark a set");
e.feed_text("'b");
assert!(e.message.contains("not set"));
let mut e = Editor::new(Buffer::from_text("one two\n"));
e.feed_text("]c");
assert!(
!e.message.is_empty() || e.head() != 0,
"]c must jump or report, never no-op"
);
let mut e = Editor::new(Buffer::from_text("one two\n"));
e.feed_text("gd");
assert!(!e.message.is_empty(), "gd with no LSP must say so");
let mut e = Editor::new(Buffer::from_text("int x;\n"));
e.feed_text("gs");
assert!(!e.message.is_empty(), "gs with no LSP must say so");
let mut e = Editor::new(Buffer::from_text("hello world\n"));
e.feed_text("\"+yiw");
assert_eq!(e.register(Some('+')).0, "hello");
assert!(e.osc52.is_some(), "clipboard yank stages OSC52");
}
#[test]
fn compat_report_is_fresh() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/vim-compat.md");
let generated = super::compat_report();
if std::env::var_os("STROP_REGEN").is_some() {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, &generated).unwrap();
}
let checked_in = std::fs::read_to_string(&path)
.expect("docs/vim-compat.md missing — STROP_REGEN=1 cargo test");
assert_eq!(
checked_in, generated,
"docs/vim-compat.md is stale — STROP_REGEN=1 cargo test to regen"
);
}