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