session_test.go

  1package model
  2
  3import (
  4	"strings"
  5	"testing"
  6
  7	"charm.land/lipgloss/v2"
  8	"git.secluded.site/crush/internal/history"
  9	"git.secluded.site/crush/internal/ui/styles"
 10	"github.com/stretchr/testify/require"
 11)
 12
 13func TestFileList(t *testing.T) {
 14	t.Parallel()
 15
 16	t.Run("empty stats no truncation needed", func(t *testing.T) {
 17		t.Parallel()
 18
 19		st := minimalFileStyles()
 20		files := []SessionFile{
 21			{FirstVersion: history.File{Path: "main.go"}, Additions: 0, Deletions: 0},
 22		}
 23		got := fileList(st, "/", files, 30, 10)
 24		require.Contains(t, stripANSI(got), "main.go")
 25	})
 26
 27	t.Run("empty stats path truncates to width", func(t *testing.T) {
 28		t.Parallel()
 29
 30		st := minimalFileStyles()
 31		files := []SessionFile{
 32			{FirstVersion: history.File{Path: "/very/long/path/to/some/deeply/nested/file.go"}, Additions: 0, Deletions: 0},
 33		}
 34		got := fileList(st, "/", files, 10, 10)
 35		plain := stripANSI(got)
 36		for _, line := range strings.Split(plain, "\n") {
 37			require.LessOrEqual(t, lipgloss.Width(line), 10, "line exceeds sidebar width: %q", line)
 38		}
 39	})
 40
 41	t.Run("with additions and deletions fits within width", func(t *testing.T) {
 42		t.Parallel()
 43
 44		st := minimalFileStyles()
 45		files := []SessionFile{
 46			{FirstVersion: history.File{Path: "main.go"}, Additions: 5, Deletions: 3},
 47		}
 48		got := fileList(st, "/", files, 20, 10)
 49		plain := stripANSI(got)
 50		require.Contains(t, plain, "+5")
 51		require.Contains(t, plain, "-3")
 52		for _, line := range strings.Split(plain, "\n") {
 53			require.LessOrEqual(t, lipgloss.Width(line), 20, "line exceeds sidebar width: %q", line)
 54		}
 55	})
 56
 57	t.Run("narrow width with stats clamps path to zero", func(t *testing.T) {
 58		t.Parallel()
 59
 60		st := minimalFileStyles()
 61		files := []SessionFile{
 62			{FirstVersion: history.File{Path: "main.go"}, Additions: 100, Deletions: 200},
 63		}
 64		got := fileList(st, "/", files, 5, 10)
 65		plain := stripANSI(got)
 66		require.NotContains(t, plain, "main.go")
 67		require.Equal(t, "+100 -200", strings.TrimSpace(plain))
 68	})
 69
 70	t.Run("single addition only", func(t *testing.T) {
 71		t.Parallel()
 72
 73		st := minimalFileStyles()
 74		files := []SessionFile{
 75			{FirstVersion: history.File{Path: "main.go"}, Additions: 3, Deletions: 0},
 76		}
 77		got := fileList(st, "/", files, 20, 10)
 78		plain := stripANSI(got)
 79		require.Contains(t, plain, "+3")
 80		require.NotContains(t, plain, "-0")
 81		for _, line := range strings.Split(plain, "\n") {
 82			require.LessOrEqual(t, lipgloss.Width(line), 20, "line exceeds sidebar width: %q", line)
 83		}
 84	})
 85
 86	t.Run("single deletion only", func(t *testing.T) {
 87		t.Parallel()
 88
 89		st := minimalFileStyles()
 90		files := []SessionFile{
 91			{FirstVersion: history.File{Path: "main.go"}, Additions: 0, Deletions: 7},
 92		}
 93		got := fileList(st, "/", files, 20, 10)
 94		plain := stripANSI(got)
 95		require.NotContains(t, plain, "+0")
 96		require.Contains(t, plain, "-7")
 97		for _, line := range strings.Split(plain, "\n") {
 98			require.LessOrEqual(t, lipgloss.Width(line), 20, "line exceeds sidebar width: %q", line)
 99		}
100	})
101
102	t.Run("max items zero returns empty", func(t *testing.T) {
103		t.Parallel()
104
105		st := minimalFileStyles()
106		files := []SessionFile{
107			{FirstVersion: history.File{Path: "main.go"}, Additions: 1, Deletions: 1},
108		}
109		got := fileList(st, "/", files, 20, 0)
110		require.Empty(t, got)
111	})
112}
113
114func minimalFileStyles() *styles.Styles {
115	st := styles.CharmtonePantera()
116	st.Files.Path = lipgloss.NewStyle()
117	st.Files.Additions = lipgloss.NewStyle()
118	st.Files.Deletions = lipgloss.NewStyle()
119	st.Files.SectionTitle = lipgloss.NewStyle()
120	st.Files.EmptyMessage = lipgloss.NewStyle()
121	st.Files.TruncationHint = lipgloss.NewStyle()
122	return &st
123}
124
125func stripANSI(s string) string {
126	var b strings.Builder
127	esc := false
128	for i := 0; i < len(s); i++ {
129		if s[i] == '\x1b' {
130			esc = true
131			continue
132		}
133		if esc {
134			if s[i] >= 'a' && s[i] <= 'z' || s[i] >= 'A' && s[i] <= 'Z' {
135				esc = false
136			}
137			continue
138		}
139		b.WriteByte(s[i])
140	}
141	return b.String()
142}