use gwm::tui::{build_sidebar_payload, draw, App};
use ratatui::{backend::TestBackend, Terminal};
use std::path::Path;
use tempfile::TempDir;
fn warm_sidebar(app: &mut App) {
let w = app.selected().expect("a worktree must be selected").clone();
let mode = app.sidebar.mode;
let payload = build_sidebar_payload(&w, mode, &app.config.doctor.trunks, &app.theme);
app.sidebar.cache = Some(((w.path.clone(), mode), payload));
}
fn repo_with_commits(commit_count: usize) -> TempDir {
let dir = TempDir::new().unwrap();
let repo = git2::Repository::init(dir.path()).unwrap();
repo.set_head("refs/heads/main").ok();
let sig = git2::Signature::now("gwm-test", "gwm@test").unwrap();
std::fs::write(dir.path().join("file.txt"), "seed").unwrap();
repo.index().unwrap().add_path(Path::new("file.txt")).unwrap();
repo.index().unwrap().write().unwrap();
{
let tree_id = repo.index().unwrap().write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[]).unwrap();
}
for i in 0..commit_count {
std::fs::write(dir.path().join("file.txt"), format!("commit-{i}")).unwrap();
repo.index().unwrap().add_path(Path::new("file.txt")).unwrap();
repo.index().unwrap().write().unwrap();
let parent = repo.head().unwrap().peel_to_commit().unwrap();
let tree_id = repo.index().unwrap().write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
repo
.commit(Some("HEAD"), &sig, &sig, &format!("commit-{i}"), &tree, &[&parent])
.unwrap();
}
dir
}
fn buffer_text(terminal: &Terminal<TestBackend>) -> String {
let buffer = terminal.backend().buffer();
buffer.content().iter().map(|c| c.symbol()).collect()
}
#[test]
fn draw_does_not_shell_out_to_git_or_warm_the_sidebar_cache() {
let dir = repo_with_commits(3);
let mut app = App::new_at_layered(Some(dir.path()), None).unwrap();
app.sidebar.cache = None;
let backend = TestBackend::new(120, 40);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|f| draw(f, &mut app)).unwrap();
assert!(
app.sidebar.cache.is_none(),
"draw must not run git subprocesses to warm the sidebar cache; the async worker does (issue #343)"
);
}
#[test]
fn stale_key_cache_renders_the_loading_placeholder_not_another_worktrees_preview() {
use gwm::tui::SidebarSections;
use ratatui::text::Line;
use std::path::PathBuf;
let dir = repo_with_commits(4);
let mut app = App::new_at_layered(Some(dir.path()), None).unwrap();
let mode = app.sidebar.mode;
app.sidebar.cache = Some((
(PathBuf::from("/tmp/gwm-test/some-other-worktree"), mode),
SidebarSections {
recent_commits: vec![Line::from("GHOST-COMMIT-XYZ")],
..SidebarSections::default()
},
));
let backend = TestBackend::new(120, 40);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|f| draw(f, &mut app)).unwrap();
let text = buffer_text(&terminal);
assert!(
text.contains("loading…"),
"a stale-key cache must render the loading placeholder: {text}"
);
assert!(
!text.contains("GHOST-COMMIT-XYZ"),
"the other worktree's cached commits must NOT render under the current header: {text}"
);
}
#[test]
fn sidebar_renders_header_name_and_commit_subject_on_warm_cache() {
let dir = repo_with_commits(8);
let mut app = App::new_at_layered(Some(dir.path()), None).unwrap();
assert!(app.selected().is_some(), "expected the main worktree to be selected");
let name = app.selected().unwrap().name.clone();
let backend = TestBackend::new(120, 40);
let mut terminal = Terminal::new(backend).unwrap();
warm_sidebar(&mut app);
assert!(
app.sidebar.cache.is_some(),
"the warm helper must seed the sidebar cache"
);
terminal.draw(|f| draw(f, &mut app)).unwrap();
let text = buffer_text(&terminal);
assert!(
text.contains(&name),
"sidebar header must render the worktree name, got:\n{text}"
);
assert!(
text.contains("commit-7"),
"sidebar recent-commits must render the most recent commit subject, got:\n{text}"
);
}
#[test]
fn sidebar_warm_cache_render_is_stable_across_frames() {
let dir = repo_with_commits(8);
let mut app = App::new_at_layered(Some(dir.path()), None).unwrap();
let backend = TestBackend::new(120, 40);
let mut terminal = Terminal::new(backend).unwrap();
warm_sidebar(&mut app);
terminal.draw(|f| draw(f, &mut app)).unwrap();
let first = buffer_text(&terminal);
terminal.draw(|f| draw(f, &mut app)).unwrap();
let second = buffer_text(&terminal);
assert_eq!(
first, second,
"two consecutive warm-cache draws must produce byte-identical sidebar buffers"
);
}
#[test]
fn working_tree_section_renders_colored_status_counts_footer() {
let dir = repo_with_commits(1);
for i in 0..11 {
std::fs::write(dir.path().join(format!("dirty-{i}.txt")), "dirty").unwrap();
}
let mut app = App::new_at_layered(Some(dir.path()), None).unwrap();
let backend = TestBackend::new(120, 40);
let mut terminal = Terminal::new(backend).unwrap();
warm_sidebar(&mut app);
terminal.draw(|f| draw(f, &mut app)).unwrap();
terminal.draw(|f| draw(f, &mut app)).unwrap();
let text = buffer_text(&terminal);
assert!(
text.contains("Working Tree"),
"sidebar must render the Working Tree pane: {text}"
);
assert!(
text.contains(gwm::tui::WT_CREATED_ICON),
"footer must render the created (diff-added) nerdfont glyph: {text}"
);
assert!(text.contains("11"), "footer must render the created-file count: {text}");
}
#[test]
fn working_tree_section_renders_file_tree_with_icons() {
let dir = repo_with_commits(1);
std::fs::create_dir_all(dir.path().join("src/app")).unwrap();
std::fs::write(dir.path().join("src/app/mod.rs"), "fn x() {}").unwrap();
let mut app = App::new_at_layered(Some(dir.path()), None).unwrap();
let backend = TestBackend::new(120, 40);
let mut terminal = Terminal::new(backend).unwrap();
warm_sidebar(&mut app);
terminal.draw(|f| draw(f, &mut app)).unwrap();
terminal.draw(|f| draw(f, &mut app)).unwrap();
let text = buffer_text(&terminal);
assert!(
text.contains("src/app"),
"single-child dir chain renders collapsed: {text}"
);
assert!(text.contains("mod.rs"), "the leaf file name renders: {text}");
assert!(
text.contains(gwm::tui::wt_tree::WT_DIR_OPEN_ICON),
"directory row carries a folder glyph: {text}"
);
assert!(
text.contains(gwm::tui::wt_tree::WT_RUST_ICON),
"the .rs leaf carries the Rust file-type glyph: {text}"
);
assert!(
text.contains('└') || text.contains('├'),
"rows are drawn with tree connector lines: {text}"
);
}
#[test]
fn working_tree_section_shows_a_scrollbar_when_the_tree_overflows() {
let dir = repo_with_commits(20);
for i in 0..30 {
std::fs::write(dir.path().join(format!("file-{i:02}.rs")), "x").unwrap();
}
let mut app = App::new_at_layered(Some(dir.path()), None).unwrap();
let backend = TestBackend::new(120, 50);
let mut terminal = Terminal::new(backend).unwrap();
warm_sidebar(&mut app);
terminal.draw(|f| draw(f, &mut app)).unwrap();
terminal.draw(|f| draw(f, &mut app)).unwrap();
assert!(
app.sidebar.wt_max_scroll > 0,
"fixture must actually overflow the Working Tree viewport"
);
let text = buffer_text(&terminal);
assert!(
text.contains('█'),
"an overflowing Working Tree pane must show the scrollbar thumb: {text}"
);
}
fn git_in(dir: &std::path::Path, args: &[&str]) {
let out = std::process::Command::new("git")
.current_dir(dir)
.args(args)
.env("GIT_AUTHOR_NAME", "gwm-test")
.env("GIT_AUTHOR_EMAIL", "gwm@test")
.env("GIT_COMMITTER_NAME", "gwm-test")
.env("GIT_COMMITTER_EMAIL", "gwm@test")
.output()
.unwrap();
assert!(
out.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn status_pane_renders_diff_vs_base_line_on_a_feature_branch() {
let dir = repo_with_commits(1);
let path = dir.path();
std::fs::write(path.join("f.txt"), "a\nb\nc\n").unwrap();
git_in(path, &["add", "f.txt"]);
git_in(path, &["commit", "-m", "base file"]);
git_in(path, &["checkout", "-b", "feat/#287-diff"]);
std::fs::write(path.join("f.txt"), "a\nB\nc\nd\n").unwrap();
git_in(path, &["commit", "-am", "tweak"]);
let mut app = App::new_at_layered(Some(path), None).unwrap();
let backend = TestBackend::new(120, 40);
let mut terminal = Terminal::new(backend).unwrap();
warm_sidebar(&mut app);
terminal.draw(|f| draw(f, &mut app)).unwrap();
terminal.draw(|f| draw(f, &mut app)).unwrap();
let text = buffer_text(&terminal);
assert!(text.contains("Diff"), "Status pane must show the Diff label: {text}");
assert!(text.contains("+2"), "Status pane must show insertions: {text}");
assert!(text.contains("-1"), "Status pane must show deletions: {text}");
}