Skip to content

Commit e525ec7

Browse files
committed
improve test coverage for interactive navigation and list module
- Add tests for cyclic navigation (down from last worktree, up from first global action) - Add tests for vim keys (j/k), left/right navigation, q to exit, e for editor - Add tests for backtab focus cycling - Add tests for list command execute and format_worktree - Coverage improved from 59.34% to 60.72%
1 parent 9dd3c0a commit e525ec7

File tree

2 files changed

+376
-0
lines changed

2 files changed

+376
-0
lines changed

src/commands/interactive/tests.rs

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,3 +945,323 @@ fn scroll_with_multiple_groups() {
945945
// Verify we can find the selected line
946946
assert!(dialog.find_selected_line().is_some());
947947
}
948+
949+
#[test]
950+
fn down_from_last_worktree_moves_to_global_actions() -> Result<()> {
951+
let backend = TestBackend::new(40, 12);
952+
let terminal = Terminal::new(backend)?;
953+
// Down twice to reach last worktree, then Down again to GlobalActions, then Enter
954+
let events = StubEvents::new(vec![
955+
key(KeyCode::Down),
956+
key(KeyCode::Down),
957+
key(KeyCode::Down), // Now at GlobalActions (Create worktree)
958+
key(KeyCode::Down), // Move to Cd to root dir
959+
key(KeyCode::Enter),
960+
]);
961+
962+
let worktrees = entries(&["alpha", "beta", "gamma"]);
963+
let command = InteractiveCommand::new(
964+
terminal,
965+
events,
966+
PathBuf::from("/tmp/worktrees"),
967+
worktrees,
968+
vec![String::from("main")],
969+
Some(String::from("main")),
970+
);
971+
972+
let result = command.run(
973+
|_, _| {
974+
Ok(RemoveOutcome {
975+
local_branch: None,
976+
repositioned: false,
977+
})
978+
},
979+
|_, _| Ok(()),
980+
noop_open_editor(),
981+
)?;
982+
983+
assert_eq!(result, Some(Selection::RepoRoot));
984+
985+
Ok(())
986+
}
987+
988+
#[test]
989+
fn up_from_first_global_action_moves_to_last_worktree() -> Result<()> {
990+
let backend = TestBackend::new(40, 12);
991+
let terminal = Terminal::new(backend)?;
992+
// Up to GlobalActions (lands on last), Up to first GlobalAction, Up again to last worktree
993+
let events = StubEvents::new(vec![
994+
key(KeyCode::Up), // From first worktree to GlobalActions (Cd to root dir)
995+
key(KeyCode::Up), // To Create worktree
996+
key(KeyCode::Up), // Back to last worktree (gamma)
997+
key(KeyCode::Enter),
998+
]);
999+
1000+
let worktrees = entries(&["alpha", "beta", "gamma"]);
1001+
let command = InteractiveCommand::new(
1002+
terminal,
1003+
events,
1004+
PathBuf::from("/tmp/worktrees"),
1005+
worktrees,
1006+
vec![String::from("main")],
1007+
Some(String::from("main")),
1008+
);
1009+
1010+
let result = command.run(
1011+
|_, _| {
1012+
Ok(RemoveOutcome {
1013+
local_branch: None,
1014+
repositioned: false,
1015+
})
1016+
},
1017+
|_, _| Ok(()),
1018+
noop_open_editor(),
1019+
)?;
1020+
1021+
assert_eq!(result, Some(Selection::Worktree(String::from("gamma"))));
1022+
1023+
Ok(())
1024+
}
1025+
1026+
#[test]
1027+
fn down_navigates_within_global_actions() -> Result<()> {
1028+
let backend = TestBackend::new(60, 18);
1029+
let terminal = Terminal::new(backend)?;
1030+
// Go to GlobalActions via down from last worktree, then navigate within GlobalActions
1031+
let events = StubEvents::new(vec![
1032+
key(KeyCode::Down), // alpha -> beta
1033+
key(KeyCode::Down), // beta -> gamma
1034+
key(KeyCode::Down), // gamma -> Create worktree (first GlobalAction)
1035+
key(KeyCode::Enter), // Open create dialog
1036+
char_key('t'),
1037+
char_key('e'),
1038+
char_key('s'),
1039+
char_key('t'),
1040+
key(KeyCode::Tab),
1041+
key(KeyCode::Tab),
1042+
key(KeyCode::Enter),
1043+
key(KeyCode::Enter),
1044+
]);
1045+
1046+
let worktrees = entries(&["alpha", "beta", "gamma"]);
1047+
let command = InteractiveCommand::new(
1048+
terminal,
1049+
events,
1050+
PathBuf::from("/tmp/worktrees"),
1051+
worktrees,
1052+
vec![String::from("main")],
1053+
Some(String::from("main")),
1054+
);
1055+
1056+
let mut created = Vec::new();
1057+
let result = command.run(
1058+
|_, _| {
1059+
Ok(RemoveOutcome {
1060+
local_branch: None,
1061+
repositioned: false,
1062+
})
1063+
},
1064+
|name, base| {
1065+
created.push((name.to_string(), base.map(|b| b.to_string())));
1066+
Ok(())
1067+
},
1068+
noop_open_editor(),
1069+
)?;
1070+
1071+
assert_eq!(result, Some(Selection::Worktree(String::from("test"))));
1072+
assert_eq!(
1073+
created,
1074+
vec![(String::from("test"), Some(String::from("main")))]
1075+
);
1076+
1077+
Ok(())
1078+
}
1079+
1080+
#[test]
1081+
fn vim_keys_navigate_worktrees() -> Result<()> {
1082+
let backend = TestBackend::new(40, 10);
1083+
let terminal = Terminal::new(backend)?;
1084+
let events = StubEvents::new(vec![
1085+
char_key('j'), // Down to beta
1086+
char_key('j'), // Down to gamma
1087+
char_key('k'), // Up to beta
1088+
key(KeyCode::Enter),
1089+
]);
1090+
let worktrees = entries(&["alpha", "beta", "gamma"]);
1091+
let command = InteractiveCommand::new(
1092+
terminal,
1093+
events,
1094+
PathBuf::from("/tmp/worktrees"),
1095+
worktrees,
1096+
vec![String::from("main")],
1097+
Some(String::from("main")),
1098+
);
1099+
1100+
let selection = command
1101+
.run(
1102+
|_, _| {
1103+
Ok(RemoveOutcome {
1104+
local_branch: None,
1105+
repositioned: false,
1106+
})
1107+
},
1108+
|_, _| panic!("create should not be called"),
1109+
noop_open_editor(),
1110+
)?
1111+
.expect("expected selection");
1112+
assert_eq!(selection, Selection::Worktree(String::from("beta")));
1113+
1114+
Ok(())
1115+
}
1116+
1117+
#[test]
1118+
fn left_right_navigate_actions_panel() -> Result<()> {
1119+
let backend = TestBackend::new(40, 12);
1120+
let terminal = Terminal::new(backend)?;
1121+
let events = StubEvents::new(vec![
1122+
key(KeyCode::Tab), // Focus on Actions (starts at Open)
1123+
key(KeyCode::Right), // Move right: Open -> OpenInEditor
1124+
key(KeyCode::Right), // Move right: OpenInEditor -> Remove
1125+
key(KeyCode::Left), // Move left: Remove -> OpenInEditor
1126+
key(KeyCode::Enter), // Select OpenInEditor action
1127+
key(KeyCode::Esc), // Exit after editor opens
1128+
]);
1129+
let worktrees = entries(&["alpha"]);
1130+
let command = InteractiveCommand::new(
1131+
terminal,
1132+
events,
1133+
PathBuf::from("/tmp/worktrees"),
1134+
worktrees,
1135+
vec![String::from("main")],
1136+
Some(String::from("main")),
1137+
);
1138+
1139+
let mut editor_opened = false;
1140+
let result = command.run(
1141+
|_, _| {
1142+
Ok(RemoveOutcome {
1143+
local_branch: None,
1144+
repositioned: false,
1145+
})
1146+
},
1147+
|_, _| Ok(()),
1148+
|_, _| {
1149+
editor_opened = true;
1150+
Ok(LaunchOutcome {
1151+
status: EditorLaunchStatus::Success,
1152+
message: String::new(),
1153+
})
1154+
},
1155+
)?;
1156+
1157+
assert!(editor_opened, "editor should have been opened");
1158+
assert!(result.is_none());
1159+
1160+
Ok(())
1161+
}
1162+
1163+
#[test]
1164+
fn q_key_exits_interactive_mode() -> Result<()> {
1165+
let backend = TestBackend::new(40, 10);
1166+
let terminal = Terminal::new(backend)?;
1167+
let events = StubEvents::new(vec![char_key('q')]);
1168+
let worktrees = entries(&["alpha"]);
1169+
let command = InteractiveCommand::new(
1170+
terminal,
1171+
events,
1172+
PathBuf::from("/tmp/worktrees"),
1173+
worktrees,
1174+
vec![String::from("main")],
1175+
Some(String::from("main")),
1176+
);
1177+
1178+
let result = command.run(
1179+
|_, _| {
1180+
Ok(RemoveOutcome {
1181+
local_branch: None,
1182+
repositioned: false,
1183+
})
1184+
},
1185+
|_, _| panic!("create should not be called"),
1186+
noop_open_editor(),
1187+
)?;
1188+
1189+
assert!(result.is_none(), "q should exit without selection");
1190+
1191+
Ok(())
1192+
}
1193+
1194+
#[test]
1195+
fn e_key_opens_editor_directly() -> Result<()> {
1196+
let backend = TestBackend::new(40, 10);
1197+
let terminal = Terminal::new(backend)?;
1198+
let events = StubEvents::new(vec![char_key('e'), key(KeyCode::Esc)]);
1199+
let worktrees = entries(&["alpha"]);
1200+
let command = InteractiveCommand::new(
1201+
terminal,
1202+
events,
1203+
PathBuf::from("/tmp/worktrees"),
1204+
worktrees,
1205+
vec![String::from("main")],
1206+
Some(String::from("main")),
1207+
);
1208+
1209+
let mut editor_calls = Vec::new();
1210+
let result = command.run(
1211+
|_, _| {
1212+
Ok(RemoveOutcome {
1213+
local_branch: None,
1214+
repositioned: false,
1215+
})
1216+
},
1217+
|_, _| Ok(()),
1218+
|name, path| {
1219+
editor_calls.push((name.to_string(), path.to_path_buf()));
1220+
Ok(LaunchOutcome {
1221+
status: EditorLaunchStatus::Success,
1222+
message: String::new(),
1223+
})
1224+
},
1225+
)?;
1226+
1227+
assert_eq!(editor_calls.len(), 1);
1228+
assert_eq!(editor_calls[0].0, "alpha");
1229+
assert!(result.is_none());
1230+
1231+
Ok(())
1232+
}
1233+
1234+
#[test]
1235+
fn backtab_cycles_focus_backward() -> Result<()> {
1236+
let backend = TestBackend::new(40, 12);
1237+
let terminal = Terminal::new(backend)?;
1238+
let events = StubEvents::new(vec![
1239+
key(KeyCode::Tab), // Worktrees -> Actions
1240+
key(KeyCode::BackTab), // Actions -> Worktrees
1241+
key(KeyCode::Enter), // Select worktree
1242+
]);
1243+
let worktrees = entries(&["alpha"]);
1244+
let command = InteractiveCommand::new(
1245+
terminal,
1246+
events,
1247+
PathBuf::from("/tmp/worktrees"),
1248+
worktrees,
1249+
vec![String::from("main")],
1250+
Some(String::from("main")),
1251+
);
1252+
1253+
let result = command.run(
1254+
|_, _| {
1255+
Ok(RemoveOutcome {
1256+
local_branch: None,
1257+
repositioned: false,
1258+
})
1259+
},
1260+
|_, _| Ok(()),
1261+
noop_open_editor(),
1262+
)?;
1263+
1264+
assert_eq!(result, Some(Selection::Worktree(String::from("alpha"))));
1265+
1266+
Ok(())
1267+
}

src/commands/list/mod.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,60 @@ mod tests {
149149

150150
Ok(())
151151
}
152+
153+
#[test]
154+
fn find_worktrees_returns_empty_for_empty_dir() -> color_eyre::Result<()> {
155+
let repo_dir = TempDir::new()?;
156+
init_git_repo(&repo_dir)?;
157+
let repo = Repo::discover_from(repo_dir.path())?;
158+
let worktrees_dir = repo.ensure_worktrees_dir()?;
159+
160+
let found = find_worktrees(&worktrees_dir)?;
161+
assert!(found.is_empty());
162+
163+
Ok(())
164+
}
165+
166+
#[test]
167+
fn format_worktree_handles_single_component() {
168+
let path = PathBuf::from("feature");
169+
assert_eq!(format_worktree(&path), "feature");
170+
}
171+
172+
#[test]
173+
fn format_worktree_handles_nested_path() {
174+
let path = PathBuf::from("feature/deep/nested");
175+
assert_eq!(format_worktree(&path), "feature/deep/nested");
176+
}
177+
178+
#[test]
179+
fn list_command_execute_shows_worktrees() -> color_eyre::Result<()> {
180+
let repo_dir = TempDir::new()?;
181+
init_git_repo(&repo_dir)?;
182+
let repo = Repo::discover_from(repo_dir.path())?;
183+
let worktrees_dir = repo.ensure_worktrees_dir()?;
184+
185+
let worktree = worktrees_dir.join("my-feature");
186+
fs::create_dir_all(&worktree)?;
187+
fs::write(worktree.join(".git"), "gitdir: ..")?;
188+
189+
let cmd = ListCommand;
190+
// Just verify it doesn't error - output goes to stdout
191+
cmd.execute(&repo)?;
192+
193+
Ok(())
194+
}
195+
196+
#[test]
197+
fn list_command_execute_handles_empty() -> color_eyre::Result<()> {
198+
let repo_dir = TempDir::new()?;
199+
init_git_repo(&repo_dir)?;
200+
let repo = Repo::discover_from(repo_dir.path())?;
201+
let _worktrees_dir = repo.ensure_worktrees_dir()?;
202+
203+
let cmd = ListCommand;
204+
cmd.execute(&repo)?;
205+
206+
Ok(())
207+
}
152208
}

0 commit comments

Comments
 (0)