1package tui
2
3import (
4 "flag"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/charmbracelet/x/ansi"
11 "github.com/floatpane/matcha/internal/logging"
12)
13
14var updateGolden = flag.Bool("update", false, "update golden snapshot files")
15
16// snapshotLogger is a deterministic in-memory logger for snapshot tests.
17type snapshotLogger struct {
18 entries []logging.Entry
19}
20
21func (l *snapshotLogger) Write(p []byte) (int, error) {
22 l.entries = append(l.entries, logging.Entry{Text: strings.TrimRight(string(p), "\n")})
23 return len(p), nil
24}
25func (l *snapshotLogger) MaxEntries() int { return logging.DefaultMaxEntries }
26func (l *snapshotLogger) Tail(n int) []logging.Entry {
27 if n <= 0 || len(l.entries) == 0 {
28 return nil
29 }
30 if n >= len(l.entries) {
31 out := make([]logging.Entry, len(l.entries))
32 copy(out, l.entries)
33 return out
34 }
35 out := make([]logging.Entry, n)
36 copy(out, l.entries[len(l.entries)-n:])
37 return out
38}
39func (l *snapshotLogger) Subscribe() <-chan logging.Entry { return nil }
40
41// assertGolden compares rendered output to a golden file in testdata/golden.
42// Re-run tests with `-update` to refresh the golden files.
43func assertGolden(t *testing.T, name, got string) {
44 t.Helper()
45
46 got = normalizeForGolden(got)
47
48 path := filepath.Join("testdata", "golden", name+".txt")
49
50 if *updateGolden {
51 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
52 t.Fatalf("mkdir golden: %v", err)
53 }
54 if err := os.WriteFile(path, []byte(got+"\n"), 0o644); err != nil {
55 t.Fatalf("write golden: %v", err)
56 }
57 return
58 }
59
60 want, err := os.ReadFile(path)
61 if err != nil {
62 t.Fatalf("read golden %q (run with -update to create): %v", path, err)
63 }
64 wantStr := normalizeForGolden(string(want))
65 if got != wantStr {
66 t.Fatalf("snapshot mismatch for %s\n--- got ---\n%s\n--- want ---\n%s\n--- diff ---\ngot bytes: %q\nwant bytes: %q",
67 name, got, wantStr, got, wantStr)
68 }
69}
70
71func normalizeForGolden(s string) string {
72 s = ansi.Strip(s)
73 s = strings.ReplaceAll(s, "\r\n", "\n")
74 s = strings.ReplaceAll(s, "\r", "\n")
75 s = stripTrailingSpace(s)
76 return strings.TrimRight(s, " \n\t")
77}
78
79func stripTrailingSpace(s string) string {
80 lines := strings.Split(s, "\n")
81 for i, line := range lines {
82 lines[i] = strings.TrimRight(line, " \t")
83 }
84 return strings.Join(lines, "\n")
85}
86
87func TestSnapshot_LogPanel_Empty(t *testing.T) {
88 panel := NewLogPanel(&snapshotLogger{})
89 panel.SetSize(60, 6)
90 assertGolden(t, "log_panel_empty", panel.View())
91}
92
93func TestSnapshot_LogPanel_WithEntries(t *testing.T) {
94 logger := &snapshotLogger{}
95 logger.Write([]byte("started fetcher\n"))
96 logger.Write([]byte("connected to imap.example.com\n"))
97 logger.Write([]byte("fetched 12 new messages\n"))
98
99 panel := NewLogPanel(logger)
100 panel.SetSize(60, 6)
101 assertGolden(t, "log_panel_with_entries", panel.View())
102}
103
104func TestSnapshot_LogPanel_TruncatesLongLines(t *testing.T) {
105 logger := &snapshotLogger{}
106 logger.Write([]byte(strings.Repeat("verylongline ", 20) + "\n"))
107
108 panel := NewLogPanel(logger)
109 panel.SetSize(30, 4)
110 assertGolden(t, "log_panel_truncated", panel.View())
111}
112
113func TestSnapshot_SearchOverlay_Empty(t *testing.T) {
114 overlay := NewSearchOverlay(80, 24)
115 assertGolden(t, "search_overlay_empty", overlay.View())
116}
117
118func TestSnapshot_SearchOverlay_Loading(t *testing.T) {
119 overlay := NewSearchOverlay(80, 24)
120 overlay.loading = true
121 assertGolden(t, "search_overlay_loading", overlay.View())
122}
123
124func TestSnapshot_SearchOverlay_Error(t *testing.T) {
125 overlay := NewSearchOverlay(80, 24)
126 overlay.err = "connection refused"
127 assertGolden(t, "search_overlay_error", overlay.View())
128}