v0.31.0 bf0756b

detector_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close