1package tui
2
3import (
4 "strings"
5 "testing"
6 "time"
7
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/floatpane/matcha/config"
10 "github.com/floatpane/matcha/fetcher"
11)
12
13func collectMsgs(cmd tea.Cmd) []tea.Msg {
14 if cmd == nil {
15 return nil
16 }
17 msg := cmd()
18 switch batch := msg.(type) {
19 case tea.BatchMsg:
20 var msgs []tea.Msg
21 for _, m := range batch {
22 if m != nil {
23 msgs = append(msgs, m)
24 }
25 }
26 return msgs
27 default:
28 if msg != nil {
29 return []tea.Msg{msg}
30 }
31 }
32 return nil
33}
34
35// TestInboxUpdate verifies the state transitions in the inbox view.
36func TestInboxUpdate(t *testing.T) {
37 // Create sample accounts
38 accounts := []config.Account{
39 {ID: "account-1", Email: "test1@example.com", Name: "Test User 1"},
40 {ID: "account-2", Email: "test2@example.com", Name: "Test User 2"},
41 }
42
43 // Create a sample list of emails.
44 sampleEmails := []fetcher.Email{
45 {UID: 1, From: "a@example.com", Subject: "Email 1", Date: time.Now(), AccountID: "account-1"},
46 {UID: 2, From: "b@example.com", Subject: "Email 2", Date: time.Now().Add(-time.Hour), AccountID: "account-1"},
47 {UID: 3, From: "c@example.com", Subject: "Email 3", Date: time.Now().Add(-2 * time.Hour), AccountID: "account-2"},
48 }
49
50 inbox := NewInbox(sampleEmails, accounts)
51
52 t.Run("Select email to view", func(t *testing.T) {
53 // By default, the first item is selected (index 0).
54 // Move down to the second item (index 1).
55 inbox.list, _ = inbox.list.Update(tea.KeyMsg{Type: tea.KeyDown})
56
57 // Simulate pressing Enter to view the selected email.
58 _, cmd := inbox.Update(tea.KeyMsg{Type: tea.KeyEnter})
59 if cmd == nil {
60 t.Fatal("Expected a command, but got nil.")
61 }
62
63 // Check the resulting message.
64 msg := cmd()
65 viewMsg, ok := msg.(ViewEmailMsg)
66 if !ok {
67 t.Fatalf("Expected a ViewEmailMsg, but got %T", msg)
68 }
69
70 // The index should match the selected item in the list.
71 expectedIndex := 1
72 if viewMsg.Index != expectedIndex {
73 t.Errorf("Expected index %d, but got %d", expectedIndex, viewMsg.Index)
74 }
75
76 // Verify UID and AccountID are passed correctly
77 expectedUID := uint32(2) // Second email has UID 2
78 if viewMsg.UID != expectedUID {
79 t.Errorf("Expected UID %d, but got %d", expectedUID, viewMsg.UID)
80 }
81
82 expectedAccountID := "account-1" // Second email belongs to account-1
83 if viewMsg.AccountID != expectedAccountID {
84 t.Errorf("Expected AccountID %q, but got %q", expectedAccountID, viewMsg.AccountID)
85 }
86 })
87}
88
89// TestInboxMultiAccountTabs verifies that tabs are created for multiple accounts.
90func TestInboxMultiAccountTabs(t *testing.T) {
91 accounts := []config.Account{
92 {ID: "account-1", Email: "test1@example.com", Name: "User 1"},
93 {ID: "account-2", Email: "test2@example.com", Name: "User 2"},
94 }
95
96 emails := []fetcher.Email{
97 {UID: 1, From: "sender@example.com", Subject: "Test", AccountID: "account-1"},
98 }
99
100 inbox := NewInbox(emails, accounts)
101
102 // Should have 3 tabs: ALL + 2 accounts
103 if len(inbox.tabs) != 3 {
104 t.Errorf("Expected 3 tabs, got %d", len(inbox.tabs))
105 }
106
107 // First tab should be "ALL"
108 if inbox.tabs[0].ID != "" {
109 t.Errorf("Expected first tab ID to be empty (ALL), got %q", inbox.tabs[0].ID)
110 }
111 if inbox.tabs[0].Label != "ALL" {
112 t.Errorf("Expected first tab label to be 'ALL', got %q", inbox.tabs[0].Label)
113 }
114}
115
116// TestInboxSingleAccount verifies behavior with a single account.
117func TestInboxSingleAccount(t *testing.T) {
118 accounts := []config.Account{
119 {ID: "account-1", Email: "test@example.com"},
120 }
121
122 emails := []fetcher.Email{
123 {UID: 1, From: "sender@example.com", Subject: "Test", AccountID: "account-1"},
124 }
125
126 inbox := NewInbox(emails, accounts)
127
128 // Should have 2 tabs: ALL + 1 account
129 if len(inbox.tabs) != 2 {
130 t.Errorf("Expected 2 tabs, got %d", len(inbox.tabs))
131 }
132}
133
134// TestInboxNoAccounts verifies behavior with no accounts (legacy/edge case).
135func TestInboxNoAccounts(t *testing.T) {
136 emails := []fetcher.Email{
137 {UID: 1, From: "sender@example.com", Subject: "Test"},
138 }
139
140 inbox := NewInbox(emails, nil)
141
142 // Should have 1 tab: ALL only
143 if len(inbox.tabs) != 1 {
144 t.Errorf("Expected 1 tab, got %d", len(inbox.tabs))
145 }
146}
147
148// TestInboxDeleteEmailMsg verifies that delete messages include account ID.
149func TestInboxDeleteEmailMsg(t *testing.T) {
150 accounts := []config.Account{
151 {ID: "account-1", Email: "test@example.com"},
152 }
153
154 emails := []fetcher.Email{
155 {UID: 123, From: "sender@example.com", Subject: "Test", AccountID: "account-1"},
156 }
157
158 inbox := NewInbox(emails, accounts)
159
160 // Simulate pressing 'd' to delete
161 _, cmd := inbox.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}})
162 if cmd == nil {
163 t.Fatal("Expected a command, but got nil.")
164 }
165
166 msg := cmd()
167 deleteMsg, ok := msg.(DeleteEmailMsg)
168 if !ok {
169 t.Fatalf("Expected a DeleteEmailMsg, but got %T", msg)
170 }
171
172 if deleteMsg.UID != 123 {
173 t.Errorf("Expected UID 123, got %d", deleteMsg.UID)
174 }
175
176 if deleteMsg.AccountID != "account-1" {
177 t.Errorf("Expected AccountID 'account-1', got %q", deleteMsg.AccountID)
178 }
179}
180
181// TestInboxArchiveEmailMsg verifies that archive messages include account ID.
182func TestInboxArchiveEmailMsg(t *testing.T) {
183 accounts := []config.Account{
184 {ID: "account-1", Email: "test@example.com"},
185 }
186
187 emails := []fetcher.Email{
188 {UID: 456, From: "sender@example.com", Subject: "Test", AccountID: "account-1"},
189 }
190
191 inbox := NewInbox(emails, accounts)
192
193 // Simulate pressing 'a' to archive
194 _, cmd := inbox.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}})
195 if cmd == nil {
196 t.Fatal("Expected a command, but got nil.")
197 }
198
199 msg := cmd()
200 archiveMsg, ok := msg.(ArchiveEmailMsg)
201 if !ok {
202 t.Fatalf("Expected an ArchiveEmailMsg, but got %T", msg)
203 }
204
205 if archiveMsg.UID != 456 {
206 t.Errorf("Expected UID 456, got %d", archiveMsg.UID)
207 }
208
209 if archiveMsg.AccountID != "account-1" {
210 t.Errorf("Expected AccountID 'account-1', got %q", archiveMsg.AccountID)
211 }
212}
213
214// TestInboxRemoveEmail verifies that emails can be removed from the inbox.
215func TestInboxRemoveEmail(t *testing.T) {
216 accounts := []config.Account{
217 {ID: "account-1", Email: "test@example.com"},
218 }
219
220 emails := []fetcher.Email{
221 {UID: 1, From: "a@example.com", Subject: "Email 1", AccountID: "account-1"},
222 {UID: 2, From: "b@example.com", Subject: "Email 2", AccountID: "account-1"},
223 }
224
225 inbox := NewInbox(emails, accounts)
226
227 // Remove the first email
228 inbox.RemoveEmail(1, "account-1")
229
230 // Check that only one email remains
231 if len(inbox.allEmails) != 1 {
232 t.Errorf("Expected 1 email after removal, got %d", len(inbox.allEmails))
233 }
234
235 if inbox.allEmails[0].UID != 2 {
236 t.Errorf("Expected remaining email UID to be 2, got %d", inbox.allEmails[0].UID)
237 }
238}
239
240// TestInboxGetEmailAtIndex verifies retrieving emails by index.
241func TestInboxGetEmailAtIndex(t *testing.T) {
242 accounts := []config.Account{
243 {ID: "account-1", Email: "test@example.com"},
244 }
245
246 emails := []fetcher.Email{
247 {UID: 1, From: "a@example.com", Subject: "Email 1", AccountID: "account-1"},
248 {UID: 2, From: "b@example.com", Subject: "Email 2", AccountID: "account-1"},
249 }
250
251 inbox := NewInbox(emails, accounts)
252
253 // Get email at index 0
254 email := inbox.GetEmailAtIndex(0)
255 if email == nil {
256 t.Fatal("Expected email at index 0, got nil")
257 }
258 if email.UID != 1 {
259 t.Errorf("Expected UID 1 at index 0, got %d", email.UID)
260 }
261
262 // Get email at invalid index
263 email = inbox.GetEmailAtIndex(999)
264 if email != nil {
265 t.Error("Expected nil for invalid index, got non-nil")
266 }
267
268 // Get email at negative index
269 email = inbox.GetEmailAtIndex(-1)
270 if email != nil {
271 t.Error("Expected nil for negative index, got non-nil")
272 }
273}
274
275func TestInboxTitleShowsCount(t *testing.T) {
276 accounts := []config.Account{
277 {ID: "account-1", Email: "test@example.com"},
278 }
279
280 emails := []fetcher.Email{
281 {UID: 1, From: "sender@example.com", Subject: "Email 1", AccountID: "account-1", Date: time.Now()},
282 {UID: 2, From: "sender@example.com", Subject: "Email 2", AccountID: "account-1", Date: time.Now().Add(-time.Minute)},
283 {UID: 3, From: "sender@example.com", Subject: "Email 3", AccountID: "account-1", Date: time.Now().Add(-2 * time.Minute)},
284 }
285
286 inbox := NewInbox(emails, accounts)
287
288 if !strings.Contains(inbox.list.Title, "Inbox (3)") {
289 t.Fatalf("expected inbox title to contain count, got %q", inbox.list.Title)
290 }
291}
292
293func TestSentInboxTitleShowsCount(t *testing.T) {
294 accounts := []config.Account{
295 {ID: "account-1", Email: "test@example.com"},
296 }
297
298 emails := []fetcher.Email{
299 {UID: 1, From: "sender@example.com", Subject: "Email 1", AccountID: "account-1", Date: time.Now()},
300 {UID: 2, From: "sender@example.com", Subject: "Email 2", AccountID: "account-1", Date: time.Now().Add(-time.Minute)},
301 }
302
303 inbox := NewSentInbox(emails, accounts)
304
305 if !strings.Contains(inbox.list.Title, "Sent (2)") {
306 t.Fatalf("expected sent title to contain count, got %q", inbox.list.Title)
307 }
308}
309
310func TestFetchMoreTriggeredAtListEnd(t *testing.T) {
311 accounts := []config.Account{
312 {ID: "account-1", Email: "test@example.com"},
313 }
314
315 emails := []fetcher.Email{
316 {UID: 1, From: "a@example.com", Subject: "Email 1", AccountID: "account-1", Date: time.Now()},
317 {UID: 2, From: "b@example.com", Subject: "Email 2", AccountID: "account-1", Date: time.Now().Add(-time.Minute)},
318 }
319
320 inbox := NewInbox(emails, accounts)
321
322 _, cmd := inbox.Update(tea.KeyMsg{Type: tea.KeyDown})
323 msgs := collectMsgs(cmd)
324
325 var fetchMsg FetchMoreEmailsMsg
326 for _, m := range msgs {
327 if msg, ok := m.(FetchMoreEmailsMsg); ok {
328 fetchMsg = msg
329 break
330 }
331 }
332
333 if fetchMsg.AccountID == "" {
334 t.Fatal("expected a FetchMoreEmailsMsg when reaching end of the list")
335 }
336
337 if fetchMsg.Offset != uint32(len(emails)) {
338 t.Fatalf("expected offset %d, got %d", len(emails), fetchMsg.Offset)
339 }
340 if fetchMsg.AccountID != "account-1" {
341 t.Fatalf("expected account ID 'account-1', got %q", fetchMsg.AccountID)
342 }
343 if fetchMsg.Mailbox != MailboxInbox {
344 t.Fatalf("expected MailboxInbox, got %s", fetchMsg.Mailbox)
345 }
346}