navigation_wrap_test.go

  1package tui
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"testing"
  7
  8	tea "charm.land/bubbletea/v2"
  9	"github.com/floatpane/matcha/config"
 10	"github.com/floatpane/matcha/theme"
 11)
 12
 13func TestSettingsNavigationWraps(t *testing.T) {
 14	settings := NewSettings(&config.Config{
 15		Accounts: []config.Account{
 16			{ID: "account-1", Email: "one@example.com"},
 17			{ID: "account-2", Email: "two@example.com"},
 18		},
 19		MailingLists: []config.MailingList{
 20			{Name: "List One"},
 21			{Name: "List Two"},
 22		},
 23	})
 24
 25	t.Run("menu", func(t *testing.T) {
 26		settings.menuCursor = 0
 27		model, _ := settings.updateMenu(tea.KeyPressMsg{Code: tea.KeyUp})
 28		settings = model.(*Settings)
 29		if settings.menuCursor != int(CategoryPlugins) {
 30			t.Fatalf("up from first menu item should wrap to last, got %d", settings.menuCursor)
 31		}
 32
 33		model, _ = settings.updateMenu(tea.KeyPressMsg{Code: tea.KeyDown})
 34		settings = model.(*Settings)
 35		if settings.menuCursor != 0 {
 36			t.Fatalf("down from last menu item should wrap to first, got %d", settings.menuCursor)
 37		}
 38	})
 39
 40	t.Run("general", func(t *testing.T) {
 41		settings.generalCursor = 0
 42		last := len(settings.buildGeneralOptions()) - 1
 43
 44		model, _ := settings.updateGeneral(tea.KeyPressMsg{Code: tea.KeyUp})
 45		settings = model.(*Settings)
 46		if settings.generalCursor != last {
 47			t.Fatalf("up from first general item should wrap to last, got %d", settings.generalCursor)
 48		}
 49
 50		model, _ = settings.updateGeneral(tea.KeyPressMsg{Code: tea.KeyDown})
 51		settings = model.(*Settings)
 52		if settings.generalCursor != 0 {
 53			t.Fatalf("down from last general item should wrap to first, got %d", settings.generalCursor)
 54		}
 55	})
 56
 57	t.Run("accounts", func(t *testing.T) {
 58		settings.accountsCursor = 0
 59		last := len(settings.cfg.Accounts)
 60
 61		model, _ := settings.updateAccounts(tea.KeyPressMsg{Code: tea.KeyUp})
 62		settings = model.(*Settings)
 63		if settings.accountsCursor != last {
 64			t.Fatalf("up from first account item should wrap to add account, got %d", settings.accountsCursor)
 65		}
 66
 67		model, _ = settings.updateAccounts(tea.KeyPressMsg{Code: tea.KeyDown})
 68		settings = model.(*Settings)
 69		if settings.accountsCursor != 0 {
 70			t.Fatalf("down from add account should wrap to first, got %d", settings.accountsCursor)
 71		}
 72	})
 73
 74	t.Run("mailing lists", func(t *testing.T) {
 75		settings.listsCursor = 0
 76		last := len(settings.cfg.MailingLists)
 77
 78		model, _ := settings.updateMailingLists(tea.KeyPressMsg{Code: tea.KeyUp})
 79		settings = model.(*Settings)
 80		if settings.listsCursor != last {
 81			t.Fatalf("up from first mailing list should wrap to add list, got %d", settings.listsCursor)
 82		}
 83
 84		model, _ = settings.updateMailingLists(tea.KeyPressMsg{Code: tea.KeyDown})
 85		settings = model.(*Settings)
 86		if settings.listsCursor != 0 {
 87			t.Fatalf("down from add list should wrap to first, got %d", settings.listsCursor)
 88		}
 89	})
 90
 91	t.Run("theme", func(t *testing.T) {
 92		themes := theme.AllThemes()
 93		if len(themes) < 2 {
 94			t.Skip("need at least two themes to test wrap-around")
 95		}
 96
 97		settings.themeCursor = 0
 98		model, _ := settings.updateTheme(tea.KeyPressMsg{Code: tea.KeyUp})
 99		settings = model.(*Settings)
100		if settings.themeCursor != len(themes)-1 {
101			t.Fatalf("up from first theme should wrap to last, got %d", settings.themeCursor)
102		}
103
104		model, _ = settings.updateTheme(tea.KeyPressMsg{Code: tea.KeyDown})
105		settings = model.(*Settings)
106		if settings.themeCursor != 0 {
107			t.Fatalf("down from last theme should wrap to first, got %d", settings.themeCursor)
108		}
109	})
110}
111
112func TestSettingsHorizontalPaneFocus(t *testing.T) {
113	t.Run("right moves focus from menu to content", func(t *testing.T) {
114		settings := NewSettings(&config.Config{})
115		settings.activePane = PaneMenu
116		settings.menuCursor = int(CategoryGeneral)
117
118		model, _ := settings.Update(tea.KeyPressMsg{Code: tea.KeyRight})
119		settings = model.(*Settings)
120
121		if settings.activePane != PaneContent {
122			t.Fatalf("right from menu pane should focus content, got %d", settings.activePane)
123		}
124	})
125
126	t.Run("esc moves focus from content to menu", func(t *testing.T) {
127		settings := NewSettings(&config.Config{})
128		settings.activePane = PaneContent
129		settings.activeCategory = CategoryGeneral
130		settings.menuCursor = int(CategoryGeneral)
131
132		model, _ := settings.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
133		settings = model.(*Settings)
134
135		if settings.activePane != PaneMenu {
136			t.Fatalf("esc from content pane should focus menu, got %d", settings.activePane)
137		}
138	})
139
140	t.Run("left moves focus from content to menu", func(t *testing.T) {
141		settings := NewSettings(&config.Config{})
142		settings.activePane = PaneContent
143		settings.activeCategory = CategoryGeneral
144		settings.menuCursor = int(CategoryGeneral)
145
146		model, _ := settings.Update(tea.KeyPressMsg{Code: tea.KeyLeft})
147		settings = model.(*Settings)
148
149		if settings.activePane != PaneMenu {
150			t.Fatalf("left from content pane should focus menu, got %d", settings.activePane)
151		}
152	})
153
154	t.Run("left does not exit settings from menu", func(t *testing.T) {
155		settings := NewSettings(&config.Config{})
156		settings.activePane = PaneMenu
157
158		model, cmd := settings.Update(tea.KeyPressMsg{Code: tea.KeyLeft})
159		settings = model.(*Settings)
160
161		if cmd != nil {
162			t.Fatal("left from menu pane should not return to choice menu")
163		}
164		if settings.activePane != PaneMenu {
165			t.Fatalf("left from menu pane should keep menu focused, got %d", settings.activePane)
166		}
167	})
168}
169
170func TestSettingsEncryptionLeftKeyInInput(t *testing.T) {
171	t.Run("at input start returns to menu", func(t *testing.T) {
172		settings := NewSettings(&config.Config{})
173		settings.activePane = PaneContent
174		settings.activeCategory = CategoryEncryption
175		settings.encFocusIndex = 0
176		settings.encPasswordInput.SetValue("secret")
177		settings.encPasswordInput.SetCursor(0)
178		settings.encPasswordInput.Focus()
179
180		model, _ := settings.Update(tea.KeyPressMsg{Code: tea.KeyLeft})
181		settings = model.(*Settings)
182
183		if settings.activePane != PaneMenu {
184			t.Fatalf("left at start of encryption input should focus menu, got %d", settings.activePane)
185		}
186		if settings.encPasswordInput.Value() != "" {
187			t.Fatal("left at start of encryption input should clear input like esc")
188		}
189	})
190
191	t.Run("inside input moves cursor", func(t *testing.T) {
192		settings := NewSettings(&config.Config{})
193		settings.activePane = PaneContent
194		settings.activeCategory = CategoryEncryption
195		settings.encFocusIndex = 0
196		settings.encPasswordInput.SetValue("secret")
197		settings.encPasswordInput.SetCursor(1)
198		settings.encPasswordInput.Focus()
199
200		model, _ := settings.Update(tea.KeyPressMsg{Code: tea.KeyLeft})
201		settings = model.(*Settings)
202
203		if settings.activePane != PaneContent {
204			t.Fatalf("left inside encryption input should keep content focused, got %d", settings.activePane)
205		}
206		if settings.encPasswordInput.Position() != 0 {
207			t.Fatalf("left inside encryption input should move cursor left, got position %d", settings.encPasswordInput.Position())
208		}
209	})
210}
211
212func TestFilePickerNavigationWraps(t *testing.T) {
213	dir := t.TempDir()
214	if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0o600); err != nil {
215		t.Fatal(err)
216	}
217	if err := os.WriteFile(filepath.Join(dir, "b.txt"), []byte("b"), 0o600); err != nil {
218		t.Fatal(err)
219	}
220
221	picker := NewFilePicker(dir)
222	if len(picker.items) != 2 {
223		t.Fatalf("expected two picker items, got %d", len(picker.items))
224	}
225
226	model, _ := picker.Update(tea.KeyPressMsg{Code: tea.KeyUp})
227	picker = model.(*FilePicker)
228	if picker.cursor != len(picker.items)-1 {
229		t.Fatalf("up from first file should wrap to last, got %d", picker.cursor)
230	}
231
232	model, _ = picker.Update(tea.KeyPressMsg{Code: tea.KeyDown})
233	picker = model.(*FilePicker)
234	if picker.cursor != 0 {
235		t.Fatalf("down from last file should wrap to first, got %d", picker.cursor)
236	}
237}
238
239func TestFilePickerNavigationEmptyDirectoryDoesNotWrap(t *testing.T) {
240	picker := NewFilePicker(t.TempDir())
241	if len(picker.items) != 0 {
242		t.Fatalf("expected empty picker, got %d items", len(picker.items))
243	}
244
245	model, _ := picker.Update(tea.KeyPressMsg{Code: tea.KeyUp})
246	picker = model.(*FilePicker)
247	if picker.cursor != 0 {
248		t.Fatalf("empty picker cursor should remain zero after up, got %d", picker.cursor)
249	}
250
251	model, _ = picker.Update(tea.KeyPressMsg{Code: tea.KeyDown})
252	picker = model.(*FilePicker)
253	if picker.cursor != 0 {
254		t.Fatalf("empty picker cursor should remain zero after down, got %d", picker.cursor)
255	}
256}