inbox_test.go

  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}