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