package theme import ( "regexp" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestGenerateCSS_StackedTitlebarUsesReadableControlText(t *testing.T) { tests := []struct { name string background string text string want string }{ {"dark background", "#775f79", "#3d303f", "#ffffff"}, {"light background", "#f0f0f0", "#dddddd", "#000000"}, {"readable configured text", "#282828", "#eeeeee", "#eeeeee"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { palette := DefaultLightPalette() palette.SurfaceVariant = tt.background palette.Text = tt.text css := GenerateCSS(palette) assert.Contains(t, css, "--control-text: "+tt.want+";") for _, selector := range []string{ ".stacked-pane-titlebar", ".stacked-pane-titlebar label", ".stacked-pane-titlebar image", } { block := regexp.MustCompile(regexp.QuoteMeta(selector) + `\s*\{[^}]*\}`).FindString(css) assert.Contains(t, block, "color: var(--control-text);", selector) } // Hover and active states must not invalidate the computed contrast. stackedCSS := generateStackedPaneCSS(palette) assert.NotContains(t, stackedCSS, "shade(var(--surface-variant)") hover := regexp.MustCompile(`\.stacked-pane-titlebar\.stacked-pane-title-clickable:hover label\s*\{[^}]*\}`).FindString(css) assert.Contains(t, hover, "text-decoration-line: underline;") assert.NotContains(t, hover, "color:") assert.Contains(t, stackedCSS, "border-left: 0.1875em solid var(--accent);") }) } } func TestGenerateCSS_UsesGTKFontForNativeWidgets(t *testing.T) { css := GenerateCSSWithScaleAndFonts(DefaultDarkPalette(), 1.0, FontConfig{ SansFont: "Fira Sans", MonospaceFont: "Fira Code", GtkFont: "Adwaita Sans", }) assert.Contains(t, css, `--font-sans: "Fira Sans", sans-serif;`) assert.Contains(t, css, `--font-mono: "Fira Code", monospace;`) assert.Contains(t, css, `--font-gtk: "Adwaita Sans", sans-serif;`) assert.Contains(t, css, "window, tooltip, popover {\n\tfont-family: var(--font-gtk);\n}") assert.NotContains(t, css, "window, tooltip, popover {\n\tfont-family: var(--font-sans);\n}") } func TestGenerateCSS_StandaloneOmniboxWindowUsesTransparentBackground(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) if !strings.Contains(css, "window.standalone-omnibox-window") { t.Fatalf("expected standalone omnibox window selector in CSS, got %q", css) } re := regexp.MustCompile(`window\.standalone-omnibox-window\s*\{\s*background-color:\s*transparent\s*;?\s*\}`) if !re.MatchString(css) { t.Fatalf("expected standalone omnibox window background to be transparent") } } func TestGenerateCSSFullWithTiming_VimModePulseUsesConfiguredTransitionDuration(t *testing.T) { // Vim Mode overlay pulse timings scale from the base transition duration: // normal pulse = 3× (150ms -> 450ms), fast pulse = 6× (150ms -> 900ms). css := GenerateCSSFullWithTiming(DefaultDarkPalette(), 1.0, DefaultFontConfig(), DefaultModeColors(), 150) assert.Contains(t, css, "animation: vim-mode-overlay-pulse-anim-a 450ms ease-in-out;") assert.Contains(t, css, "animation: vim-mode-overlay-pulse-fast-anim-a 900ms ease-in-out;") assert.NotContains(t, css, "vim-mode-indicator") } func TestGenerateCSS_OmniboxHeaderIsOpaque(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) if !strings.Contains(css, ".omnibox-header {") { t.Fatalf("expected omnibox header selector in CSS") } if !strings.Contains(css, "background-color: var(--surface);") { t.Fatalf("expected omnibox header to use an opaque surface background") } if !strings.Contains(css, "background-image: none;") { t.Fatalf("expected omnibox header gradient to be disabled") } } func TestGenerateCSS_OmniboxHeaderBadgeSelectorsExist(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) if !strings.Contains(css, ".omnibox-header-badge {") { t.Fatalf("expected omnibox header badge selector in CSS") } if !strings.Contains(css, ".omnibox-header-badge:hover {") { t.Fatalf("expected omnibox header badge hover selector in CSS") } } func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) 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;`) if !badgeRe.MatchString(css) { t.Fatalf("expected omnibox header badge to use passive neutral chip styling") } 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\);`) if !hoverRe.MatchString(css) { t.Fatalf("expected omnibox header badge hover styling to stay neutral while hinting interactivity") } badgeBlock := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*\}`).FindString(css) if strings.Contains(badgeBlock, "background-color: alpha(var(--accent)") { t.Fatalf("expected omnibox header badge resting state to avoid accent-based styling") } if strings.Contains(badgeBlock, "color: var(--accent)") { t.Fatalf("expected omnibox header badge resting text to avoid accent color") } } func TestGenerateCSS_FavoriteRowsOnlyTintTrailingAffordance(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) if !strings.Contains(css, ".omnibox-row.omnibox-row-favorite {") { t.Fatalf("expected favorite row selector in CSS") } if !strings.Contains(css, "border-left: 0.1875em solid var(--warning);") { t.Fatalf("expected favorite rows to mark the left edge with the warning color") } if strings.Contains(css, "border-right: 0.125em solid alpha(var(--warning), 0.55);") { t.Fatalf("expected favorite indicator to move off the right edge") } if !strings.Contains(css, "color: mix(var(--warning), var(--muted), 0.45);") { t.Fatalf("expected favorite star color to be softened") } if strings.Contains(css, ".omnibox-row.omnibox-row-favorite .omnibox-favorite-star-slot") { t.Fatalf("expected favorite highlight to move off the star slot") } } func TestGenerateCSS_OmniboxSearchEntryUsesControlSurface(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) containerRe := regexp.MustCompile(`(?s)\.omnibox-container\s*\{[^}]*background-color:\s*var\(--surface\);`) if !containerRe.MatchString(css) { t.Fatalf("expected omnibox container to use the header surface color") } 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\);`) if !entryRe.MatchString(css) { t.Fatalf("expected omnibox entry and its text node to use the control surface fill and readable text") } ghostBlock := cssRuleBlock(t, css, ".omnibox-ghost") assert.Contains(t, ghostBlock, "color: var(--control-text);") assert.NotContains(t, ghostBlock, "alpha(var(--control-text)") 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\);`) if !focusedEntryRe.MatchString(css) { t.Fatalf("expected focused omnibox entry text node to keep a distinct control-surface fill") } bangActiveRe := regexp.MustCompile(`(?s)entry\.omnibox-entry\.omnibox-entry-bang-active[^\{]*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);`) if !bangActiveRe.MatchString(css) { t.Fatalf("expected bang-active omnibox entry to keep a distinct control-surface fill") } if strings.Contains(css, "background-color: alpha(var(--bg), 0.88);") || strings.Contains(css, "background-color: shade(var(--bg), 1.05);") { t.Fatalf("expected omnibox entry fills not to derive from the page background") } scrolledRe := regexp.MustCompile(`(?s)\.omnibox-scrolled\s*\{[^}]*background-color:\s*var\(--surface\);`) if !scrolledRe.MatchString(css) { t.Fatalf("expected omnibox results area to use the header surface color") } } func TestGenerateCSS_ContextMenuUsesCompactSingleFrameStyling(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) assert.Contains(t, css, "popover.context-menu-popover > contents {") assert.Contains(t, css, ".context-menu {") assert.Contains(t, css, ".context-menu button.context-menu-item {") assert.Contains(t, css, ".context-menu button.context-menu-item:hover,") assert.Contains(t, css, ".context-menu button.context-menu-item:disabled {") assert.Contains(t, css, ".context-menu-separator {") popoverBlockRe := regexp.MustCompile(`(?s)popover\.context-menu-popover\s*>\s*contents\s*\{[^}]*\}`) popoverBlock := popoverBlockRe.FindString(css) if popoverBlock == "" { t.Fatal("expected context menu popover contents block in CSS") } assert.Contains(t, popoverBlock, "background-color: var(--surface);") assert.Contains(t, popoverBlock, "border: 0.0625em solid alpha(var(--border), 0.85);") assert.Contains(t, popoverBlock, "border-radius: 0.25em;") assert.Contains(t, popoverBlock, "box-shadow:") blockRe := regexp.MustCompile(`(?s)\.context-menu\s*\{[^}]*\}`) block := blockRe.FindString(css) if block == "" { t.Fatal("expected context menu block in CSS") } assert.NotContains(t, block, "background-color:") assert.Contains(t, block, "padding: 0.125em 0;") assert.Contains(t, block, "min-width: 10.5em;") assert.NotContains(t, block, "border:") assert.NotContains(t, block, "border-radius:") assert.NotContains(t, block, "box-shadow:") assert.NotContains(t, block, "#") assert.NotContains(t, block, "black") assert.NotContains(t, block, "white") itemBlockRe := regexp.MustCompile(`(?s)\.context-menu button\.context-menu-item\s*\{[^}]*\}`) itemBlock := itemBlockRe.FindString(css) if itemBlock == "" { t.Fatal("expected context menu item block in CSS") } assert.Contains(t, itemBlock, "padding: 0.25em 0.75em;") assert.Contains(t, itemBlock, "min-height: 1.75em;") } func TestGenerateCSSWithScale_DoesNotEmitRootTextScaling(t *testing.T) { css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) assert.NotContains(t, css, "/* UI Scaling (130%) */") assert.NotContains(t, css, "window {\n\tfont-size:") } func TestGenerateCSSWithScale_DoesNotEmitLineHeightOverrides(t *testing.T) { css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) assert.NotContains(t, css, "line-height:") assert.NotContains(t, css, "label,\nentry,\nentry > text {\n\tline-height:") } func TestGenerateCSSWithScale_UsesOpaqueOmniboxRowBaseBackground(t *testing.T) { css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) assert.Contains(t, css, ".omnibox-row {") assert.Contains(t, css, "background-color: var(--surface);") 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;") } func TestGenerateCSSWithScale_UsesMediumWeightForOmniboxSuggestionTitle(t *testing.T) { css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) assert.Contains(t, css, ".omnibox-suggestion-title {") assert.Contains(t, css, "font-weight: 500;") assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;") } func TestGenerateCSS_ContentAreaTabBarInset(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) topBlock := cssRuleBlock(t, css, ".content-area-tabbar-inset-top") if !strings.Contains(topBlock, "padding-top: 2em;") { t.Fatal("expected padding-top: 2em in .content-area-tabbar-inset-top block") } bottomBlock := cssRuleBlock(t, css, ".content-area-tabbar-inset-bottom") if !strings.Contains(bottomBlock, "padding-bottom: 2em;") { t.Fatal("expected padding-bottom: 2em in .content-area-tabbar-inset-bottom block") } if !strings.Contains(css, ".tab-bar") { t.Fatal("expected .tab-bar selector in generated CSS") } if !strings.Contains(css, "min-height: 2em;") { t.Fatal("expected min-height: 2em in .tab-bar block") } } func cssRuleBlock(t *testing.T, css, selector string) string { t.Helper() re := regexp.MustCompile(regexp.QuoteMeta(selector) + `\s*\{([^}]*)\}`) matches := re.FindStringSubmatch(css) if len(matches) != 2 { t.Fatalf("expected %s selector in generated CSS", selector) } return matches[1] } func TestGenerateCSS_TouchpadNavigationIndicatorFadesWholeOverlay(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) baseBlock := cssRuleBlock(t, css, ".touchpad-navigation-indicator") assert.Contains(t, baseBlock, "opacity: 1;") assert.Contains(t, baseBlock, "filter: blur(0px);") assert.Contains(t, baseBlock, "backdrop-filter: blur(0.75em);") assert.Contains(t, baseBlock, "transition: opacity 220ms ease-out,") assert.Contains(t, baseBlock, "filter 220ms ease-out,") assert.Contains(t, baseBlock, "backdrop-filter 220ms ease-out,") assert.Contains(t, baseBlock, "box-shadow 220ms ease-out;") hidingBlock := cssRuleBlock(t, css, ".touchpad-navigation-indicator.hiding") assert.Contains(t, hidingBlock, "opacity: 0;") assert.Contains(t, hidingBlock, "filter: blur(0.0625em);") assert.Contains(t, hidingBlock, "box-shadow:") } func TestGenerateCSS_DoesNotEmitUnsupportedGTKProperties(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) assert.NotContains(t, css, "pointer-events:") assert.NotContains(t, css, "text-align:") } func TestGenerateCSS_SessionContainerHasNoMinWidth(t *testing.T) { css := GenerateCSS(DefaultDarkPalette()) block := cssRuleBlock(t, css, ".session-manager-container") assert.NotContains(t, block, "min-width:") assert.Contains(t, block, "background-color: var(--surface-variant);") }