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