1package theme
2
3import (
4 "regexp"
5 "strings"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9)
10
11func TestGenerateCSS_StackedTitlebarUsesReadableControlText(t *testing.T) {
12 tests := []struct {
13 name string
14 background string
15 text string
16 want string
17 }{
18 {"dark background", "#775f79", "#3d303f", "#ffffff"},
19 {"light background", "#f0f0f0", "#dddddd", "#000000"},
20 {"readable configured text", "#282828", "#eeeeee", "#eeeeee"},
21 }
22 for _, tt := range tests {
23 t.Run(tt.name, func(t *testing.T) {
24 palette := DefaultLightPalette()
25 palette.SurfaceVariant = tt.background
26 palette.Text = tt.text
27 css := GenerateCSS(palette)
28
29 assert.Contains(t, css, "--control-text: "+tt.want+";")
30 for _, selector := range []string{
31 ".stacked-pane-titlebar",
32 ".stacked-pane-titlebar label",
33 ".stacked-pane-titlebar image",
34 } {
35 block := regexp.MustCompile(regexp.QuoteMeta(selector) + `\s*\{[^}]*\}`).FindString(css)
36 assert.Contains(t, block, "color: var(--control-text);", selector)
37 }
38
39 // Hover and active states must not invalidate the computed contrast.
40 stackedCSS := generateStackedPaneCSS(palette)
41 assert.NotContains(t, stackedCSS, "shade(var(--surface-variant)")
42 hover := regexp.MustCompile(`\.stacked-pane-titlebar\.stacked-pane-title-clickable:hover label\s*\{[^}]*\}`).FindString(css)
43 assert.Contains(t, hover, "text-decoration-line: underline;")
44 assert.NotContains(t, hover, "color:")
45 assert.Contains(t, stackedCSS, "border-left: 0.1875em solid var(--accent);")
46 })
47 }
48}
49
50func TestGenerateCSS_UsesGTKFontForNativeWidgets(t *testing.T) {
51 css := GenerateCSSWithScaleAndFonts(DefaultDarkPalette(), 1.0, FontConfig{
52 SansFont: "Fira Sans",
53 MonospaceFont: "Fira Code",
54 GtkFont: "Adwaita Sans",
55 })
56
57 assert.Contains(t, css, `--font-sans: "Fira Sans", sans-serif;`)
58 assert.Contains(t, css, `--font-mono: "Fira Code", monospace;`)
59 assert.Contains(t, css, `--font-gtk: "Adwaita Sans", sans-serif;`)
60 assert.Contains(t, css, "window, tooltip, popover {\n\tfont-family: var(--font-gtk);\n}")
61 assert.NotContains(t, css, "window, tooltip, popover {\n\tfont-family: var(--font-sans);\n}")
62}
63
64func TestGenerateCSS_StandaloneOmniboxWindowUsesTransparentBackground(t *testing.T) {
65 css := GenerateCSS(DefaultDarkPalette())
66
67 if !strings.Contains(css, "window.standalone-omnibox-window") {
68 t.Fatalf("expected standalone omnibox window selector in CSS, got %q", css)
69 }
70 re := regexp.MustCompile(`window\.standalone-omnibox-window\s*\{\s*background-color:\s*transparent\s*;?\s*\}`)
71 if !re.MatchString(css) {
72 t.Fatalf("expected standalone omnibox window background to be transparent")
73 }
74}
75
76func TestGenerateCSSFullWithTiming_VimModePulseUsesConfiguredTransitionDuration(t *testing.T) {
77 // Vim Mode overlay pulse timings scale from the base transition duration:
78 // normal pulse = 3× (150ms -> 450ms), fast pulse = 6× (150ms -> 900ms).
79 css := GenerateCSSFullWithTiming(DefaultDarkPalette(), 1.0, DefaultFontConfig(), DefaultModeColors(), 150)
80
81 assert.Contains(t, css, "animation: vim-mode-overlay-pulse-anim-a 450ms ease-in-out;")
82 assert.Contains(t, css, "animation: vim-mode-overlay-pulse-fast-anim-a 900ms ease-in-out;")
83 assert.NotContains(t, css, "vim-mode-indicator")
84}
85
86func TestGenerateCSS_OmniboxHeaderIsOpaque(t *testing.T) {
87 css := GenerateCSS(DefaultDarkPalette())
88
89 if !strings.Contains(css, ".omnibox-header {") {
90 t.Fatalf("expected omnibox header selector in CSS")
91 }
92 if !strings.Contains(css, "background-color: var(--surface);") {
93 t.Fatalf("expected omnibox header to use an opaque surface background")
94 }
95 if !strings.Contains(css, "background-image: none;") {
96 t.Fatalf("expected omnibox header gradient to be disabled")
97 }
98}
99
100func TestGenerateCSS_OmniboxHeaderBadgeSelectorsExist(t *testing.T) {
101 css := GenerateCSS(DefaultDarkPalette())
102
103 if !strings.Contains(css, ".omnibox-header-badge {") {
104 t.Fatalf("expected omnibox header badge selector in CSS")
105 }
106 if !strings.Contains(css, ".omnibox-header-badge:hover {") {
107 t.Fatalf("expected omnibox header badge hover selector in CSS")
108 }
109}
110
111func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.T) {
112 css := GenerateCSS(DefaultDarkPalette())
113
114 badgeRe := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*background-color:\s*alpha\(var\(--surface-variant\),\s*0\.65\);[^}]*color:\s*var\(--muted\);[^}]*border:\s*0\.0625em solid alpha\(var\(--border\),\s*0\.85\);[^}]*border-radius:\s*0\.1875em;[^}]*padding:\s*0\.0625em 0\.375em;[^}]*font-size:\s*0\.6875em;`)
115 if !badgeRe.MatchString(css) {
116 t.Fatalf("expected omnibox header badge to use passive neutral chip styling")
117 }
118
119 hoverRe := regexp.MustCompile(`(?s)\.omnibox-header-badge:hover\s*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);[^}]*border-color:\s*var\(--border\);[^}]*color:\s*var\(--text\);`)
120 if !hoverRe.MatchString(css) {
121 t.Fatalf("expected omnibox header badge hover styling to stay neutral while hinting interactivity")
122 }
123
124 badgeBlock := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*\}`).FindString(css)
125 if strings.Contains(badgeBlock, "background-color: alpha(var(--accent)") {
126 t.Fatalf("expected omnibox header badge resting state to avoid accent-based styling")
127 }
128 if strings.Contains(badgeBlock, "color: var(--accent)") {
129 t.Fatalf("expected omnibox header badge resting text to avoid accent color")
130 }
131}
132
133func TestGenerateCSS_FavoriteRowsOnlyTintTrailingAffordance(t *testing.T) {
134 css := GenerateCSS(DefaultDarkPalette())
135
136 if !strings.Contains(css, ".omnibox-row.omnibox-row-favorite {") {
137 t.Fatalf("expected favorite row selector in CSS")
138 }
139 if !strings.Contains(css, "border-left: 0.1875em solid var(--warning);") {
140 t.Fatalf("expected favorite rows to mark the left edge with the warning color")
141 }
142 if strings.Contains(css, "border-right: 0.125em solid alpha(var(--warning), 0.55);") {
143 t.Fatalf("expected favorite indicator to move off the right edge")
144 }
145 if !strings.Contains(css, "color: mix(var(--warning), var(--muted), 0.45);") {
146 t.Fatalf("expected favorite star color to be softened")
147 }
148 if strings.Contains(css, ".omnibox-row.omnibox-row-favorite .omnibox-favorite-star-slot") {
149 t.Fatalf("expected favorite highlight to move off the star slot")
150 }
151}
152
153func TestGenerateCSS_OmniboxSearchEntryUsesControlSurface(t *testing.T) {
154 css := GenerateCSS(DefaultDarkPalette())
155
156 containerRe := regexp.MustCompile(`(?s)\.omnibox-container\s*\{[^}]*background-color:\s*var\(--surface\);`)
157 if !containerRe.MatchString(css) {
158 t.Fatalf("expected omnibox container to use the header surface color")
159 }
160 entryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry\s*,\s*entry\.omnibox-entry\s*>\s*text\s*\{[^}]*background-color:\s*var\(--surface-variant\);[^}]*background-image:\s*none;[^}]*color:\s*var\(--control-text\);`)
161 if !entryRe.MatchString(css) {
162 t.Fatalf("expected omnibox entry and its text node to use the control surface fill and readable text")
163 }
164 ghostBlock := cssRuleBlock(t, css, ".omnibox-ghost")
165 assert.Contains(t, ghostBlock, "color: var(--control-text);")
166 assert.NotContains(t, ghostBlock, "alpha(var(--control-text)")
167 focusedEntryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry:focus[^\{]*,\s*entry\.omnibox-entry:focus-within[^\{]*,\s*entry\.omnibox-entry:focus-visible[^\{]*,\s*entry\.omnibox-entry:focus\s*>\s*text[^\{]*,\s*entry\.omnibox-entry:focus-within\s*>\s*text[^\{]*,\s*entry\.omnibox-entry:focus-visible\s*>\s*text\s*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);`)
168 if !focusedEntryRe.MatchString(css) {
169 t.Fatalf("expected focused omnibox entry text node to keep a distinct control-surface fill")
170 }
171 bangActiveRe := regexp.MustCompile(`(?s)entry\.omnibox-entry\.omnibox-entry-bang-active[^\{]*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);`)
172 if !bangActiveRe.MatchString(css) {
173 t.Fatalf("expected bang-active omnibox entry to keep a distinct control-surface fill")
174 }
175 if strings.Contains(css, "background-color: alpha(var(--bg), 0.88);") || strings.Contains(css, "background-color: shade(var(--bg), 1.05);") {
176 t.Fatalf("expected omnibox entry fills not to derive from the page background")
177 }
178 scrolledRe := regexp.MustCompile(`(?s)\.omnibox-scrolled\s*\{[^}]*background-color:\s*var\(--surface\);`)
179 if !scrolledRe.MatchString(css) {
180 t.Fatalf("expected omnibox results area to use the header surface color")
181 }
182}
183
184func TestGenerateCSS_ContextMenuUsesCompactSingleFrameStyling(t *testing.T) {
185 css := GenerateCSS(DefaultDarkPalette())
186
187 assert.Contains(t, css, "popover.context-menu-popover > contents {")
188 assert.Contains(t, css, ".context-menu {")
189 assert.Contains(t, css, ".context-menu button.context-menu-item {")
190 assert.Contains(t, css, ".context-menu button.context-menu-item:hover,")
191 assert.Contains(t, css, ".context-menu button.context-menu-item:disabled {")
192 assert.Contains(t, css, ".context-menu-separator {")
193
194 popoverBlockRe := regexp.MustCompile(`(?s)popover\.context-menu-popover\s*>\s*contents\s*\{[^}]*\}`)
195 popoverBlock := popoverBlockRe.FindString(css)
196 if popoverBlock == "" {
197 t.Fatal("expected context menu popover contents block in CSS")
198 }
199
200 assert.Contains(t, popoverBlock, "background-color: var(--surface);")
201 assert.Contains(t, popoverBlock, "border: 0.0625em solid alpha(var(--border), 0.85);")
202 assert.Contains(t, popoverBlock, "border-radius: 0.25em;")
203 assert.Contains(t, popoverBlock, "box-shadow:")
204
205 blockRe := regexp.MustCompile(`(?s)\.context-menu\s*\{[^}]*\}`)
206 block := blockRe.FindString(css)
207 if block == "" {
208 t.Fatal("expected context menu block in CSS")
209 }
210
211 assert.NotContains(t, block, "background-color:")
212 assert.Contains(t, block, "padding: 0.125em 0;")
213 assert.Contains(t, block, "min-width: 10.5em;")
214 assert.NotContains(t, block, "border:")
215 assert.NotContains(t, block, "border-radius:")
216 assert.NotContains(t, block, "box-shadow:")
217 assert.NotContains(t, block, "#")
218 assert.NotContains(t, block, "black")
219 assert.NotContains(t, block, "white")
220
221 itemBlockRe := regexp.MustCompile(`(?s)\.context-menu button\.context-menu-item\s*\{[^}]*\}`)
222 itemBlock := itemBlockRe.FindString(css)
223 if itemBlock == "" {
224 t.Fatal("expected context menu item block in CSS")
225 }
226
227 assert.Contains(t, itemBlock, "padding: 0.25em 0.75em;")
228 assert.Contains(t, itemBlock, "min-height: 1.75em;")
229}
230
231func TestGenerateCSSWithScale_DoesNotEmitRootTextScaling(t *testing.T) {
232 css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3)
233
234 assert.NotContains(t, css, "/* UI Scaling (130%) */")
235 assert.NotContains(t, css, "window {\n\tfont-size:")
236}
237
238func TestGenerateCSSWithScale_DoesNotEmitLineHeightOverrides(t *testing.T) {
239 css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3)
240
241 assert.NotContains(t, css, "line-height:")
242 assert.NotContains(t, css, "label,\nentry,\nentry > text {\n\tline-height:")
243}
244
245func TestGenerateCSSWithScale_UsesOpaqueOmniboxRowBaseBackground(t *testing.T) {
246 css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3)
247
248 assert.Contains(t, css, ".omnibox-row {")
249 assert.Contains(t, css, "background-color: var(--surface);")
250 assert.NotContains(t, css, ".omnibox-row {\n\tpadding: 0.5em 0.75em;\n\tmargin: 0;\n\tborder-radius: 0;\n\tborder-left: 0.1875em solid transparent;\n\tborder-bottom: 0.0625em solid alpha(var(--border), 0.35);\n\tbackground-color: transparent;")
251}
252
253func TestGenerateCSSWithScale_UsesMediumWeightForOmniboxSuggestionTitle(t *testing.T) {
254 css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3)
255
256 assert.Contains(t, css, ".omnibox-suggestion-title {")
257 assert.Contains(t, css, "font-weight: 500;")
258 assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;")
259}
260
261func TestGenerateCSS_ContentAreaTabBarInset(t *testing.T) {
262 css := GenerateCSS(DefaultDarkPalette())
263
264 topBlock := cssRuleBlock(t, css, ".content-area-tabbar-inset-top")
265 if !strings.Contains(topBlock, "padding-top: 2em;") {
266 t.Fatal("expected padding-top: 2em in .content-area-tabbar-inset-top block")
267 }
268 bottomBlock := cssRuleBlock(t, css, ".content-area-tabbar-inset-bottom")
269 if !strings.Contains(bottomBlock, "padding-bottom: 2em;") {
270 t.Fatal("expected padding-bottom: 2em in .content-area-tabbar-inset-bottom block")
271 }
272 if !strings.Contains(css, ".tab-bar") {
273 t.Fatal("expected .tab-bar selector in generated CSS")
274 }
275 if !strings.Contains(css, "min-height: 2em;") {
276 t.Fatal("expected min-height: 2em in .tab-bar block")
277 }
278}
279
280func cssRuleBlock(t *testing.T, css, selector string) string {
281 t.Helper()
282 re := regexp.MustCompile(regexp.QuoteMeta(selector) + `\s*\{([^}]*)\}`)
283 matches := re.FindStringSubmatch(css)
284 if len(matches) != 2 {
285 t.Fatalf("expected %s selector in generated CSS", selector)
286 }
287 return matches[1]
288}
289
290func TestGenerateCSS_TouchpadNavigationIndicatorFadesWholeOverlay(t *testing.T) {
291 css := GenerateCSS(DefaultDarkPalette())
292
293 baseBlock := cssRuleBlock(t, css, ".touchpad-navigation-indicator")
294 assert.Contains(t, baseBlock, "opacity: 1;")
295 assert.Contains(t, baseBlock, "filter: blur(0px);")
296 assert.Contains(t, baseBlock, "backdrop-filter: blur(0.75em);")
297 assert.Contains(t, baseBlock, "transition: opacity 220ms ease-out,")
298 assert.Contains(t, baseBlock, "filter 220ms ease-out,")
299 assert.Contains(t, baseBlock, "backdrop-filter 220ms ease-out,")
300 assert.Contains(t, baseBlock, "box-shadow 220ms ease-out;")
301
302 hidingBlock := cssRuleBlock(t, css, ".touchpad-navigation-indicator.hiding")
303 assert.Contains(t, hidingBlock, "opacity: 0;")
304 assert.Contains(t, hidingBlock, "filter: blur(0.0625em);")
305 assert.Contains(t, hidingBlock, "box-shadow:")
306}
307
308func TestGenerateCSS_DoesNotEmitUnsupportedGTKProperties(t *testing.T) {
309 css := GenerateCSS(DefaultDarkPalette())
310
311 assert.NotContains(t, css, "pointer-events:")
312 assert.NotContains(t, css, "text-align:")
313}
314
315func TestGenerateCSS_SessionContainerHasNoMinWidth(t *testing.T) {
316 css := GenerateCSS(DefaultDarkPalette())
317 block := cssRuleBlock(t, css, ".session-manager-container")
318 assert.NotContains(t, block, "min-width:")
319 assert.Contains(t, block, "background-color: var(--surface-variant);")
320}