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}