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}