v0.33.0 62a09f5

mode_legend_preferences_gateway_test.go RawHistoryFolder

1package config
2
3import (
4	"context"
5	"sync/atomic"
6	"testing"
7
8	"github.com/bnema/dumber/internal/domain/entity"
9
10	"github.com/stretchr/testify/require"
11)
12
13func TestModeLegendPreferencesGateway_SaveModeLegendCollapsed_Persists(t *testing.T) {
14	configHome := t.TempDir()
15	mgr := newTestOmniboxPreferencesGatewayManager(t, configHome)
16	gateway := NewModeLegendPreferencesGateway(mgr)
17	require.False(t, mgr.Get().Workspace.Styling.ModeLegendCollapsed)
18
19	require.NoError(t, gateway.SaveModeLegendCollapsed(context.Background(), true))
20	require.True(t, newTestOmniboxPreferencesGatewayManager(t, configHome).Get().Workspace.Styling.ModeLegendCollapsed)
21
22	require.NoError(t, gateway.SaveModeLegendCollapsed(context.Background(), false))
23	require.False(t, newTestOmniboxPreferencesGatewayManager(t, configHome).Get().Workspace.Styling.ModeLegendCollapsed)
24}
25
26func TestModeLegendPreferencesGateway_NilManagerReturnsError(t *testing.T) {
27	require.Error(t, NewModeLegendPreferencesGateway(nil).SaveModeLegendCollapsed(context.Background(), true))
28}
29
30// newWatchedTestManager mirrors production, where the manager is in watching mode
31// so Save() publishes the in-memory config directly instead of re-reading the
32// file. No fsnotify watcher is started, which keeps the tests deterministic.
33func newWatchedTestManager(t *testing.T, configHome string) *Manager {
34	t.Helper()
35
36	mgr := newTestOmniboxPreferencesGatewayManager(t, configHome)
37	mgr.watching = true
38
39	return mgr
40}
41
42// TestManagerUpdate_UpdateAfterStaleSnapshotDoesNotLoseWrites forces the
43// interleaving a Get()-then-Save() implementation loses: another update
44// publishes right after the snapshot the first update works from was taken.
45// Update must apply its change on top of that update instead of overwriting it.
46func TestManagerUpdate_UpdateAfterStaleSnapshotDoesNotLoseWrites(t *testing.T) {
47	configHome := t.TempDir()
48	mgr := newWatchedTestManager(t, configHome)
49	legend := NewModeLegendPreferencesGateway(mgr)
50	omnibox := NewOmniboxPreferencesGateway(mgr)
51
52	saveOther := func() error {
53		return omnibox.SaveOmniboxInitialBehavior(context.Background(), entity.OmniboxInitialBehaviorMostVisited)
54	}
55	queued := make(chan error, 1)
56	var fired atomic.Bool
57	mgr.afterSnapshot = func() {
58		// The nested update calls this hook too; only the first snapshot reacts.
59		if fired.Swap(true) {
60			return
61		}
62		func() {
63			if mgr.mu.TryLock() {
64				// The snapshot was taken without keeping the lock: another
65				// update publishes before this one continues.
66				mgr.mu.Unlock()
67				require.NoError(t, saveOther())
68				queued <- nil
69				return
70			}
71			// The lock is held across the whole update: the other update has
72			// to queue behind it.
73			go func() { queued <- saveOther() }()
74		}()
75	}
76
77	require.NoError(t, legend.SaveModeLegendCollapsed(context.Background(), true))
78	require.NoError(t, <-queued)
79
80	for name, got := range map[string]*Config{
81		"in-memory": mgr.Get(),
82		"on-disk":   newTestOmniboxPreferencesGatewayManager(t, configHome).Get(),
83	} {
84		require.True(t, got.Workspace.Styling.ModeLegendCollapsed, name)
85		require.Equal(t, entity.OmniboxInitialBehaviorMostVisited, got.Omnibox.InitialBehavior, name)
86	}
87}
88
89func TestManagerUpdate_AppliesToLatestConfigAfterSave(t *testing.T) {
90	mgr := newWatchedTestManager(t, t.TempDir())
91
92	cfg := mgr.Get()
93	cfg.Omnibox.InitialBehavior = entity.OmniboxInitialBehaviorMostVisited
94	require.NoError(t, mgr.Save(cfg))
95
96	require.NoError(t, NewModeLegendPreferencesGateway(mgr).SaveModeLegendCollapsed(context.Background(), true))
97
98	got := mgr.Get()
99	require.True(t, got.Workspace.Styling.ModeLegendCollapsed)
100	require.Equal(t, entity.OmniboxInitialBehaviorMostVisited, got.Omnibox.InitialBehavior)
101}
102
103func TestManagerUpdate_RejectsNilAndInvalidMutations(t *testing.T) {
104	mgr := newWatchedTestManager(t, t.TempDir())
105	before := mgr.Get().Workspace.Styling.ModeLegendDelayMs
106
107	require.Error(t, mgr.Update(nil))
108	require.Error(t, mgr.Update(func(cfg *Config) { cfg.Workspace.Styling.ModeLegendDelayMs = -1 }))
109	require.Equal(t, before, mgr.Get().Workspace.Styling.ModeLegendDelayMs)
110}
111
112func TestManagerUpdate_ConcurrentUpdatesAreAllPersisted(t *testing.T) {
113	configHome := t.TempDir()
114	mgr := newWatchedTestManager(t, configHome)
115
116	mutations := []func(*Config){
117		func(cfg *Config) { cfg.Workspace.Styling.ModeLegendCollapsed = true },
118		func(cfg *Config) { cfg.Omnibox.InitialBehavior = entity.OmniboxInitialBehaviorNone },
119		func(cfg *Config) { cfg.Workspace.Styling.ModeLegendDelayMs = 1234 },
120		func(cfg *Config) { cfg.Workspace.Styling.TransitionDuration = 321 },
121		func(cfg *Config) { cfg.Workspace.Styling.ModeBorderWidth = 7 },
122		func(cfg *Config) { cfg.Workspace.Styling.ModeLegendLinger = false },
123	}
124
125	start := make(chan struct{})
126	errs := make(chan error, len(mutations))
127	for _, mutate := range mutations {
128		go func() {
129			<-start
130			errs <- mgr.Update(mutate)
131		}()
132	}
133	close(start)
134	for range mutations {
135		require.NoError(t, <-errs)
136	}
137
138	for name, got := range map[string]*Config{
139		"in-memory": mgr.Get(),
140		"on-disk":   newTestOmniboxPreferencesGatewayManager(t, configHome).Get(),
141	} {
142		require.True(t, got.Workspace.Styling.ModeLegendCollapsed, name)
143		require.Equal(t, entity.OmniboxInitialBehaviorNone, got.Omnibox.InitialBehavior, name)
144		require.Equal(t, 1234, got.Workspace.Styling.ModeLegendDelayMs, name)
145		require.Equal(t, 321, got.Workspace.Styling.TransitionDuration, name)
146		require.Equal(t, 7, got.Workspace.Styling.ModeBorderWidth, name)
147		require.False(t, got.Workspace.Styling.ModeLegendLinger, name)
148	}
149}

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