1package tui
2
3import (
4 "testing"
5
6 tea "github.com/charmbracelet/bubbletea"
7 "github.com/floatpane/matcha/config"
8)
9
10// TestComposerUpdate verifies the state transitions in the email composer.
11func TestComposerUpdate(t *testing.T) {
12 // Initialize a new composer with accounts.
13 accounts := []config.Account{
14 {ID: "account-1", Email: "test@example.com", Name: "Test User"},
15 }
16 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
17
18 t.Run("Focus cycling", func(t *testing.T) {
19 // Initial focus is on the 'To' input (index 1, since From is 0).
20 // But NewComposer starts focus at focusTo which is 1.
21 if composer.focusIndex != focusTo {
22 t.Errorf("Initial focusIndex should be %d (focusTo), got %d", focusTo, composer.focusIndex)
23 }
24
25 // Simulate pressing Tab to move to the 'Subject' field.
26 model, _ := composer.Update(tea.KeyMsg{Type: tea.KeyTab})
27 composer = model.(*Composer)
28 if composer.focusIndex != focusSubject {
29 t.Errorf("After one Tab, focusIndex should be %d (focusSubject), got %d", focusSubject, composer.focusIndex)
30 }
31
32 // Simulate pressing Tab again to move to the 'Body' field.
33 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
34 composer = model.(*Composer)
35 if composer.focusIndex != focusBody {
36 t.Errorf("After two Tabs, focusIndex should be %d (focusBody), got %d", focusBody, composer.focusIndex)
37 }
38
39 // Simulate pressing Tab again to move to the 'Attachment' field.
40 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
41 composer = model.(*Composer)
42 if composer.focusIndex != focusAttachment {
43 t.Errorf("After three Tabs, focusIndex should be %d (focusAttachment), got %d", focusAttachment, composer.focusIndex)
44 }
45
46 // Simulate pressing Tab again to move to the 'Send' button.
47 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
48 composer = model.(*Composer)
49 if composer.focusIndex != focusSend {
50 t.Errorf("After four Tabs, focusIndex should be %d (focusSend), got %d", focusSend, composer.focusIndex)
51 }
52
53 // Simulate one more Tab to wrap around.
54 // With single account, From field is skipped, so it wraps to focusTo.
55 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
56 composer = model.(*Composer)
57 if composer.focusIndex != focusTo {
58 t.Errorf("After five Tabs, focusIndex should wrap to %d (focusTo) since single account skips From, got %d", focusTo, composer.focusIndex)
59 }
60 })
61
62 t.Run("Send email message", func(t *testing.T) {
63 // Re-initialize composer for this test
64 composer = NewComposerWithAccounts(accounts, "account-1", "", "", "")
65
66 // Set values for the email fields.
67 composer.toInput.SetValue("recipient@example.com")
68 composer.subjectInput.SetValue("Test Subject")
69 composer.bodyInput.SetValue("This is the body.")
70 // Set focus to the Send button.
71 composer.focusIndex = focusSend
72
73 // Simulate pressing Enter to send the email.
74 _, cmd := composer.Update(tea.KeyMsg{Type: tea.KeyEnter})
75 if cmd == nil {
76 t.Fatal("Expected a command to be returned, but got nil.")
77 }
78
79 // Execute the command and check the resulting message.
80 msg := cmd()
81 sendMsg, ok := msg.(SendEmailMsg)
82 if !ok {
83 t.Fatalf("Expected a SendEmailMsg, but got %T", msg)
84 }
85
86 // Verify the content of the message.
87 if sendMsg.To != "recipient@example.com" {
88 t.Errorf("Expected To 'recipient@example.com', got %q", sendMsg.To)
89 }
90 if sendMsg.Subject != "Test Subject" {
91 t.Errorf("Expected Subject 'Test Subject', got %q", sendMsg.Subject)
92 }
93 if sendMsg.Body != "This is the body." {
94 t.Errorf("Expected Body 'This is the body.', got %q", sendMsg.Body)
95 }
96 if sendMsg.AccountID != "account-1" {
97 t.Errorf("Expected AccountID 'account-1', got %q", sendMsg.AccountID)
98 }
99 })
100
101 t.Run("Account picker with multiple accounts", func(t *testing.T) {
102 multiAccounts := []config.Account{
103 {ID: "account-1", Email: "test1@example.com", Name: "User 1"},
104 {ID: "account-2", Email: "test2@example.com", Name: "User 2"},
105 }
106 multiComposer := NewComposerWithAccounts(multiAccounts, "account-1", "", "", "")
107
108 // Move focus to From field
109 multiComposer.focusIndex = focusFrom
110
111 // Press Enter to open account picker
112 model, _ := multiComposer.Update(tea.KeyMsg{Type: tea.KeyEnter})
113 multiComposer = model.(*Composer)
114
115 if !multiComposer.showAccountPicker {
116 t.Error("Expected account picker to be shown")
117 }
118
119 // Navigate down to select second account
120 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyDown})
121 multiComposer = model.(*Composer)
122
123 if multiComposer.selectedAccountIdx != 1 {
124 t.Errorf("Expected selectedAccountIdx to be 1, got %d", multiComposer.selectedAccountIdx)
125 }
126
127 // Press Enter to confirm selection
128 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyEnter})
129 multiComposer = model.(*Composer)
130
131 if multiComposer.showAccountPicker {
132 t.Error("Expected account picker to be closed")
133 }
134
135 // Verify the selected account
136 if multiComposer.GetSelectedAccountID() != "account-2" {
137 t.Errorf("Expected selected account ID 'account-2', got %q", multiComposer.GetSelectedAccountID())
138 }
139 })
140
141 t.Run("Single account no picker", func(t *testing.T) {
142 singleAccounts := []config.Account{
143 {ID: "account-1", Email: "test@example.com"},
144 }
145 singleComposer := NewComposerWithAccounts(singleAccounts, "account-1", "", "", "")
146
147 // Move focus to From field
148 singleComposer.focusIndex = focusFrom
149
150 // Press Enter - should not open picker with single account
151 model, _ := singleComposer.Update(tea.KeyMsg{Type: tea.KeyEnter})
152 singleComposer = model.(*Composer)
153
154 if singleComposer.showAccountPicker {
155 t.Error("Account picker should not open with single account")
156 }
157 })
158
159 t.Run("Multi-account focus cycling includes From", func(t *testing.T) {
160 multiAccounts := []config.Account{
161 {ID: "account-1", Email: "test1@example.com"},
162 {ID: "account-2", Email: "test2@example.com"},
163 }
164 multiComposer := NewComposerWithAccounts(multiAccounts, "account-1", "", "", "")
165
166 // Initial focus is on 'To' field
167 if multiComposer.focusIndex != focusTo {
168 t.Errorf("Initial focusIndex should be %d (focusTo), got %d", focusTo, multiComposer.focusIndex)
169 }
170
171 // Tab through all fields: To -> Subject -> Body -> Attachment -> Send -> From (wrap)
172 model, _ := multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // To -> Subject
173 multiComposer = model.(*Composer)
174 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Subject -> Body
175 multiComposer = model.(*Composer)
176 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Body -> Attachment
177 multiComposer = model.(*Composer)
178 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Attachment -> Send
179 multiComposer = model.(*Composer)
180 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Send -> From (wrap)
181 multiComposer = model.(*Composer)
182
183 // With multiple accounts, From field should be included in tab order
184 if multiComposer.focusIndex != focusFrom {
185 t.Errorf("After five Tabs with multi-account, focusIndex should wrap to %d (focusFrom), got %d", focusFrom, multiComposer.focusIndex)
186 }
187
188 // One more Tab should go to To
189 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // From -> To
190 multiComposer = model.(*Composer)
191 if multiComposer.focusIndex != focusTo {
192 t.Errorf("After Tab from From, focusIndex should be %d (focusTo), got %d", focusTo, multiComposer.focusIndex)
193 }
194 })
195
196 t.Run("Shift+Tab backwards navigation", func(t *testing.T) {
197 accounts := []config.Account{
198 {ID: "account-1", Email: "test@example.com"},
199 }
200 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
201
202 // Start at focusTo, move forward a couple times
203 if composer.focusIndex != focusTo {
204 t.Errorf("Initial focusIndex should be %d (focusTo), got %d", focusTo, composer.focusIndex)
205 }
206
207 // Tab forward to Subject
208 model, _ := composer.Update(tea.KeyMsg{Type: tea.KeyTab})
209 composer = model.(*Composer)
210 if composer.focusIndex != focusSubject {
211 t.Errorf("After Tab, focusIndex should be %d (focusSubject), got %d", focusSubject, composer.focusIndex)
212 }
213
214 // Tab forward to Body
215 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
216 composer = model.(*Composer)
217 if composer.focusIndex != focusBody {
218 t.Errorf("After second Tab, focusIndex should be %d (focusBody), got %d", focusBody, composer.focusIndex)
219 }
220
221 // Shift+Tab back to Subject
222 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyShiftTab})
223 composer = model.(*Composer)
224 if composer.focusIndex != focusSubject {
225 t.Errorf("After Shift+Tab, focusIndex should be %d (focusSubject), got %d", focusSubject, composer.focusIndex)
226 }
227
228 // Shift+Tab back to To
229 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyShiftTab})
230 composer = model.(*Composer)
231 if composer.focusIndex != focusTo {
232 t.Errorf("After second Shift+Tab, focusIndex should be %d (focusTo), got %d", focusTo, composer.focusIndex)
233 }
234
235 // Shift+Tab should wrap to Send (skipping From since single account)
236 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyShiftTab})
237 composer = model.(*Composer)
238 if composer.focusIndex != focusSend {
239 t.Errorf("After Shift+Tab from To, focusIndex should wrap to %d (focusSend), got %d", focusSend, composer.focusIndex)
240 }
241 })
242
243 t.Run("Multi-account Shift+Tab includes From", func(t *testing.T) {
244 multiAccounts := []config.Account{
245 {ID: "account-1", Email: "test1@example.com"},
246 {ID: "account-2", Email: "test2@example.com"},
247 }
248 multiComposer := NewComposerWithAccounts(multiAccounts, "account-1", "", "", "")
249
250 // Start at focusTo
251 if multiComposer.focusIndex != focusTo {
252 t.Errorf("Initial focusIndex should be %d (focusTo), got %d", focusTo, multiComposer.focusIndex)
253 }
254
255 // Shift+Tab should go to From (since multi-account)
256 model, _ := multiComposer.Update(tea.KeyMsg{Type: tea.KeyShiftTab})
257 multiComposer = model.(*Composer)
258 if multiComposer.focusIndex != focusFrom {
259 t.Errorf("After Shift+Tab from To with multi-account, focusIndex should be %d (focusFrom), got %d", focusFrom, multiComposer.focusIndex)
260 }
261
262 // Shift+Tab again should wrap to Send
263 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyShiftTab})
264 multiComposer = model.(*Composer)
265 if multiComposer.focusIndex != focusSend {
266 t.Errorf("After Shift+Tab from From, focusIndex should wrap to %d (focusSend), got %d", focusSend, multiComposer.focusIndex)
267 }
268 })
269}
270
271// TestComposerGetFromAddress verifies the from address formatting.
272func TestComposerGetFromAddress(t *testing.T) {
273 t.Run("With name", func(t *testing.T) {
274 accounts := []config.Account{
275 {ID: "account-1", Email: "test@example.com", Name: "Test User"},
276 }
277 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
278
279 fromAddr := composer.getFromAddress()
280 expected := "Test User <test@example.com>"
281 if fromAddr != expected {
282 t.Errorf("Expected from address %q, got %q", expected, fromAddr)
283 }
284 })
285
286 t.Run("Without name", func(t *testing.T) {
287 accounts := []config.Account{
288 {ID: "account-1", Email: "test@example.com"},
289 }
290 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
291
292 fromAddr := composer.getFromAddress()
293 expected := "test@example.com"
294 if fromAddr != expected {
295 t.Errorf("Expected from address %q, got %q", expected, fromAddr)
296 }
297 })
298
299 t.Run("No accounts", func(t *testing.T) {
300 composer := NewComposer("", "", "", "")
301
302 fromAddr := composer.getFromAddress()
303 if fromAddr != "" {
304 t.Errorf("Expected empty from address, got %q", fromAddr)
305 }
306 })
307}
308
309// TestComposerSetSelectedAccount verifies account selection.
310func TestComposerSetSelectedAccount(t *testing.T) {
311 accounts := []config.Account{
312 {ID: "account-1", Email: "test1@example.com"},
313 {ID: "account-2", Email: "test2@example.com"},
314 {ID: "account-3", Email: "test3@example.com"},
315 }
316 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
317
318 composer.SetSelectedAccount("account-3")
319 if composer.selectedAccountIdx != 2 {
320 t.Errorf("Expected selectedAccountIdx 2, got %d", composer.selectedAccountIdx)
321 }
322 if composer.GetSelectedAccountID() != "account-3" {
323 t.Errorf("Expected selected account ID 'account-3', got %q", composer.GetSelectedAccountID())
324 }
325
326 // Test non-existent account (should not change)
327 composer.SetSelectedAccount("non-existent")
328 if composer.selectedAccountIdx != 2 {
329 t.Errorf("Expected selectedAccountIdx to remain 2, got %d", composer.selectedAccountIdx)
330 }
331}