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}