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 'Cc' field.
26 model, _ := composer.Update(tea.KeyMsg{Type: tea.KeyTab})
27 composer = model.(*Composer)
28 if composer.focusIndex != focusCc {
29 t.Errorf("After one Tab, focusIndex should be %d (focusCc), got %d", focusCc, composer.focusIndex)
30 }
31
32 // Simulate pressing Tab to move to the 'Bcc' field.
33 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
34 composer = model.(*Composer)
35 if composer.focusIndex != focusBcc {
36 t.Errorf("After two Tabs, focusIndex should be %d (focusBcc), got %d", focusBcc, composer.focusIndex)
37 }
38
39 // Simulate pressing Tab to move to the 'Subject' field.
40 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
41 composer = model.(*Composer)
42 if composer.focusIndex != focusSubject {
43 t.Errorf("After three Tabs, focusIndex should be %d (focusSubject), got %d", focusSubject, composer.focusIndex)
44 }
45
46 // Simulate pressing Tab again to move to the 'Body' field.
47 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
48 composer = model.(*Composer)
49 if composer.focusIndex != focusBody {
50 t.Errorf("After four Tabs, focusIndex should be %d (focusBody), got %d", focusBody, composer.focusIndex)
51 }
52
53 // Simulate pressing Tab again to move to the 'Signature' field.
54 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
55 composer = model.(*Composer)
56 if composer.focusIndex != focusSignature {
57 t.Errorf("After five Tabs, focusIndex should be %d (focusSignature), got %d", focusSignature, composer.focusIndex)
58 }
59
60 // Simulate pressing Tab again to move to the 'Attachment' field.
61 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
62 composer = model.(*Composer)
63 if composer.focusIndex != focusAttachment {
64 t.Errorf("After six Tabs, focusIndex should be %d (focusAttachment), got %d", focusAttachment, composer.focusIndex)
65 }
66
67 // Simulate pressing Tab again to move to the 'Send' button.
68 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
69 composer = model.(*Composer)
70 if composer.focusIndex != focusSend {
71 t.Errorf("After seven Tabs, focusIndex should be %d (focusSend), got %d", focusSend, composer.focusIndex)
72 }
73
74 // Simulate one more Tab to wrap around.
75 // With single account, From field is skipped, so it wraps to focusTo.
76 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
77 composer = model.(*Composer)
78 if composer.focusIndex != focusTo {
79 t.Errorf("After eight Tabs, focusIndex should wrap to %d (focusTo) since single account skips From, got %d", focusTo, composer.focusIndex)
80 }
81 })
82
83 t.Run("Send email message", func(t *testing.T) {
84 // Re-initialize composer for this test
85 composer = NewComposerWithAccounts(accounts, "account-1", "", "", "")
86
87 // Set values for the email fields.
88 composer.toInput.SetValue("recipient@example.com")
89 composer.subjectInput.SetValue("Test Subject")
90 composer.bodyInput.SetValue("This is the body.")
91 // Set focus to the Send button.
92 composer.focusIndex = focusSend
93
94 // Simulate pressing Enter to send the email.
95 _, cmd := composer.Update(tea.KeyMsg{Type: tea.KeyEnter})
96 if cmd == nil {
97 t.Fatal("Expected a command to be returned, but got nil.")
98 }
99
100 // Execute the command and check the resulting message.
101 msg := cmd()
102 sendMsg, ok := msg.(SendEmailMsg)
103 if !ok {
104 t.Fatalf("Expected a SendEmailMsg, but got %T", msg)
105 }
106
107 // Verify the content of the message.
108 if sendMsg.To != "recipient@example.com" {
109 t.Errorf("Expected To 'recipient@example.com', got %q", sendMsg.To)
110 }
111 if sendMsg.Subject != "Test Subject" {
112 t.Errorf("Expected Subject 'Test Subject', got %q", sendMsg.Subject)
113 }
114 if sendMsg.Body != "This is the body." {
115 t.Errorf("Expected Body 'This is the body.', got %q", sendMsg.Body)
116 }
117 if sendMsg.AccountID != "account-1" {
118 t.Errorf("Expected AccountID 'account-1', got %q", sendMsg.AccountID)
119 }
120 })
121
122 t.Run("Account picker with multiple accounts", func(t *testing.T) {
123 multiAccounts := []config.Account{
124 {ID: "account-1", Email: "test1@example.com", Name: "User 1"},
125 {ID: "account-2", Email: "test2@example.com", Name: "User 2"},
126 }
127 multiComposer := NewComposerWithAccounts(multiAccounts, "account-1", "", "", "")
128
129 // Move focus to From field
130 multiComposer.focusIndex = focusFrom
131
132 // Press Enter to open account picker
133 model, _ := multiComposer.Update(tea.KeyMsg{Type: tea.KeyEnter})
134 multiComposer = model.(*Composer)
135
136 if !multiComposer.showAccountPicker {
137 t.Error("Expected account picker to be shown")
138 }
139
140 // Navigate down to select second account
141 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyDown})
142 multiComposer = model.(*Composer)
143
144 if multiComposer.selectedAccountIdx != 1 {
145 t.Errorf("Expected selectedAccountIdx to be 1, got %d", multiComposer.selectedAccountIdx)
146 }
147
148 // Press Enter to confirm selection
149 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyEnter})
150 multiComposer = model.(*Composer)
151
152 if multiComposer.showAccountPicker {
153 t.Error("Expected account picker to be closed")
154 }
155
156 // Verify the selected account
157 if multiComposer.GetSelectedAccountID() != "account-2" {
158 t.Errorf("Expected selected account ID 'account-2', got %q", multiComposer.GetSelectedAccountID())
159 }
160 })
161
162 t.Run("Single account no picker", func(t *testing.T) {
163 singleAccounts := []config.Account{
164 {ID: "account-1", Email: "test@example.com"},
165 }
166 singleComposer := NewComposerWithAccounts(singleAccounts, "account-1", "", "", "")
167
168 // Move focus to From field
169 singleComposer.focusIndex = focusFrom
170
171 // Press Enter - should not open picker with single account
172 model, _ := singleComposer.Update(tea.KeyMsg{Type: tea.KeyEnter})
173 singleComposer = model.(*Composer)
174
175 if singleComposer.showAccountPicker {
176 t.Error("Account picker should not open with single account")
177 }
178 })
179
180 t.Run("Multi-account focus cycling includes From", func(t *testing.T) {
181 multiAccounts := []config.Account{
182 {ID: "account-1", Email: "test1@example.com"},
183 {ID: "account-2", Email: "test2@example.com"},
184 }
185 multiComposer := NewComposerWithAccounts(multiAccounts, "account-1", "", "", "")
186
187 // Initial focus is on 'To' field
188 if multiComposer.focusIndex != focusTo {
189 t.Errorf("Initial focusIndex should be %d (focusTo), got %d", focusTo, multiComposer.focusIndex)
190 }
191
192 // Tab through all fields: To -> Cc -> Bcc -> Subject -> Body -> Signature -> Attachment -> Send -> From (wrap)
193 model, _ := multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // To -> Cc
194 multiComposer = model.(*Composer)
195 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Cc -> Bcc
196 multiComposer = model.(*Composer)
197 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Bcc -> Subject
198 multiComposer = model.(*Composer)
199 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Subject -> Body
200 multiComposer = model.(*Composer)
201 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Body -> Signature
202 multiComposer = model.(*Composer)
203 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Signature -> Attachment
204 multiComposer = model.(*Composer)
205 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Attachment -> Send
206 multiComposer = model.(*Composer)
207 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Send -> From (wrap)
208 multiComposer = model.(*Composer)
209 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // From -> To (wrap)
210 multiComposer = model.(*Composer)
211
212 // With multiple accounts, From field should be included in tab order
213 if multiComposer.focusIndex != focusTo {
214 t.Errorf("After nine Tabs with multi-account, focusIndex should wrap to %d (focusTo), got %d", focusTo, multiComposer.focusIndex)
215 }
216 })
217}
218
219// TestComposerGetFromAddress verifies the from address formatting.
220func TestComposerGetFromAddress(t *testing.T) {
221 t.Run("With name", func(t *testing.T) {
222 accounts := []config.Account{
223 {ID: "account-1", Email: "test@example.com", Name: "Test User"},
224 }
225 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
226
227 fromAddr := composer.getFromAddress()
228 expected := "Test User <test@example.com>"
229 if fromAddr != expected {
230 t.Errorf("Expected from address %q, got %q", expected, fromAddr)
231 }
232 })
233
234 t.Run("Without name", func(t *testing.T) {
235 accounts := []config.Account{
236 {ID: "account-1", Email: "test@example.com"},
237 }
238 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
239
240 fromAddr := composer.getFromAddress()
241 expected := "test@example.com"
242 if fromAddr != expected {
243 t.Errorf("Expected from address %q, got %q", expected, fromAddr)
244 }
245 })
246
247 t.Run("No accounts", func(t *testing.T) {
248 composer := NewComposer("", "", "", "")
249
250 fromAddr := composer.getFromAddress()
251 if fromAddr != "" {
252 t.Errorf("Expected empty from address, got %q", fromAddr)
253 }
254 })
255}
256
257// TestComposerSetSelectedAccount verifies account selection.
258func TestComposerSetSelectedAccount(t *testing.T) {
259 accounts := []config.Account{
260 {ID: "account-1", Email: "test1@example.com"},
261 {ID: "account-2", Email: "test2@example.com"},
262 {ID: "account-3", Email: "test3@example.com"},
263 }
264 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
265
266 composer.SetSelectedAccount("account-3")
267 if composer.selectedAccountIdx != 2 {
268 t.Errorf("Expected selectedAccountIdx 2, got %d", composer.selectedAccountIdx)
269 }
270 if composer.GetSelectedAccountID() != "account-3" {
271 t.Errorf("Expected selected account ID 'account-3', got %q", composer.GetSelectedAccountID())
272 }
273
274 // Test non-existent account (should not change)
275 composer.SetSelectedAccount("non-existent")
276 if composer.selectedAccountIdx != 2 {
277 t.Errorf("Expected selectedAccountIdx to remain 2, got %d", composer.selectedAccountIdx)
278 }
279}