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}