1package screens
2
3import (
4 "image/color"
5 "testing"
6
7 tea "charm.land/bubbletea/v2"
8
9 "git.secluded.site/keld/internal/theme"
10 "git.secluded.site/keld/internal/ui"
11)
12
13func testPresets() []string {
14 return []string{"home@cloud", "work@local", "media"}
15}
16
17func TestPresetTitle(t *testing.T) {
18 t.Parallel()
19
20 p := NewPreset(testPresets(), testStyles())
21 if got := p.Title(); got != "Select a preset" {
22 t.Errorf("Title() = %q, want %q", got, "Select a preset")
23 }
24}
25
26func TestPresetSelectionEmpty(t *testing.T) {
27 t.Parallel()
28
29 p := NewPreset(testPresets(), testStyles())
30 if got := p.Selection(); got != "" {
31 t.Errorf("Selection() before interaction = %q, want empty", got)
32 }
33}
34
35func TestPresetKeyBindings(t *testing.T) {
36 t.Parallel()
37
38 p := NewPreset(testPresets(), testStyles())
39 bindings := p.KeyBindings()
40
41 if len(bindings) == 0 {
42 t.Fatal("KeyBindings() returned no bindings")
43 }
44}
45
46func TestPresetEscWhenNotFilteringReturnsBack(t *testing.T) {
47 t.Parallel()
48
49 p := NewPreset(testPresets(), testStyles())
50 // Initialise the form so huh is ready to receive input.
51 p.Init()
52 // Send a WindowSizeMsg so huh can lay out its fields.
53 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
54
55 // Esc when not filtering should signal back navigation.
56 _, cmd := p.Update(tea.KeyPressMsg{Code: tea.KeyEscape})
57
58 if cmd == nil {
59 t.Fatal("expected BackCmd on Esc when not filtering")
60 }
61 msg := cmd()
62 if _, ok := msg.(ui.BackMsg); !ok {
63 t.Errorf("cmd produced %T, want ui.BackMsg", msg)
64 }
65}
66
67func TestPresetEscWhileFilteringDoesNotBack(t *testing.T) {
68 t.Parallel()
69
70 p := NewPreset(testPresets(), testStyles())
71 p.Init()
72 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
73
74 // Activate filter mode by pressing '/'.
75 p.Update(tea.KeyPressMsg{Code: '/', Text: "/"})
76
77 // Esc while filtering should close the filter, not back out.
78 updated, cmd := p.Update(tea.KeyPressMsg{Code: tea.KeyEscape})
79 preset := updated.(*Preset)
80
81 // Should still be on this screen (no BackCmd).
82 if cmd != nil {
83 msg := cmd()
84 if _, ok := msg.(ui.BackMsg); ok {
85 t.Error("Esc while filtering should not produce BackMsg")
86 }
87 }
88
89 // Screen should still be functional (not backed out).
90 if preset.Selection() != "" {
91 t.Error("preset should not have a selection after Esc from filter")
92 }
93}
94
95func TestPresetCompleteReturnsDone(t *testing.T) {
96 t.Parallel()
97
98 p := NewPreset(testPresets(), testStyles())
99 p.Init()
100 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
101
102 // Press enter to select the focused option (first one: global defaults).
103 // huh uses an internal message chain (nextFieldMsg → nextGroupMsg →
104 // StateCompleted), so we drain until DoneMsg appears.
105 var screen ui.Screen
106 var cmd tea.Cmd
107 screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
108 _, cmd = drain(screen.(*Preset), cmd)
109
110 if cmd == nil {
111 t.Fatal("expected DoneCmd after form completion")
112 }
113 msg := cmd()
114 if _, ok := msg.(ui.DoneMsg); !ok {
115 t.Errorf("cmd produced %T, want ui.DoneMsg", msg)
116 }
117}
118
119func TestPresetSelectionValue(t *testing.T) {
120 t.Parallel()
121
122 p := NewPreset(testPresets(), testStyles())
123 p.Init()
124 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
125
126 // Move down to the first real preset ("home@cloud") and select it.
127 p.Update(tea.KeyPressMsg{Code: tea.KeyDown})
128 var screen ui.Screen
129 var cmd tea.Cmd
130 screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
131 p, _ = drain(screen.(*Preset), cmd)
132
133 if got := p.Selection(); got != "home@cloud" {
134 t.Errorf("Selection() = %q, want %q", got, "home@cloud")
135 }
136}
137
138func TestPresetGlobalDefaultDisplayLabel(t *testing.T) {
139 t.Parallel()
140
141 // When the user selects "(global defaults only)", the resolved
142 // value is "" but the breadcrumb display should be meaningful.
143 p := NewPreset(testPresets(), testStyles())
144 p.Init()
145 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
146
147 // First option is the global default. Select it.
148 var screen ui.Screen
149 var cmd tea.Cmd
150 screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
151 p, _ = drain(screen.(*Preset), cmd)
152
153 // Selection() should return a display-friendly label, not "".
154 sel := p.Selection()
155 if sel == "" {
156 t.Error("Selection() for global defaults should return a display label, not empty string")
157 }
158}
159
160func TestPresetReinitPreservesSelection(t *testing.T) {
161 t.Parallel()
162
163 p := NewPreset(testPresets(), testStyles())
164 p.Init()
165 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
166
167 // Move down to "home@cloud" and select it.
168 p.Update(tea.KeyPressMsg{Code: tea.KeyDown})
169 var screen ui.Screen
170 var cmd tea.Cmd
171 screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
172 p, _ = drain(screen.(*Preset), cmd)
173
174 if p.Selection() != "home@cloud" {
175 t.Fatalf("precondition: Selection() = %q, want %q", p.Selection(), "home@cloud")
176 }
177
178 // Re-initialise (simulates back navigation). The form state
179 // should carry the previous selection through.
180 p.Init()
181
182 // The internal selected value should still be "home@cloud".
183 if p.selected != "home@cloud" {
184 t.Errorf("after re-init, selected = %q, want %q (should preserve previous choice)", p.selected, "home@cloud")
185 }
186}
187
188func TestPresetReinitAfterBack(t *testing.T) {
189 t.Parallel()
190
191 p := NewPreset(testPresets(), testStyles())
192 p.Init()
193 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
194
195 // Complete the form first so the state is no longer StateNormal.
196 var screen ui.Screen
197 var cmd tea.Cmd
198 screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
199 p, _ = drain(screen.(*Preset), cmd)
200
201 // Re-initialise (simulates the session navigating back to this screen).
202 p.Init()
203 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
204
205 // Should be functional again — selecting should produce DoneCmd.
206 screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
207 _, cmd = drain(screen.(*Preset), cmd)
208
209 if cmd == nil {
210 t.Fatal("expected DoneCmd after re-init, form may not have been reset")
211 }
212 msg := cmd()
213 if _, ok := msg.(ui.DoneMsg); !ok {
214 t.Errorf("after re-init, cmd produced %T, want ui.DoneMsg", msg)
215 }
216}
217
218func TestPresetRebuildsFormOnBackgroundChange(t *testing.T) {
219 t.Parallel()
220
221 styles := testStyles()
222 p := NewPreset(testPresets(), styles)
223 p.Init()
224 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
225
226 originalForm := p.form
227
228 // Simulate background detection switching to light. The session
229 // would have already mutated *styles before forwarding this msg.
230 *styles = theme.New(false)
231 updated, _ := p.Update(tea.BackgroundColorMsg{Color: color.White})
232 p = updated.(*Preset)
233
234 // The form should have been rebuilt with the new theme.
235 if p.form == originalForm {
236 t.Error("form was not rebuilt after BackgroundColorMsg; cached theme is stale")
237 }
238}
239
240func TestPresetValueReturnsActualPresetName(t *testing.T) {
241 t.Parallel()
242
243 p := NewPreset(testPresets(), testStyles())
244 p.Init()
245 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
246
247 // Move down to "home@cloud" and select it.
248 p.Update(tea.KeyPressMsg{Code: tea.KeyDown})
249 var screen ui.Screen
250 var cmd tea.Cmd
251 screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
252 p, _ = drain(screen.(*Preset), cmd)
253
254 if got := p.Value(); got != "home@cloud" {
255 t.Errorf("Value() = %q, want %q", got, "home@cloud")
256 }
257}
258
259func TestPresetValueEmptyForGlobalDefaults(t *testing.T) {
260 t.Parallel()
261
262 p := NewPreset(testPresets(), testStyles())
263 p.Init()
264 p.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
265
266 // First option is global defaults. Select it.
267 var screen ui.Screen
268 var cmd tea.Cmd
269 screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
270 p, _ = drain(screen.(*Preset), cmd)
271
272 if got := p.Value(); got != "" {
273 t.Errorf("Value() = %q, want empty for global defaults", got)
274 }
275}