v0.26.1 75cd777

resolver_test.go RawHistoryFolder

1package colorscheme
2
3import (
4	"sync"
5	"testing"
6
7	"github.com/bnema/dumber/internal/application/port"
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/require"
10)
11
12// mockConfigProvider implements ConfigProvider for testing.
13type mockConfigProvider struct {
14	scheme string
15}
16
17func (m *mockConfigProvider) GetColorScheme() string {
18	return m.scheme
19}
20
21// mockDetector implements port.ColorSchemeDetector for testing.
22type mockDetector struct {
23	name        string
24	priority    int
25	available   bool
26	prefersDark bool
27	detectOk    bool
28}
29
30func (m *mockDetector) Name() string         { return m.name }
31func (m *mockDetector) Priority() int        { return m.priority }
32func (m *mockDetector) Available() bool      { return m.available }
33func (m *mockDetector) Detect() (bool, bool) { return m.prefersDark, m.detectOk }
34
35func TestResolver_ConfigOverride(t *testing.T) {
36	tests := []struct {
37		name        string
38		configValue string
39		wantDark    bool
40		wantSource  string
41	}{
42		{
43			name:        "prefer-dark from config",
44			configValue: "prefer-dark",
45			wantDark:    true,
46			wantSource:  "config",
47		},
48		{
49			name:        "dark from config",
50			configValue: "dark",
51			wantDark:    true,
52			wantSource:  "config",
53		},
54		{
55			name:        "prefer-light from config",
56			configValue: "prefer-light",
57			wantDark:    false,
58			wantSource:  "config",
59		},
60		{
61			name:        "light from config",
62			configValue: "light",
63			wantDark:    false,
64			wantSource:  "config",
65		},
66	}
67
68	for _, tt := range tests {
69		t.Run(tt.name, func(t *testing.T) {
70			config := &mockConfigProvider{scheme: tt.configValue}
71			resolver := NewResolver(config)
72
73			pref := resolver.Resolve()
74
75			assert.Equal(t, tt.wantDark, pref.PrefersDark)
76			assert.Equal(t, tt.wantSource, pref.Source)
77		})
78	}
79}
80
81func TestResolver_DefaultFallsThrough(t *testing.T) {
82	config := &mockConfigProvider{scheme: "default"}
83	resolver := NewResolver(config)
84
85	// Add a detector that returns light
86	detector := &mockDetector{
87		name:        "test",
88		priority:    50,
89		available:   true,
90		prefersDark: false,
91		detectOk:    true,
92	}
93	resolver.RegisterDetector(detector)
94
95	pref := resolver.Resolve()
96
97	assert.False(t, pref.PrefersDark)
98	assert.Equal(t, "test", pref.Source)
99}
100
101func TestResolver_DetectorPriority(t *testing.T) {
102	config := &mockConfigProvider{scheme: "default"}
103	resolver := NewResolver(config)
104
105	// Low priority detector returns dark
106	lowPriority := &mockDetector{
107		name:        "low",
108		priority:    10,
109		available:   true,
110		prefersDark: true,
111		detectOk:    true,
112	}
113	// High priority detector returns light
114	highPriority := &mockDetector{
115		name:        "high",
116		priority:    100,
117		available:   true,
118		prefersDark: false,
119		detectOk:    true,
120	}
121
122	// Register low first, high second (order shouldn't matter)
123	resolver.RegisterDetector(lowPriority)
124	resolver.RegisterDetector(highPriority)
125
126	pref := resolver.Resolve()
127
128	// Should use high priority detector
129	assert.False(t, pref.PrefersDark)
130	assert.Equal(t, "high", pref.Source)
131}
132
133func TestResolver_SkipsUnavailableDetector(t *testing.T) {
134	config := &mockConfigProvider{scheme: "default"}
135	resolver := NewResolver(config)
136
137	// High priority but unavailable
138	unavailable := &mockDetector{
139		name:        "unavailable",
140		priority:    100,
141		available:   false,
142		prefersDark: false,
143		detectOk:    true,
144	}
145	// Low priority but available
146	available := &mockDetector{
147		name:        "available",
148		priority:    10,
149		available:   true,
150		prefersDark: true,
151		detectOk:    true,
152	}
153
154	resolver.RegisterDetector(unavailable)
155	resolver.RegisterDetector(available)
156
157	pref := resolver.Resolve()
158
159	// Should use available detector
160	assert.True(t, pref.PrefersDark)
161	assert.Equal(t, "available", pref.Source)
162}
163
164func TestResolver_SkipsFailedDetection(t *testing.T) {
165	config := &mockConfigProvider{scheme: "default"}
166	resolver := NewResolver(config)
167
168	// High priority but detection fails
169	failing := &mockDetector{
170		name:        "failing",
171		priority:    100,
172		available:   true,
173		prefersDark: false,
174		detectOk:    false, // Detection fails
175	}
176	// Low priority but succeeds
177	succeeding := &mockDetector{
178		name:        "succeeding",
179		priority:    10,
180		available:   true,
181		prefersDark: true,
182		detectOk:    true,
183	}
184
185	resolver.RegisterDetector(failing)
186	resolver.RegisterDetector(succeeding)
187
188	pref := resolver.Resolve()
189
190	// Should use succeeding detector
191	assert.True(t, pref.PrefersDark)
192	assert.Equal(t, "succeeding", pref.Source)
193}
194
195func TestResolver_FallbackWhenNoDetectors(t *testing.T) {
196	config := &mockConfigProvider{scheme: "default"}
197	resolver := NewResolver(config)
198
199	// No detectors registered
200	pref := resolver.Resolve()
201
202	// Should fallback to dark
203	assert.True(t, pref.PrefersDark)
204	assert.Equal(t, "fallback", pref.Source)
205}
206
207func TestResolver_FallbackWhenAllFail(t *testing.T) {
208	config := &mockConfigProvider{scheme: "default"}
209	resolver := NewResolver(config)
210
211	// All detectors fail
212	resolver.RegisterDetector(&mockDetector{
213		name:      "fail1",
214		priority:  100,
215		available: true,
216		detectOk:  false,
217	})
218	resolver.RegisterDetector(&mockDetector{
219		name:      "fail2",
220		priority:  50,
221		available: false,
222	})
223
224	pref := resolver.Resolve()
225
226	// Should fallback to dark
227	assert.True(t, pref.PrefersDark)
228	assert.Equal(t, "fallback", pref.Source)
229}
230
231func TestResolver_NilConfig(t *testing.T) {
232	resolver := NewResolver(nil)
233
234	detector := &mockDetector{
235		name:        "test",
236		priority:    50,
237		available:   true,
238		prefersDark: false,
239		detectOk:    true,
240	}
241	resolver.RegisterDetector(detector)
242
243	pref := resolver.Resolve()
244
245	// Should use detector since config is nil
246	assert.False(t, pref.PrefersDark)
247	assert.Equal(t, "test", pref.Source)
248}
249
250func TestResolver_Refresh(t *testing.T) {
251	config := &mockConfigProvider{scheme: "default"}
252	resolver := NewResolver(config)
253
254	// Start with light-preferring detector
255	detector := &mockDetector{
256		name:        "test",
257		priority:    50,
258		available:   true,
259		prefersDark: false,
260		detectOk:    true,
261	}
262	resolver.RegisterDetector(detector)
263
264	pref1 := resolver.Refresh()
265	assert.False(t, pref1.PrefersDark)
266
267	// Change detector preference
268	detector.prefersDark = true
269
270	pref2 := resolver.Refresh()
271	assert.True(t, pref2.PrefersDark)
272}
273
274func TestResolver_OnChange(t *testing.T) {
275	config := &mockConfigProvider{scheme: "default"}
276	resolver := NewResolver(config)
277
278	detector := &mockDetector{
279		name:        "test",
280		priority:    50,
281		available:   true,
282		prefersDark: false,
283		detectOk:    true,
284	}
285	resolver.RegisterDetector(detector)
286
287	var callbackPref port.ColorSchemePreference
288	var callbackCount int
289	resolver.OnChange(func(pref port.ColorSchemePreference) {
290		callbackPref = pref
291		callbackCount++
292	})
293
294	// Initial refresh - callback should be called (default was dark, now light)
295	resolver.Refresh()
296	assert.Equal(t, 1, callbackCount)
297	assert.False(t, callbackPref.PrefersDark)
298
299	// Same preference - callback should NOT be called
300	resolver.Refresh()
301	assert.Equal(t, 1, callbackCount)
302
303	// Change preference - callback should be called
304	detector.prefersDark = true
305	resolver.Refresh()
306	assert.Equal(t, 2, callbackCount)
307	assert.True(t, callbackPref.PrefersDark)
308}
309
310func TestResolver_OnChangeUnregister(t *testing.T) {
311	config := &mockConfigProvider{scheme: "default"}
312	resolver := NewResolver(config)
313
314	detector := &mockDetector{
315		name:        "test",
316		priority:    50,
317		available:   true,
318		prefersDark: false,
319		detectOk:    true,
320	}
321	resolver.RegisterDetector(detector)
322
323	var callbackCount int
324	unregister := resolver.OnChange(func(_ port.ColorSchemePreference) {
325		callbackCount++
326	})
327
328	// Initial refresh triggers callback
329	resolver.Refresh()
330	assert.Equal(t, 1, callbackCount)
331
332	// Unregister
333	unregister()
334
335	// Change preference - callback should NOT be called
336	detector.prefersDark = true
337	resolver.Refresh()
338	assert.Equal(t, 1, callbackCount) // Still 1, not 2
339}
340
341func TestResolver_ConcurrentAccess(_ *testing.T) {
342	config := &mockConfigProvider{scheme: "default"}
343	resolver := NewResolver(config)
344
345	detector := &mockDetector{
346		name:        "test",
347		priority:    50,
348		available:   true,
349		prefersDark: false,
350		detectOk:    true,
351	}
352	resolver.RegisterDetector(detector)
353
354	var wg sync.WaitGroup
355	const goroutines = 10
356
357	// Concurrent Resolve calls
358	for i := 0; i < goroutines; i++ {
359		wg.Add(1)
360		go func() {
361			defer wg.Done()
362			for j := 0; j < 100; j++ {
363				resolver.Resolve()
364			}
365		}()
366	}
367
368	// Concurrent Refresh calls
369	for i := 0; i < goroutines; i++ {
370		wg.Add(1)
371		go func() {
372			defer wg.Done()
373			for j := 0; j < 100; j++ {
374				resolver.Refresh()
375			}
376		}()
377	}
378
379	// Concurrent RegisterDetector calls
380	for i := 0; i < goroutines; i++ {
381		wg.Add(1)
382		go func(id int) {
383			defer wg.Done()
384			resolver.RegisterDetector(&mockDetector{
385				name:        "concurrent",
386				priority:    id,
387				available:   true,
388				prefersDark: id%2 == 0,
389				detectOk:    true,
390			})
391		}(i)
392	}
393
394	wg.Wait()
395	// Test passes if no race conditions detected
396}
397
398func TestResolver_EmptyConfigScheme(t *testing.T) {
399	config := &mockConfigProvider{scheme: ""}
400	resolver := NewResolver(config)
401
402	detector := &mockDetector{
403		name:        "test",
404		priority:    50,
405		available:   true,
406		prefersDark: false,
407		detectOk:    true,
408	}
409	resolver.RegisterDetector(detector)
410
411	pref := resolver.Resolve()
412
413	// Empty config should fall through to detector
414	assert.False(t, pref.PrefersDark)
415	assert.Equal(t, "test", pref.Source)
416}
417
418func TestResolver_ImplementsInterface(t *testing.T) {
419	config := &mockConfigProvider{scheme: "default"}
420	resolver := NewResolver(config)
421
422	// Verify Resolver implements port.ColorSchemeResolver
423	var _ port.ColorSchemeResolver = resolver
424	require.NotNil(t, resolver)
425}

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