folder_inbox_test.go

  1package tui
  2
  3import (
  4	"testing"
  5
  6	tea "charm.land/bubbletea/v2"
  7	"github.com/floatpane/matcha/config"
  8	"github.com/floatpane/matcha/fetcher"
  9)
 10
 11// TestFolderInboxSplitPreviewRendersSearchHit covers the case Lea reported on
 12// PR #1186: opening a search result in split-pane mode used to silently drop
 13// the keypress because the email was not in m.inbox.allEmails. After the fix
 14// OpenSplitPreview accepts the resolved email and findEmailByUID falls back
 15// to it, so PreviewBodyFetchedMsg can build the preview pane.
 16func TestFolderInboxSplitPreviewRendersSearchHit(t *testing.T) {
 17	accounts := []config.Account{
 18		{ID: "account-1", Email: "host.example.com", FetchEmail: "first@example.com"},
 19	}
 20	fi := NewFolderInbox([]string{keyINBOX, "Archive"}, accounts)
 21	// Force a non-zero canvas so calculate*Width does not panic on Update.
 22	model, _ := fi.Update(tea.WindowSizeMsg{Width: 200, Height: 60})
 23	fi = model.(*FolderInbox)
 24
 25	// Search hit lives in a different folder; allEmails is empty.
 26	hit := &fetcher.Email{
 27		UID:       4242,
 28		AccountID: "account-1",
 29		MessageID: "<search-hit@example.com>",
 30		From:      "sender@example.com",
 31		To:        []string{"first@example.com"},
 32		Subject:   "Search hit",
 33	}
 34
 35	fi.OpenSplitPreview(hit.UID, hit.AccountID, hit)
 36
 37	if fi.previewSearchEmail == nil {
 38		t.Fatal("OpenSplitPreview should retain the search hit email")
 39	}
 40	if got := fi.findEmailByUID(hit.UID, hit.AccountID); got == nil {
 41		t.Fatal("findEmailByUID should fall back to the search hit email")
 42	}
 43
 44	// Simulate the body arriving and verify the preview pane is built.
 45	model, _ = fi.Update(PreviewBodyFetchedMsg{
 46		UID:       hit.UID,
 47		AccountID: hit.AccountID,
 48		Body:      "hello body",
 49	})
 50	fi = model.(*FolderInbox)
 51
 52	if fi.previewPane == nil {
 53		t.Fatal("expected previewPane to be built from the search hit fallback")
 54	}
 55
 56	// closeSplitPreview must clear the cached search hit so a later open with
 57	// no email cannot accidentally reuse the stale reference.
 58	fi.closeSplitPreview()
 59	if fi.previewSearchEmail != nil {
 60		t.Fatal("closeSplitPreview should clear previewSearchEmail")
 61	}
 62}
 63
 64// TestFolderInboxSplitPreviewPrefersAllEmails verifies that when the email is
 65// already known in allEmails, findEmailByUID returns the live entry (so reads
 66// like IsRead stay current) instead of the snapshot passed via OpenSplitPreview.
 67func TestFolderInboxSplitPreviewPrefersAllEmails(t *testing.T) {
 68	accounts := []config.Account{
 69		{ID: "account-1", Email: "host.example.com", FetchEmail: "first@example.com"},
 70	}
 71	fi := NewFolderInbox([]string{keyINBOX}, accounts)
 72	model, _ := fi.Update(tea.WindowSizeMsg{Width: 200, Height: 60})
 73	fi = model.(*FolderInbox)
 74
 75	live := fetcher.Email{UID: 7, AccountID: "account-1", Subject: "live", IsRead: true}
 76	fi.SetEmails([]fetcher.Email{live}, accounts)
 77
 78	stale := &fetcher.Email{UID: 7, AccountID: "account-1", Subject: "stale", IsRead: false}
 79	fi.OpenSplitPreview(live.UID, live.AccountID, stale)
 80
 81	got := fi.findEmailByUID(live.UID, live.AccountID)
 82	if got == nil {
 83		t.Fatal("findEmailByUID should resolve the email")
 84	}
 85	if got.Subject != "live" || !got.IsRead {
 86		t.Fatalf("expected the live allEmails entry, got %+v", got)
 87	}
 88}
 89
 90// TestSearchOverlayKeysNotIntercepted covers issue #1199: pressing keys that
 91// match folder-level bindings (e.g. "m" for move) while the search overlay is
 92// active used to trigger the move flow instead of entering text into the
 93// search input. FolderInbox.Update now passes through to the inner inbox
 94// while m.inbox.searchOverlay != nil so the overlay receives raw keystrokes.
 95func TestSearchOverlayKeysNotIntercepted(t *testing.T) {
 96	accounts := []config.Account{
 97		{ID: "account-1", Email: "host.example.com", FetchEmail: "first@example.com"},
 98	}
 99	fi := NewFolderInbox([]string{keyINBOX, "Archive"}, accounts)
100	model, _ := fi.Update(tea.WindowSizeMsg{Width: 200, Height: 60})
101	fi = model.(*FolderInbox)
102
103	// Selection must exist so the bug's "m" -> Move handler would actually fire.
104	fi.SetEmails([]fetcher.Email{
105		{UID: 1, AccountID: "account-1", Subject: "first"},
106	}, accounts)
107
108	// Open the search overlay (the same state pressing "/" produces in inbox.go).
109	fi.inbox.searchOverlay = NewSearchOverlay(fi.width, fi.height)
110
111	// Press "m" -- with the bug this would set movingEmail = true.
112	model, _ = fi.Update(tea.KeyPressMsg{Code: 'm', Text: "m"})
113	fi = model.(*FolderInbox)
114
115	if fi.movingEmail {
116		t.Fatal("pressing 'm' while search overlay is active must not start the move flow")
117	}
118	if fi.inbox.searchOverlay == nil {
119		t.Fatal("search overlay must remain open after typing into it")
120	}
121	if got := fi.inbox.searchOverlay.input.Value(); got != "m" {
122		t.Fatalf("search input should contain typed character, got %q", got)
123	}
124}