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