v0.32.0 0617278

history_sidebar_css_test.go RawHistoryFolder

1package theme
2
3import (
4	"crypto/sha256"
5	"strings"
6	"testing"
7
8	"github.com/stretchr/testify/assert"
9)
10
11func TestGenerateHistorySidebarCSS_ContainsExpectedSelectors(t *testing.T) {
12	css := generateHistorySidebarCSS(DefaultDarkPalette())
13
14	selectors := []string{
15		".history-sidebar-group-header",
16		".history-sidebar-row-time",
17		".history-sidebar-loading",
18	}
19
20	for _, sel := range selectors {
21		assert.Contains(t, css, sel+" {",
22			"expected selector %s { in generated CSS", sel)
23	}
24}
25
26func TestGenerateSidebarCSS_AccentAlphaInterpolation(t *testing.T) {
27	css := generateSidebarCSS(DefaultDarkPalette())
28
29	expectedAlpha := "alpha(var(--accent), 0.18)"
30	assert.Contains(t, css, expectedAlpha,
31		"expected accent alpha variable usage %q in generated CSS", expectedAlpha)
32	assert.NotContains(t, css, "alpha(#",
33		"history sidebar CSS should not inline hardcoded accent hex values")
34
35	// The accent alpha should appear in hover, selected, and focus blocks.
36	assert.GreaterOrEqual(t, strings.Count(css, expectedAlpha), 3,
37		"expected accent alpha to appear at least 3 times (hover/selected/focus)")
38}
39
40func TestGenerateHistorySidebarCSS_DeterministicOutput(t *testing.T) {
41	palette := DefaultDarkPalette()
42
43	css1 := generateHistorySidebarCSS(palette)
44	css2 := generateHistorySidebarCSS(palette)
45
46	hash1 := sha256.Sum256([]byte(css1))
47	hash2 := sha256.Sum256([]byte(css2))
48
49	assert.Equal(t, hash1, hash2, "CSS output must be deterministic for the same palette")
50}
51
52func TestGenerateSidebarCSS_UsesPaletteVariablesInsteadOfInliningColors(t *testing.T) {
53	darkCSS := generateSidebarCSS(DefaultDarkPalette())
54	lightCSS := generateSidebarCSS(DefaultLightPalette())
55
56	assert.Equal(t, darkCSS, lightCSS,
57		"history sidebar fragment should be palette-independent when it relies on CSS variables")
58	assert.Contains(t, darkCSS, "alpha(var(--accent), 0.18)")
59	assert.NotContains(t, darkCSS, "alpha(#")
60}
61
62func TestGenerateHistorySidebarCSS_ThroughGenerateCSS(t *testing.T) {
63	fullCSS := GenerateCSS(DefaultDarkPalette())
64
65	assert.Contains(t, fullCSS, ".sidebar-outer {",
66		"full CSS must contain shared sidebar selectors")
67	assert.Contains(t, fullCSS, ".history-sidebar-group-header {",
68		"full CSS must contain group header selector")
69	assert.Contains(t, fullCSS, ".sidebar-row-title {",
70		"full CSS must contain shared row title selector")
71	assert.Contains(t, fullCSS, "History Sidebar Styling",
72		"full CSS must contain the history sidebar comment marker")
73}
74
75func TestGenerateHistorySidebarCSS_NoEmptyCSSBlocks(t *testing.T) {
76	css := generateHistorySidebarCSS(DefaultDarkPalette())
77
78	sections := strings.Split(css, "{")
79	for i, section := range sections {
80		if i == 0 {
81			continue
82		}
83		before, _, ok := strings.Cut(section, "}")
84		if !ok {
85			continue
86		}
87		blockContent := strings.TrimSpace(before)
88		assert.NotEmpty(t, blockContent, "CSS block must not be empty: section %d", i)
89	}
90}
91
92func TestGenerateSidebarCSS_CustomPaletteValuesInterpolated(t *testing.T) {
93	customPalette := Palette{
94		Background:     "#111111",
95		Surface:        "#222222",
96		SurfaceVariant: "#333333",
97		Text:           "#eeeeee",
98		Muted:          "#aaaaaa",
99		Accent:         "#ff6600",
100		Border:         "#444444",
101		Success:        "#00cc44",
102		Warning:        "#ffaa00",
103		Destructive:    "#cc2222",
104	}
105
106	css := generateSidebarCSS(customPalette)
107
108	assert.Contains(t, css, "alpha(var(--accent), 0.18)",
109		"sidebar CSS should use the shared accent variable")
110	assert.NotContains(t, css, "#ff6600",
111		"sidebar fragment should not inline palette-specific accent values")
112
113	assert.Contains(t, css, ".sidebar-outer {")
114	assert.Contains(t, css, ".sidebar-row-title {")
115	assert.Contains(t, css, ".sidebar-row-subtitle {")
116	assert.Contains(t, css, ".sidebar-empty {")
117}
118
119func TestGenerateSidebarCSS_UsesCompactBalancedSearchChrome(t *testing.T) {
120	css := generateSidebarCSS(DefaultDarkPalette())
121
122	assert.Contains(t, css, ".sidebar-search-box {\n\tpadding: 0.5em 0.5em 0.375em;")
123	assert.Contains(t, css, ".sidebar-search {\n\tmin-height: 0;\n\tpadding: 0.125em 0.375em;")
124}
125
126func TestGenerateSidebarCSS_ContainsTransition(t *testing.T) {
127	css := generateSidebarCSS(DefaultDarkPalette())
128
129	assert.Contains(t, css, "transition:",
130		"CSS should include transition property for smooth hover effects")
131	assert.Contains(t, css, "background-color 100ms ease",
132		"row transition should specify background-color 100ms ease")
133}
134
135func TestGenerateHistorySidebarCSS_ContainsUppercaseGroupHeader(t *testing.T) {
136	css := generateHistorySidebarCSS(DefaultDarkPalette())
137
138	assert.Contains(t, css, "text-transform: uppercase;",
139		"group header should use text-transform uppercase")
140	assert.Contains(t, css, "letter-spacing: 0.04em;",
141		"group header should have letter-spacing")
142}
143
144func TestGenerateHistorySidebarCSS_InGenerateCSSFull_DarkPalette(t *testing.T) {
145	fullCSS := GenerateCSSFull(DefaultDarkPalette(), 1.0, DefaultFontConfig(), DefaultModeColors())
146
147	assert.Contains(t, fullCSS, ".sidebar-outer {",
148		"GenerateCSSFull must include shared sidebar outer selector for dark palette")
149	assert.Contains(t, fullCSS, ".sidebar-row-title {",
150		"GenerateCSSFull must include shared sidebar row title selector for dark palette")
151	assert.Contains(t, fullCSS, ".sidebar-search {",
152		"GenerateCSSFull must include shared sidebar search selector for dark palette")
153	assert.Contains(t, fullCSS, ".sidebar-empty {",
154		"GenerateCSSFull must include shared sidebar empty selector for dark palette")
155}
156
157func TestGenerateHistorySidebarCSS_InGenerateCSSFull_LightPalette(t *testing.T) {
158	fullCSS := GenerateCSSFull(DefaultLightPalette(), 1.0, DefaultFontConfig(), DefaultModeColors())
159
160	assert.Contains(t, fullCSS, ".sidebar-outer {",
161		"GenerateCSSFull must include shared sidebar outer selector for light palette")
162	assert.Contains(t, fullCSS, ".sidebar-row-title {",
163		"GenerateCSSFull must include shared sidebar row title selector for light palette")
164	assert.Contains(t, fullCSS, ".sidebar-search {",
165		"GenerateCSSFull must include shared sidebar search selector for light palette")
166	assert.Contains(t, fullCSS, ".sidebar-empty {",
167		"GenerateCSSFull must include shared sidebar empty selector for light palette")
168}
169
170func TestGenerateHistorySidebarCSS_LiveReloadSeamIncludesUpdatedCSS(t *testing.T) {
171	// GenerateCSSFull is the live theme reload entry point called when
172	// the palette changes at runtime. This test proves that switching
173	// palettes produces different history sidebar CSS, confirming the
174	// full generation path includes and re-generates the sidebar styles.
175	darkCSS := GenerateCSSFull(DefaultDarkPalette(), 1.0, DefaultFontConfig(), DefaultModeColors())
176	lightCSS := GenerateCSSFull(DefaultLightPalette(), 1.0, DefaultFontConfig(), DefaultModeColors())
177
178	// Both must contain history sidebar selectors.
179	assert.Contains(t, darkCSS, ".sidebar-outer {", "dark CSS must have shared sidebar styles")
180	assert.Contains(t, lightCSS, ".sidebar-outer {", "light CSS must have shared sidebar styles")
181
182	// The CSS must differ between palettes (different color values).
183	if darkCSS == lightCSS {
184		t.Fatal("dark and light palette should produce different CSS when history sidebar is included")
185	}
186
187	// The dark and light outputs must both be parseable: each '{' has '}'.
188	assert.Equal(t, strings.Count(darkCSS, "{"), strings.Count(darkCSS, "}"),
189		"dark CSS braces must be balanced")
190	assert.Equal(t, strings.Count(lightCSS, "{"), strings.Count(lightCSS, "}"),
191		"light CSS braces must be balanced")
192}
193
194func TestGenerateHistorySidebarCSS_InGenerateCSSFull_WithCustomScaleAndFonts(t *testing.T) {
195	customFonts := FontConfig{
196		SansFont:      "Noto Sans",
197		MonospaceFont: "JetBrains Mono",
198		GtkFont:       "Noto Sans",
199	}
200	customModeColors := ModeColors{
201		PaneMode:    "#ff0000",
202		TabMode:     "#00ff00",
203		SessionMode: "#0000ff",
204		ResizeMode:  "#ffff00",
205	}
206
207	fullCSS := GenerateCSSFull(DefaultDarkPalette(), 2.0, customFonts, customModeColors)
208
209	assert.Contains(t, fullCSS, ".sidebar-outer {",
210		"GenerateCSSFull with custom scale/fonts must include shared sidebar selectors")
211	assert.Contains(t, fullCSS, ".sidebar-row:hover {",
212		"GenerateCSSFull must include hover state for shared sidebar rows")
213	assert.Contains(t, fullCSS, ".sidebar-row:selected {",
214		"GenerateCSSFull must include selected state for shared sidebar rows")
215	assert.Contains(t, fullCSS, ".sidebar-row:focus {",
216		"GenerateCSSFull must include focus state for shared sidebar rows")
217}
218
219// TestGenerateHistorySidebarCSS_LiveReloadFullPath verifies that the
220// GenerateCSSFull entry point (used by live theme reload) includes the
221// history sidebar CSS with correct palette values. This is the path that
222// would be called when the user changes themes at runtime.
223func TestGenerateHistorySidebarCSS_LiveReloadFullPath(t *testing.T) {
224	darkPalette := DefaultDarkPalette()
225	lightPalette := DefaultLightPalette()
226	customPalette := Palette{
227		Background:     "#111111",
228		Surface:        "#222222",
229		SurfaceVariant: "#333333",
230		Text:           "#eeeeee",
231		Muted:          "#aaaaaa",
232		Accent:         "#ff6600",
233		Border:         "#444444",
234		Success:        "#00cc44",
235		Warning:        "#ffaa00",
236		Destructive:    "#cc2222",
237	}
238
239	// GenerateCSSFull is the exact function called by the live theme reload
240	// path. It generates ALL CSS including the history sidebar selectors.
241	darkCSS := GenerateCSSFull(darkPalette, 1.0, DefaultFontConfig(), DefaultModeColors())
242	lightCSS := GenerateCSSFull(lightPalette, 1.0, DefaultFontConfig(), DefaultModeColors())
243	customCSS := GenerateCSSFull(customPalette, 1.0, DefaultFontConfig(), DefaultModeColors())
244
245	// All three must contain the shared foundation and history-specific rules.
246	sidebarSelectors := []string{
247		".sidebar-outer",
248		".sidebar-search-box",
249		".sidebar-search",
250		".sidebar-list",
251		".sidebar-row",
252		".sidebar-row:hover",
253		".sidebar-row:selected",
254		".sidebar-row:focus",
255		".sidebar-row-title",
256		".sidebar-row-subtitle",
257		".sidebar-empty",
258		".history-sidebar-group-header",
259		".history-sidebar-row-time",
260		".history-sidebar-loading",
261	}
262
263	for _, sel := range sidebarSelectors {
264		assert.Contains(t, darkCSS, sel+" {", "dark CSS must have selector %s", sel)
265		assert.Contains(t, lightCSS, sel+" {", "light CSS must have selector %s", sel)
266		assert.Contains(t, customCSS, sel+" {", "custom CSS must have selector %s", sel)
267	}
268
269	// Palette-specific values must differ.
270	assert.NotEqual(t, darkCSS, lightCSS, "dark and light GenerateCSSFull output must differ")
271	assert.NotEqual(t, darkCSS, customCSS, "dark and custom GenerateCSSFull output must differ")
272	assert.NotEqual(t, lightCSS, customCSS, "light and custom GenerateCSSFull output must differ")
273
274	// Sidebar CSS should always use the accent variable, while the :root block
275	// carries the palette-specific value that changes on live reload.
276	assert.Contains(t, darkCSS, "alpha(var(--accent), 0.18)")
277	assert.Contains(t, lightCSS, "alpha(var(--accent), 0.18)")
278	assert.Contains(t, customCSS, "alpha(var(--accent), 0.18)")
279	assert.Contains(t, darkCSS, "\n  --accent: "+darkPalette.Accent+";\n")
280	assert.Contains(t, lightCSS, "\n  --accent: "+lightPalette.Accent+";\n")
281	assert.Contains(t, customCSS, "\n  --accent: "+customPalette.Accent+";\n")
282
283	// All three outputs must be syntactically valid: every '{' has a matching '}'.
284	for name, css := range map[string]string{
285		"dark":   darkCSS,
286		"light":  lightCSS,
287		"custom": customCSS,
288	} {
289		assert.Equal(t, strings.Count(css, "{"), strings.Count(css, "}"),
290			"%s CSS braces must be balanced", name)
291	}
292}
293
294// TestGenerateHistorySidebarCSS_LiveReloadPaletteSwitch verifies that
295// switching palettes at GenerateCSSFull level updates the history sidebar
296// accent values. This simulates what happens when the theme system applies
297// a new palette at runtime: the full CSS (including sidebar) is regenerated.
298func TestGenerateHistorySidebarCSS_LiveReloadPaletteSwitch(t *testing.T) {
299	// Start with dark palette.
300	current := DefaultDarkPalette()
301	cssBefore := GenerateCSSFull(current, 1.0, DefaultFontConfig(), DefaultModeColors())
302
303	// Verify the sidebar section uses the shared accent variable.
304	assert.Contains(t, cssBefore, "alpha(var(--accent), 0.18)",
305		"initial palette should render sidebar CSS through the accent variable")
306	assert.Contains(t, cssBefore, "\n  --accent: "+current.Accent+";\n")
307
308	// Switch to light palette (simulating runtime theme change).
309	current = DefaultLightPalette()
310	cssAfter := GenerateCSSFull(current, 1.0, DefaultFontConfig(), DefaultModeColors())
311
312	assert.Contains(t, cssAfter, "alpha(var(--accent), 0.18)",
313		"updated palette should still use the shared accent variable")
314	assert.Contains(t, cssAfter, "\n  --accent: "+current.Accent+";\n")
315
316	// The old palette variable definition must not remain after switching.
317	darkAccent := DefaultDarkPalette().Accent
318	if current.Accent != darkAccent {
319		assert.NotContains(t, cssAfter, "\n  --accent: "+darkAccent+";\n")
320	}
321
322	// Shared sidebar selectors must be present in both.
323	assert.Contains(t, cssBefore, ".sidebar-row:selected {")
324	assert.Contains(t, cssAfter, ".sidebar-row:selected {")
325
326	// The before and after CSS must be different (palette changed).
327	assert.NotEqual(t, cssBefore, cssAfter, "CSS must change when palette switches")
328}

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