v0.32.0 0617278

css_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close