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}