snapshot_test.go

  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}