@@ -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+ }
0 commit comments