use gwm::tui::wt_tree::{
build_capped_tree, build_tree, file_icon, parse_status_z, sanitize_name, status_badge, working_tree_category,
StatusRecord, WtCategory, WtNode, WT_FILE_ICON, WT_JSON_ICON, WT_MARKDOWN_ICON, WT_RUST_ICON, WT_TOML_ICON,
};
fn dir<'a>(nodes: &'a [WtNode], name: &str) -> &'a WtNode {
nodes
.iter()
.find(|n| matches!(n, WtNode::Dir { name: n, .. } if n == name))
.unwrap_or_else(|| panic!("no Dir named {name:?} in {nodes:?}"))
}
fn children(node: &WtNode) -> &[WtNode] {
match node {
WtNode::Dir { children, .. } => children,
other => panic!("expected a Dir, got {other:?}"),
}
}
#[test]
fn build_tree_empty_status_is_empty() {
assert!(build_tree("").is_empty());
assert!(build_tree("\0").is_empty(), "a lone trailing NUL yields no nodes");
}
#[test]
fn build_tree_collapses_single_child_directory_chain() {
let tree = build_tree("?? src/tui/ui.rs\0");
assert_eq!(tree.len(), 1, "one top-level node: {tree:?}");
let top = dir(&tree, "src/tui");
let kids = children(top);
assert_eq!(kids.len(), 1, "the collapsed dir holds just the file: {kids:?}");
assert!(
matches!(&kids[0], WtNode::File { name, .. } if name == "ui.rs"),
"leaf file kept separate from the collapsed chain: {:?}",
kids[0]
);
}
#[test]
fn build_tree_does_not_collapse_a_dir_with_a_file_sibling() {
let tree = build_tree("?? src/lib.rs\0?? src/tui/ui.rs\0");
let src = dir(&tree, "src");
let kids = children(src);
assert_eq!(kids.len(), 2, "src keeps both children: {kids:?}");
assert!(matches!(&kids[0], WtNode::Dir { name, .. } if name == "tui"));
assert!(matches!(&kids[1], WtNode::File { name, .. } if name == "lib.rs"));
}
#[test]
fn build_tree_sorts_dirs_before_files_alphabetically() {
let tree = build_tree("?? zebra.txt\0?? alpha.txt\0?? lib/mod.rs\0?? abc/x.rs\0");
let names: Vec<&str> = tree
.iter()
.map(|n| match n {
WtNode::Dir { name, .. } => name.as_str(),
WtNode::File { name, .. } => name.as_str(),
})
.collect();
assert_eq!(names, vec!["abc", "lib", "alpha.txt", "zebra.txt"], "got {names:?}");
}
#[test]
fn build_tree_file_carries_extension_icon_badge_and_category() {
let tree = build_tree(" M src/main.rs\0");
let kids = children(dir(&tree, "src"));
match &kids[0] {
WtNode::File {
name,
icon,
badge,
category,
} => {
assert_eq!(name, "main.rs");
assert_eq!(*icon, WT_RUST_ICON, "extension drives the icon");
assert_eq!(*badge, 'M', "tracked-modified badge");
assert_eq!(*category, WtCategory::Modified);
}
other => panic!("expected a File, got {other:?}"),
}
}
#[test]
fn build_tree_takes_destination_of_a_rename() {
let tree = build_tree("R new/b.rs\0old/a.rs\0");
assert_eq!(tree.len(), 1, "only the destination dir survives: {tree:?}");
let kids = children(dir(&tree, "new"));
assert!(
matches!(&kids[0], WtNode::File { name, badge, .. } if name == "b.rs" && *badge == 'M'),
"rename keyed to destination, badge M (modified): {:?}",
kids[0]
);
}
#[test]
fn build_tree_keeps_special_chars_in_names_verbatim() {
let tree = build_tree("?? a -> café b.txt\0");
assert!(
matches!(&tree[0], WtNode::File { name, .. } if name == "a -> café b.txt"),
"special chars preserved verbatim: {:?}",
tree[0]
);
}
#[test]
fn build_tree_directory_category_is_uniform_or_mixed() {
let uniform = build_tree(" M app/a.rs\0 M app/sub/b.rs\0");
match dir(&uniform, "app") {
WtNode::Dir { category, .. } => assert_eq!(*category, Some(WtCategory::Modified), "all modified"),
other => panic!("expected a Dir, got {other:?}"),
}
let mixed = build_tree(" M app/a.rs\0?? app/c.rs\0");
match dir(&mixed, "app") {
WtNode::Dir { category, .. } => assert_eq!(*category, None, "modified + created → mixed"),
other => panic!("expected a Dir, got {other:?}"),
}
}
#[test]
fn build_capped_tree_stops_at_max_and_reports_overflow() {
let recs: Vec<StatusRecord> = (0..10)
.map(|i| StatusRecord {
x: '?',
y: '?',
path: format!("f{i:02}.txt"),
})
.collect();
let (tree, overflow) = build_capped_tree(&recs, 4);
assert_eq!(overflow, 6, "10 records, cap 4 → 6 dropped");
let files = tree.iter().filter(|n| matches!(n, WtNode::File { .. })).count();
assert_eq!(files, 4, "only the first 4 records built into leaves");
assert_eq!(build_capped_tree(&recs, 100).1, 0, "cap above the count → no overflow");
}
#[test]
fn parse_status_z_drops_rename_source_and_preserves_paths() {
let recs = parse_status_z("?? a b.rs\0R dst.rs\0src.rs\0 M c.rs\0");
let got: Vec<(char, char, &str)> = recs.iter().map(|r| (r.x, r.y, r.path.as_str())).collect();
assert_eq!(
got,
vec![('?', '?', "a b.rs"), ('R', ' ', "dst.rs"), (' ', 'M', "c.rs")],
"rename source dropped, paths verbatim: {got:?}"
);
}
#[test]
fn status_badge_maps_each_porcelain_family() {
assert_eq!(status_badge('?', '?'), '?', "untracked");
assert_eq!(status_badge('A', ' '), 'A', "staged add");
assert_eq!(status_badge(' ', 'A'), 'A', "add in worktree column");
assert_eq!(status_badge('D', ' '), 'D', "deleted");
assert_eq!(status_badge(' ', 'M'), 'M', "worktree-modified");
assert_eq!(status_badge('R', ' '), 'M', "rename → modified");
assert_eq!(status_badge('A', 'D'), 'A');
}
#[test]
fn working_tree_category_matches_badge_families() {
assert_eq!(working_tree_category('?', '?'), WtCategory::Created);
assert_eq!(working_tree_category('A', ' '), WtCategory::Created);
assert_eq!(working_tree_category('D', ' '), WtCategory::Deleted);
assert_eq!(working_tree_category(' ', 'M'), WtCategory::Modified);
assert_eq!(working_tree_category('R', ' '), WtCategory::Modified);
}
#[test]
fn sanitize_name_replaces_control_characters() {
assert_eq!(sanitize_name("normal.rs"), "normal.rs", "plain names pass through");
assert_eq!(sanitize_name("a\nb\t.rs"), "a?b?.rs", "newline and tab neutralised");
assert_eq!(sanitize_name("x\u{1b}[2J.rs"), "x?[2J.rs");
}
#[test]
fn file_icon_maps_known_extensions_and_falls_back() {
assert_eq!(file_icon("main.rs"), WT_RUST_ICON);
assert_eq!(file_icon("README.md"), WT_MARKDOWN_ICON);
assert_eq!(file_icon("Cargo.toml"), WT_TOML_ICON);
assert_eq!(file_icon("pkg.json"), WT_JSON_ICON);
assert_eq!(file_icon("MAIN.RS"), WT_RUST_ICON);
assert_eq!(file_icon("Makefile"), WT_FILE_ICON);
assert_eq!(file_icon(".gitignore"), WT_FILE_ICON);
}