inbox_test.go

  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}