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 to the 'From' field.
54 model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab})
55 composer = model.(*Composer)
56 if composer.focusIndex != focusFrom {
57 t.Errorf("After five Tabs, focusIndex should wrap to %d (focusFrom), got %d", focusFrom, composer.focusIndex)
58 }
59 })
60
61 t.Run("Send email message", func(t *testing.T) {
62 // Re-initialize composer for this test
63 composer = NewComposerWithAccounts(accounts, "account-1", "", "", "")
64
65 // Set values for the email fields.
66 composer.toInput.SetValue("recipient@example.com")
67 composer.subjectInput.SetValue("Test Subject")
68 composer.bodyInput.SetValue("This is the body.")
69 // Set focus to the Send button.
70 composer.focusIndex = focusSend
71
72 // Simulate pressing Enter to send the email.
73 _, cmd := composer.Update(tea.KeyMsg{Type: tea.KeyEnter})
74 if cmd == nil {
75 t.Fatal("Expected a command to be returned, but got nil.")
76 }
77
78 // Execute the command and check the resulting message.
79 msg := cmd()
80 sendMsg, ok := msg.(SendEmailMsg)
81 if !ok {
82 t.Fatalf("Expected a SendEmailMsg, but got %T", msg)
83 }
84
85 // Verify the content of the message.
86 if sendMsg.To != "recipient@example.com" {
87 t.Errorf("Expected To 'recipient@example.com', got %q", sendMsg.To)
88 }
89 if sendMsg.Subject != "Test Subject" {
90 t.Errorf("Expected Subject 'Test Subject', got %q", sendMsg.Subject)
91 }
92 if sendMsg.Body != "This is the body." {
93 t.Errorf("Expected Body 'This is the body.', got %q", sendMsg.Body)
94 }
95 if sendMsg.AccountID != "account-1" {
96 t.Errorf("Expected AccountID 'account-1', got %q", sendMsg.AccountID)
97 }
98 })
99
100 t.Run("Account picker with multiple accounts", func(t *testing.T) {
101 multiAccounts := []config.Account{
102 {ID: "account-1", Email: "test1@example.com", Name: "User 1"},
103 {ID: "account-2", Email: "test2@example.com", Name: "User 2"},
104 }
105 multiComposer := NewComposerWithAccounts(multiAccounts, "account-1", "", "", "")
106
107 // Move focus to From field
108 multiComposer.focusIndex = focusFrom
109
110 // Press Enter to open account picker
111 model, _ := multiComposer.Update(tea.KeyMsg{Type: tea.KeyEnter})
112 multiComposer = model.(*Composer)
113
114 if !multiComposer.showAccountPicker {
115 t.Error("Expected account picker to be shown")
116 }
117
118 // Navigate down to select second account
119 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyDown})
120 multiComposer = model.(*Composer)
121
122 if multiComposer.selectedAccountIdx != 1 {
123 t.Errorf("Expected selectedAccountIdx to be 1, got %d", multiComposer.selectedAccountIdx)
124 }
125
126 // Press Enter to confirm selection
127 model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyEnter})
128 multiComposer = model.(*Composer)
129
130 if multiComposer.showAccountPicker {
131 t.Error("Expected account picker to be closed")
132 }
133
134 // Verify the selected account
135 if multiComposer.GetSelectedAccountID() != "account-2" {
136 t.Errorf("Expected selected account ID 'account-2', got %q", multiComposer.GetSelectedAccountID())
137 }
138 })
139
140 t.Run("Single account no picker", func(t *testing.T) {
141 singleAccounts := []config.Account{
142 {ID: "account-1", Email: "test@example.com"},
143 }
144 singleComposer := NewComposerWithAccounts(singleAccounts, "account-1", "", "", "")
145
146 // Move focus to From field
147 singleComposer.focusIndex = focusFrom
148
149 // Press Enter - should not open picker with single account
150 model, _ := singleComposer.Update(tea.KeyMsg{Type: tea.KeyEnter})
151 singleComposer = model.(*Composer)
152
153 if singleComposer.showAccountPicker {
154 t.Error("Account picker should not open with single account")
155 }
156 })
157}
158
159// TestComposerGetFromAddress verifies the from address formatting.
160func TestComposerGetFromAddress(t *testing.T) {
161 t.Run("With name", func(t *testing.T) {
162 accounts := []config.Account{
163 {ID: "account-1", Email: "test@example.com", Name: "Test User"},
164 }
165 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
166
167 fromAddr := composer.getFromAddress()
168 expected := "Test User <test@example.com>"
169 if fromAddr != expected {
170 t.Errorf("Expected from address %q, got %q", expected, fromAddr)
171 }
172 })
173
174 t.Run("Without name", func(t *testing.T) {
175 accounts := []config.Account{
176 {ID: "account-1", Email: "test@example.com"},
177 }
178 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
179
180 fromAddr := composer.getFromAddress()
181 expected := "test@example.com"
182 if fromAddr != expected {
183 t.Errorf("Expected from address %q, got %q", expected, fromAddr)
184 }
185 })
186
187 t.Run("No accounts", func(t *testing.T) {
188 composer := NewComposer("", "", "", "")
189
190 fromAddr := composer.getFromAddress()
191 if fromAddr != "" {
192 t.Errorf("Expected empty from address, got %q", fromAddr)
193 }
194 })
195}
196
197// TestComposerSetSelectedAccount verifies account selection.
198func TestComposerSetSelectedAccount(t *testing.T) {
199 accounts := []config.Account{
200 {ID: "account-1", Email: "test1@example.com"},
201 {ID: "account-2", Email: "test2@example.com"},
202 {ID: "account-3", Email: "test3@example.com"},
203 }
204 composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
205
206 composer.SetSelectedAccount("account-3")
207 if composer.selectedAccountIdx != 2 {
208 t.Errorf("Expected selectedAccountIdx 2, got %d", composer.selectedAccountIdx)
209 }
210 if composer.GetSelectedAccountID() != "account-3" {
211 t.Errorf("Expected selected account ID 'account-3', got %q", composer.GetSelectedAccountID())
212 }
213
214 // Test non-existent account (should not change)
215 composer.SetSelectedAccount("non-existent")
216 if composer.selectedAccountIdx != 2 {
217 t.Errorf("Expected selectedAccountIdx to remain 2, got %d", composer.selectedAccountIdx)
218 }
219}