github.com/aretext/aretext@v1.3.0/state/menu_test.go (about) 1 package state 2 3 import ( 4 "os" 5 "path/filepath" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 12 "github.com/aretext/aretext/menu" 13 "github.com/aretext/aretext/selection" 14 ) 15 16 func TestShowMenu(t *testing.T) { 17 state := NewEditorState(100, 100, nil, nil) 18 items := []menu.Item{ 19 {Name: "test item 1"}, 20 {Name: "test item 2"}, 21 } 22 ShowMenu(state, MenuStyleCommand, items) 23 assert.Equal(t, InputModeMenu, state.InputMode()) 24 assert.Equal(t, MenuStyleCommand, state.Menu().Style()) 25 assert.Equal(t, "", state.Menu().SearchQuery()) 26 27 results, selectedIdx := state.Menu().SearchResults() 28 assert.Equal(t, 0, selectedIdx) 29 assert.Equal(t, 0, len(results)) 30 } 31 32 func TestHideMenu(t *testing.T) { 33 state := NewEditorState(100, 100, nil, nil) 34 items := []menu.Item{ 35 {Name: "test item"}, 36 } 37 ShowMenu(state, MenuStyleCommand, items) 38 HideMenu(state) 39 assert.Equal(t, InputModeNormal, state.InputMode()) 40 } 41 42 func TestShowMenuFromVisualMode(t *testing.T) { 43 state := NewEditorState(100, 100, nil, nil) 44 ToggleVisualMode(state, selection.ModeChar) 45 ShowMenu(state, MenuStyleCommand, nil) 46 assert.Equal(t, InputModeMenu, state.inputMode) 47 HideMenu(state) 48 assert.Equal(t, InputModeVisual, state.inputMode) 49 } 50 51 func TestSelectAndExecuteMenuItem(t *testing.T) { 52 state := NewEditorState(100, 100, nil, nil) 53 items := []menu.Item{ 54 { 55 Name: "test item", 56 Action: func(s *EditorState) {}, 57 }, 58 { 59 Name: "quit", 60 Action: Quit, 61 }, 62 } 63 ShowMenu(state, MenuStyleCommand, items) 64 AppendRuneToMenuSearch(state, 'q') // search for "q", should match "quit" 65 ExecuteSelectedMenuItem(state) 66 assert.Equal(t, InputModeNormal, state.InputMode()) 67 assert.Equal(t, "", state.Menu().SearchQuery()) 68 assert.True(t, state.QuitFlag()) 69 } 70 71 func TestMoveMenuSelection(t *testing.T) { 72 testCases := []struct { 73 name string 74 items []menu.Item 75 searchRune rune 76 moveDeltas []int 77 expectSelectedIdx int 78 }{ 79 { 80 name: "empty results, move up", 81 items: []menu.Item{}, 82 searchRune: 't', 83 moveDeltas: []int{-1}, 84 expectSelectedIdx: 0, 85 }, 86 { 87 name: "empty results, move down", 88 items: []menu.Item{}, 89 searchRune: 't', 90 moveDeltas: []int{1}, 91 expectSelectedIdx: 0, 92 }, 93 { 94 name: "single result, move up", 95 items: []menu.Item{ 96 {Name: "test"}, 97 }, 98 searchRune: 't', 99 moveDeltas: []int{1}, 100 expectSelectedIdx: 0, 101 }, 102 { 103 name: "single result, move down", 104 items: []menu.Item{ 105 {Name: "test"}, 106 }, 107 searchRune: 't', 108 moveDeltas: []int{1}, 109 expectSelectedIdx: 0, 110 }, 111 { 112 name: "multiple results, move down and up", 113 items: []menu.Item{ 114 {Name: "test1"}, 115 {Name: "test2"}, 116 {Name: "test3"}, 117 }, 118 searchRune: 't', 119 moveDeltas: []int{2, -1}, 120 expectSelectedIdx: 1, 121 }, 122 { 123 name: "multiple results, move up and wraparound", 124 items: []menu.Item{ 125 {Name: "test1"}, 126 {Name: "test2"}, 127 {Name: "test3"}, 128 {Name: "test4"}, 129 }, 130 searchRune: 't', 131 moveDeltas: []int{-1}, 132 expectSelectedIdx: 3, 133 }, 134 { 135 name: "multiple results, move down and wraparound", 136 items: []menu.Item{ 137 {Name: "test1"}, 138 {Name: "test2"}, 139 {Name: "test3"}, 140 {Name: "test4"}, 141 }, 142 searchRune: 't', 143 moveDeltas: []int{3, 1}, 144 expectSelectedIdx: 0, 145 }, 146 } 147 148 for _, tc := range testCases { 149 t.Run(tc.name, func(t *testing.T) { 150 state := NewEditorState(100, 100, nil, nil) 151 ShowMenu(state, MenuStyleCommand, tc.items) 152 AppendRuneToMenuSearch(state, tc.searchRune) 153 for _, delta := range tc.moveDeltas { 154 MoveMenuSelection(state, delta) 155 } 156 _, selectedIdx := state.Menu().SearchResults() 157 assert.Equal(t, tc.expectSelectedIdx, selectedIdx) 158 }) 159 } 160 } 161 162 func TestAppendRuneToMenuSearch(t *testing.T) { 163 state := NewEditorState(100, 100, nil, nil) 164 ShowMenu(state, MenuStyleCommand, nil) 165 AppendRuneToMenuSearch(state, 'a') 166 AppendRuneToMenuSearch(state, 'b') 167 AppendRuneToMenuSearch(state, 'c') 168 assert.Equal(t, "abc", state.Menu().SearchQuery()) 169 } 170 171 func TestDeleteRuneFromMenuSearch(t *testing.T) { 172 testCases := []struct { 173 name string 174 searchQuery string 175 numDeleted int 176 expectQuery string 177 }{ 178 { 179 name: "delete from empty query", 180 searchQuery: "", 181 numDeleted: 1, 182 expectQuery: "", 183 }, 184 { 185 name: "delete ascii from end of query", 186 searchQuery: "abc", 187 numDeleted: 2, 188 expectQuery: "a", 189 }, 190 { 191 name: "delete non-ascii unicode from end of query", 192 searchQuery: "£፴", 193 numDeleted: 1, 194 expectQuery: "£", 195 }, 196 } 197 198 for _, tc := range testCases { 199 t.Run(tc.name, func(t *testing.T) { 200 state := NewEditorState(100, 100, nil, nil) 201 ShowMenu(state, MenuStyleCommand, nil) 202 for _, r := range tc.searchQuery { 203 AppendRuneToMenuSearch(state, r) 204 } 205 for i := 0; i < tc.numDeleted; i++ { 206 DeleteRuneFromMenuSearch(state) 207 } 208 assert.Equal(t, tc.expectQuery, state.Menu().SearchQuery()) 209 }) 210 } 211 } 212 213 func TestShowFileMenu(t *testing.T) { 214 paths := []string{ 215 "a/foo.txt", 216 "a/b/bar.txt", 217 "c/baz.txt", 218 } 219 withTempDirPaths(t, paths, func(dir string) { 220 // Show the file menu. 221 state := NewEditorState(100, 100, nil, nil) 222 ShowFileMenu(state, nil) 223 completeTaskOrTimeout(t, state) 224 225 // Verify that the menu shows file paths. 226 items, selectedIdx := state.Menu().SearchResults() 227 require.Equal(t, 3, len(items)) 228 assert.Equal(t, 0, selectedIdx) 229 assert.Equal(t, "a/b/bar.txt", items[0].Name) 230 assert.Equal(t, "a/foo.txt", items[1].Name) 231 assert.Equal(t, "c/baz.txt", items[2].Name) 232 233 // Execute the second item and verify that it opens the file. 234 MoveMenuSelection(state, 1) 235 ExecuteSelectedMenuItem(state) 236 assert.Equal(t, "Opened a/foo.txt", state.StatusMsg().Text) 237 assert.Equal(t, "a/foo.txt content", state.DocumentBuffer().TextTree().String()) 238 }) 239 } 240 241 func TestShowFileLocationsMenu(t *testing.T) { 242 // These are NOT in lexicographic order. 243 items := []menu.Item{ 244 {Name: "foo.txt:3 foo"}, 245 {Name: "bar.txt:2 bar"}, 246 {Name: "baz.txt:123 baz"}, 247 } 248 249 // Show the menu with style FileLocation 250 state := NewEditorState(100, 100, nil, nil) 251 ShowMenu(state, MenuStyleFileLocation, items) 252 253 // Verify that the menu shows items in their original order. 254 items, selectedIdx := state.Menu().SearchResults() 255 require.Equal(t, 3, len(items)) 256 assert.Equal(t, 0, selectedIdx) 257 assert.Equal(t, "foo.txt:3 foo", items[0].Name) 258 assert.Equal(t, "bar.txt:2 bar", items[1].Name) 259 assert.Equal(t, "baz.txt:123 baz", items[2].Name) 260 } 261 262 func TestShowChildDirsMenu(t *testing.T) { 263 paths := []string{ 264 "root.txt", 265 "a/foo.txt", 266 "a/b/bar.txt", 267 "c/baz.txt", 268 } 269 withTempDirPaths(t, paths, func(dir string) { 270 // Show the child dirs menu. 271 state := NewEditorState(100, 100, nil, nil) 272 ShowChildDirsMenu(state, nil) 273 completeTaskOrTimeout(t, state) 274 275 // Verify that the menu shows subdirectory paths. 276 items, selectedIdx := state.Menu().SearchResults() 277 require.Equal(t, 3, len(items)) 278 assert.Equal(t, 0, selectedIdx) 279 assert.Equal(t, "./a", items[0].Name) 280 assert.Equal(t, "./a/b", items[1].Name) 281 assert.Equal(t, "./c", items[2].Name) 282 283 // Execute the second item and verify that the working directory changed. 284 MoveMenuSelection(state, 1) 285 ExecuteSelectedMenuItem(state) 286 assert.Contains(t, state.StatusMsg().Text, "Changed working directory") 287 workingDir, err := os.Getwd() 288 require.NoError(t, err) 289 assert.Equal(t, "b", filepath.Base(workingDir)) 290 }) 291 } 292 293 func TestShowParentDirsMenu(t *testing.T) { 294 withTempDirPaths(t, nil, func(dir string) { 295 // This path may be different than the temp directory due to symlinks (macOS) 296 originalWorkingDir, err := os.Getwd() 297 require.NoError(t, err) 298 299 // Show the parent dirs menu. 300 state := NewEditorState(100, 100, nil, nil) 301 ShowParentDirsMenu(state) 302 303 // Verify that the menu shows parent directory paths. 304 // This depends on the randomly chosen tempdir, so 305 // we check that the paths are in descending order by length. 306 items, selectedIdx := state.Menu().SearchResults() 307 assert.Greater(t, len(items), 0) 308 assert.Equal(t, 0, selectedIdx) 309 for i := 1; i < len(items); i++ { 310 assert.Less(t, len(items[i].Name), len(items[i-1].Name)) 311 } 312 313 // Execute the first item and verify that the working directory changed. 314 // The new working directory should be a parent of the current directory. 315 ExecuteSelectedMenuItem(state) 316 assert.Contains(t, state.StatusMsg().Text, "Changed working directory") 317 workingDir, err := os.Getwd() 318 require.NoError(t, err) 319 assert.Less(t, len(workingDir), len(originalWorkingDir)) 320 assert.Contains(t, originalWorkingDir, workingDir) 321 }) 322 } 323 324 func withTempDirPaths(t *testing.T, paths []string, f func(string)) { 325 // Reset the original working directory after the test. 326 originalWd, err := os.Getwd() 327 require.NoError(t, err) 328 defer os.Chdir(originalWd) 329 330 // Change the current working directory to a tempdir. 331 dir := t.TempDir() 332 err = os.Chdir(dir) 333 require.NoError(t, err) 334 335 // Create paths in the tempdir. 336 for _, p := range paths { 337 err = os.MkdirAll(filepath.Dir(p), 0755) 338 require.NoError(t, err) 339 err = os.WriteFile(p, []byte(p+" content"), 0644) 340 require.NoError(t, err) 341 } 342 343 // Run the test. 344 f(dir) 345 } 346 347 func completeTaskOrTimeout(t *testing.T, state *EditorState) { 348 select { 349 case action := <-state.TaskResultChan(): 350 action(state) 351 case <-time.After(10 * time.Second): 352 require.Fail(t, "Timed out") 353 } 354 }