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  }