v0.30.0 eb3068f

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

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