v0.32.0 0617278

manager_test.go RawHistoryFolder

1package theme
2
3import (
4	"context"
5	"testing"
6
7	"github.com/bnema/dumber/internal/domain/entity"
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/require"
10)
11
12func resolvedThemeFixture(prefersDark bool) entity.ResolvedTheme {
13	light := entity.ColorPalette{
14		Background:     "#ffffff",
15		Surface:        "#f8f8f8",
16		SurfaceVariant: "#eeeeee",
17		Text:           "#111111",
18		Muted:          "#666666",
19		Accent:         "#0055ff",
20		Border:         "#dddddd",
21	}
22	dark := entity.ColorPalette{
23		Background:     "#101010",
24		Surface:        "#181818",
25		SurfaceVariant: "#242424",
26		Text:           "#f5f5f5",
27		Muted:          "#999999",
28		Accent:         "#66aaff",
29		Border:         "#333333",
30	}
31	active := light
32	if prefersDark {
33		active = dark
34	}
35	return entity.ResolvedTheme{
36		LightPalette:      light,
37		DarkPalette:       dark,
38		ActivePalette:     active,
39		PrefersDark:       prefersDark,
40		ColorSchemeSource: "test",
41		ThemeSource:       entity.ThemeSourceMetadata{Kind: entity.ThemeSourceConfig},
42		Fonts: entity.ThemeFonts{
43			SansFont:      "Inter",
44			SerifFont:     "Georgia",
45			MonospaceFont: "JetBrains Mono",
46			GtkFont:       "Adwaita Sans",
47			DefaultSize:   16,
48		},
49		UIScale: 1.5,
50		ModeColors: entity.ThemeModeColors{
51			PaneMode:    "#ff0000",
52			TabMode:     "#00ff00",
53			SessionMode: "#0000ff",
54			ResizeMode:  "#ffff00",
55		},
56	}
57}
58
59func TestNewManager_UsesResolvedThemeFields(t *testing.T) {
60	ctx := context.Background()
61	resolved := resolvedThemeFixture(true)
62
63	manager := NewManager(ctx, resolved)
64
65	require.NotNil(t, manager)
66	assert.True(t, manager.PrefersDark())
67	assert.Equal(t, "#ffffff", manager.GetLightPalette().Background)
68	assert.Equal(t, "#101010", manager.GetDarkPalette().Background)
69	assert.Equal(t, manager.GetDarkPalette(), manager.GetCurrentPalette())
70	assert.Equal(t, "#ff0000", manager.GetModeColors().PaneMode)
71	assert.Equal(t, FontConfig{SansFont: "Inter", MonospaceFont: "JetBrains Mono", GtkFont: "Adwaita Sans"}, manager.fonts)
72	assert.Equal(t, "Adwaita Sans", manager.gtkFont)
73	assert.InDelta(t, 1.5, manager.uiScale, 0.000001)
74}
75
76func TestNewManager_DefaultsInvalidScaleToOne(t *testing.T) {
77	ctx := context.Background()
78	resolved := resolvedThemeFixture(true)
79	resolved.UIScale = 0
80
81	manager := NewManager(ctx, resolved)
82
83	require.NotNil(t, manager)
84	assert.InDelta(t, 1.0, manager.uiScale, 0.000001)
85}
86
87func TestManager_UpdateFromResolvedReplacesResolvedValues(t *testing.T) {
88	ctx := context.Background()
89	manager := NewManager(ctx, resolvedThemeFixture(true))
90	updated := resolvedThemeFixture(false)
91	updated.LightPalette.Background = "#abcdef"
92	updated.DarkPalette.Background = "#123456"
93	updated.Fonts.SansFont = "Recursive"
94	updated.UIScale = 2.0
95	updated.ModeColors.PaneMode = "#654321"
96
97	manager.UpdateFromResolved(ctx, updated, nil)
98
99	assert.False(t, manager.PrefersDark())
100	assert.Equal(t, "#abcdef", manager.GetLightPalette().Background)
101	assert.Equal(t, "#123456", manager.GetDarkPalette().Background)
102	assert.Equal(t, manager.GetLightPalette(), manager.GetCurrentPalette())
103	assert.Equal(t, "Recursive", manager.fonts.SansFont)
104	assert.InDelta(t, 2.0, manager.uiScale, 0.000001)
105	assert.Equal(t, "#654321", manager.GetModeColors().PaneMode)
106}
107
108func TestManager_GetWebUIThemeCSS(t *testing.T) {
109	ctx := context.Background()
110	manager := NewManager(ctx, resolvedThemeFixture(true))
111
112	css := manager.GetWebUIThemeCSS()
113
114	assert.Contains(t, css, ":root{")
115	assert.Contains(t, css, ".dark{")
116	assert.Contains(t, css, "--background: #ffffff")
117	assert.Contains(t, css, "--background: #101010")
118}
119
120func TestManager_GetBackgroundRGBAUsesCurrentPalette(t *testing.T) {
121	ctx := context.Background()
122	manager := NewManager(ctx, resolvedThemeFixture(true))
123
124	r, g, b, a := manager.GetBackgroundRGBA()
125
126	assert.InDelta(t, float32(0x10)/255, r, 0.0001)
127	assert.InDelta(t, float32(0x10)/255, g, 0.0001)
128	assert.InDelta(t, float32(0x10)/255, b, 0.0001)
129	assert.InDelta(t, float32(1), a, 0.0001)
130}
131
132func TestPaletteFromEntityAddsSemanticDefaults(t *testing.T) {
133	palette := PaletteFromEntity(entity.ColorPalette{Background: "#ffffff", Text: "#111111"}, false)
134
135	assert.Equal(t, "#ffffff", palette.Background)
136	assert.Equal(t, "#111111", palette.Text)
137	assert.Equal(t, DefaultLightPalette().Success, palette.Success)
138	assert.Equal(t, DefaultLightPalette().Warning, palette.Warning)
139	assert.Equal(t, DefaultLightPalette().Destructive, palette.Destructive)
140}
141
142func TestModeColorsFromEntity(t *testing.T) {
143	modeColors := ModeColorsFromEntity(entity.ThemeModeColors{
144		PaneMode:    "#111111",
145		TabMode:     "#222222",
146		SessionMode: "#333333",
147		ResizeMode:  "#444444",
148	})
149
150	assert.Equal(t, "#111111", modeColors.PaneMode)
151	assert.Equal(t, "#222222", modeColors.TabMode)
152	assert.Equal(t, "#333333", modeColors.SessionMode)
153	assert.Equal(t, "#444444", modeColors.ResizeMode)
154}
155
156func TestFontConfigFromEntity(t *testing.T) {
157	fonts := FontConfigFromEntity(entity.ThemeFonts{
158		SansFont:      "Inter",
159		MonospaceFont: "JetBrains Mono",
160		GtkFont:       "Adwaita Sans",
161	})
162
163	assert.Equal(t, FontConfig{SansFont: "Inter", MonospaceFont: "JetBrains Mono", GtkFont: "Adwaita Sans"}, fonts)
164}
165
166func TestDefaultGTKFont(t *testing.T) {
167	assert.Equal(t, "Adwaita Sans", DefaultGTKFont())
168}
169
170func TestFormatGTKFontName_UsesScaledConfiguredGTKFont(t *testing.T) {
171	assert.Equal(t, "Adwaita Sans 14", formatGTKFontName("Adwaita Sans", 1.3))
172}
173
174func TestFormatGTKFontName_DefaultsScaleAndRoundsToNearestPoint(t *testing.T) {
175	assert.Equal(t, "Inter 11", formatGTKFontName("Inter", 0))
176	assert.Equal(t, "Inter 12", formatGTKFontName("Inter", 1.05))
177	assert.Equal(t, "Adwaita Sans 11", formatGTKFontName("", 1.0))
178}
179
180func TestShouldApplyGTKFontName(t *testing.T) {
181	assert.True(t, shouldApplyGTKFontName("", "Fira Sans 14"))
182	assert.False(t, shouldApplyGTKFontName("Fira Sans 14", "Fira Sans 14"))
183	assert.False(t, shouldApplyGTKFontName("Fira Sans 14", ""))
184}
185
186func TestShouldSkipThemeReapply(t *testing.T) {
187	const (
188		displayA  = uintptr(0x1000)
189		displayB  = uintptr(0x2000)
190		css       = "window{background:#000;}"
191		otherCSS  = "window{background:#fff;}"
192		font      = "Adwaita Sans 14"
193		otherFont = "Adwaita Sans 17"
194	)
195
196	// Same provider, display, CSS and font: skip the reload and re-add.
197	assert.True(t, shouldSkipThemeReapply(true, displayA, displayA, css, css, font, font),
198		"repeated window creation with an unchanged theme must skip reapply")
199	// No provider yet: full apply.
200	assert.False(t, shouldSkipThemeReapply(false, 0, displayA, "", css, "", font))
201	// Different display: the provider must be added for the new display even
202	// when the CSS is unchanged. Displays are never conflated.
203	assert.False(t, shouldSkipThemeReapply(true, displayA, displayB, css, css, font, font))
204	// Changed CSS (real theme change): reload.
205	assert.False(t, shouldSkipThemeReapply(true, displayA, displayA, css, otherCSS, font, font))
206	// Changed font (scale change): reapply for the font setting.
207	assert.False(t, shouldSkipThemeReapply(true, displayA, displayA, css, css, font, otherFont))
208	// Empty font identity never skips.
209	assert.False(t, shouldSkipThemeReapply(true, displayA, displayA, css, css, "", ""))
210}
211
212func TestEffectiveCSSIdentity_StableForUnchangedTheme(t *testing.T) {
213	ctx := context.Background()
214	first := NewManager(ctx, resolvedThemeFixture(true))
215	second := NewManager(ctx, resolvedThemeFixture(true))
216
217	cssOf := func(m *Manager) string {
218		return GenerateCSSFullWithTiming(m.GetCurrentPalette(), m.uiScale, m.fonts, m.modeColors, m.transitionDurationMs)
219	}
220
221	// The dedup relies on CSS identity: identical resolved themes must
222	// produce identical CSS so repeated window creation skips the reload.
223	assert.Equal(t, cssOf(first), cssOf(second))
224	assert.NotEmpty(t, cssOf(first))
225
226	// A real theme change must change the identity so it is never skipped.
227	changed := resolvedThemeFixture(true)
228	changed.DarkPalette.Background = "#000001"
229	other := NewManager(ctx, changed)
230	assert.NotEqual(t, cssOf(first), cssOf(other))
231}
232
233func TestManager_AppliedFontTrackedPerDisplay(t *testing.T) {
234	ctx := context.Background()
235	m := NewManager(ctx, resolvedThemeFixture(true))
236
237	const (
238		displayA = uintptr(0x1000)
239		displayB = uintptr(0x2000)
240		font     = "Adwaita Sans 14"
241	)
242
243	// No font recorded anywhere initially, including on a nil manager.
244	assert.Empty(t, m.lastAppliedFontFor(displayA))
245	assert.Empty(t, m.lastAppliedFontFor(displayB))
246	assert.Empty(t, (*Manager)(nil).lastAppliedFontFor(displayA))
247
248	// Recording for one display leaves the other untouched so its
249	// GtkSettings still get configured on first apply.
250	m.recordAppliedFontFor(displayA, font)
251	assert.Equal(t, font, m.lastAppliedFontFor(displayA))
252	assert.Empty(t, m.lastAppliedFontFor(displayB))
253
254	m.recordAppliedFontFor(displayB, font)
255	assert.Equal(t, font, m.lastAppliedFontFor(displayB))
256
257	// Updating one display does not disturb the other.
258	m.recordAppliedFontFor(displayA, "Adwaita Sans 17")
259	assert.Equal(t, "Adwaita Sans 17", m.lastAppliedFontFor(displayA))
260	assert.Equal(t, font, m.lastAppliedFontFor(displayB))
261
262	// Recording on a nil manager is a safe no-op.
263	assert.NotPanics(t, func() { (*Manager)(nil).recordAppliedFontFor(displayA, font) })
264}

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