1package fonts
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/rs/zerolog"
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11)
12
13func testContext() context.Context {
14 logger := zerolog.Nop()
15 return logger.WithContext(context.Background())
16}
17
18func TestDetector_IsAvailable(t *testing.T) {
19 ctx := testContext()
20 detector := NewDetector()
21
22 // fc-list should be available on most Linux systems
23 // This test documents the behavior rather than asserting a specific value
24 available := detector.IsAvailable(ctx)
25 t.Logf("fc-list available: %v", available)
26}
27
28func TestDetector_GetAvailableFonts(t *testing.T) {
29 ctx := testContext()
30 detector := NewDetector()
31
32 if !detector.IsAvailable(ctx) {
33 t.Skip("fc-list not available on this system")
34 }
35
36 fonts, err := detector.GetAvailableFonts(ctx)
37 require.NoError(t, err)
38 assert.NotEmpty(t, fonts, "expected at least some fonts to be installed")
39
40 t.Logf("Found %d font families", len(fonts))
41}
42
43func TestDetector_GetAvailableFonts_Caching(t *testing.T) {
44 ctx := testContext()
45 detector := NewDetector()
46
47 if !detector.IsAvailable(ctx) {
48 t.Skip("fc-list not available on this system")
49 }
50
51 // First call populates cache
52 fonts1, err := detector.GetAvailableFonts(ctx)
53 require.NoError(t, err)
54
55 // Second call should return cached result
56 fonts2, err := detector.GetAvailableFonts(ctx)
57 require.NoError(t, err)
58
59 assert.Len(t, fonts2, len(fonts1), "cached result should match original")
60}
61
62func TestDetector_SelectBestFont_SansSerif(t *testing.T) {
63 ctx := testContext()
64 detector := NewDetector()
65
66 if !detector.IsAvailable(ctx) {
67 t.Skip("fc-list not available on this system")
68 }
69
70 font := detector.SelectBestFont(ctx, port.FontCategorySansSerif, SansSerifFallbackChain())
71 assert.NotEmpty(t, font)
72
73 // Should either be a font from the chain or the generic fallback
74 isFromChain := false
75 for _, f := range SansSerifFallbackChain() {
76 if font == f {
77 isFromChain = true
78 break
79 }
80 }
81 if !isFromChain {
82 assert.Equal(t, "sans-serif", font, "should fall back to generic if no chain fonts available")
83 }
84
85 t.Logf("Selected sans-serif font: %s", font)
86}
87
88func TestDetector_SelectBestFont_Serif(t *testing.T) {
89 ctx := testContext()
90 detector := NewDetector()
91
92 if !detector.IsAvailable(ctx) {
93 t.Skip("fc-list not available on this system")
94 }
95
96 font := detector.SelectBestFont(ctx, port.FontCategorySerif, SerifFallbackChain())
97 assert.NotEmpty(t, font)
98
99 isFromChain := false
100 for _, f := range SerifFallbackChain() {
101 if font == f {
102 isFromChain = true
103 break
104 }
105 }
106 if !isFromChain {
107 assert.Equal(t, "serif", font, "should fall back to generic if no chain fonts available")
108 }
109
110 t.Logf("Selected serif font: %s", font)
111}
112
113func TestDetector_SelectBestFont_Monospace(t *testing.T) {
114 ctx := testContext()
115 detector := NewDetector()
116
117 if !detector.IsAvailable(ctx) {
118 t.Skip("fc-list not available on this system")
119 }
120
121 font := detector.SelectBestFont(ctx, port.FontCategoryMonospace, MonospaceFallbackChain())
122 assert.NotEmpty(t, font)
123
124 isFromChain := false
125 for _, f := range MonospaceFallbackChain() {
126 if font == f {
127 isFromChain = true
128 break
129 }
130 }
131 if !isFromChain {
132 assert.Equal(t, "monospace", font, "should fall back to generic if no chain fonts available")
133 }
134
135 t.Logf("Selected monospace font: %s", font)
136}
137
138func TestDetector_SelectBestFont_EmptyChain(t *testing.T) {
139 ctx := testContext()
140 detector := NewDetector()
141
142 if !detector.IsAvailable(ctx) {
143 t.Skip("fc-list not available on this system")
144 }
145
146 // Empty chain should return generic fallback
147 font := detector.SelectBestFont(ctx, port.FontCategorySansSerif, []string{})
148 assert.Equal(t, "sans-serif", font)
149
150 font = detector.SelectBestFont(ctx, port.FontCategorySerif, []string{})
151 assert.Equal(t, "serif", font)
152
153 font = detector.SelectBestFont(ctx, port.FontCategoryMonospace, []string{})
154 assert.Equal(t, "monospace", font)
155}
156
157func TestDetector_SelectBestFont_NoMatchingFonts(t *testing.T) {
158 ctx := testContext()
159 detector := NewDetector()
160
161 if !detector.IsAvailable(ctx) {
162 t.Skip("fc-list not available on this system")
163 }
164
165 // Chain with fonts that definitely don't exist
166 chain := []string{
167 "NonExistentFont1234",
168 "AnotherFakeFont5678",
169 }
170
171 font := detector.SelectBestFont(ctx, port.FontCategorySansSerif, chain)
172 assert.Equal(t, "sans-serif", font, "should fall back to generic when no fonts match")
173}
174
175func TestDetector_genericFallback(t *testing.T) {
176 detector := NewDetector()
177
178 tests := []struct {
179 category port.FontCategory
180 expected string
181 }{
182 {port.FontCategorySansSerif, "sans-serif"},
183 {port.FontCategorySerif, "serif"},
184 {port.FontCategoryMonospace, "monospace"},
185 {port.FontCategory("unknown"), "sans-serif"}, // default case
186 }
187
188 for _, tt := range tests {
189 t.Run(string(tt.category), func(t *testing.T) {
190 result := detector.genericFallback(tt.category)
191 assert.Equal(t, tt.expected, result)
192 })
193 }
194}
195
196func TestFallbackChains_NotEmpty(t *testing.T) {
197 assert.NotEmpty(t, SansSerifFallbackChain(), "sans-serif fallback chain should not be empty")
198 assert.NotEmpty(t, SerifFallbackChain(), "serif fallback chain should not be empty")
199 assert.NotEmpty(t, MonospaceFallbackChain(), "monospace fallback chain should not be empty")
200}
201
202func TestFallbackChains_PreferredFontsFirst(t *testing.T) {
203 // Verify preferred fonts are first in chain
204 assert.Equal(t, "Fira Sans", SansSerifFallbackChain()[0], "Fira Sans should be first in sans-serif chain")
205 assert.Equal(t, "Noto Serif", SerifFallbackChain()[0], "Noto Serif should be first in serif chain")
206 assert.Equal(t, "Fira Code", MonospaceFallbackChain()[0], "Fira Code should be first in monospace chain")
207}