package ui import ( "strings" "testing" "github.com/charmbracelet/lipgloss" ) // Every registered theme must carry valid #RRGGBB tokens — guards against a // typo in the ported palettes. func TestThemesHaveValidHex(t *testing.T) { if len(themes) != 6 { t.Fatalf("expected 6 themes, got %d", len(themes)) } valid := func(c string) bool { if len(c) != 7 || c[0] != '#' { return false } _, ok := strings.CutPrefix(c, "#") r, g, b := hexRGB(c) return ok && r >= 0 && g >= 0 && b >= 0 } for _, th := range themes { for name, c := range map[string]string{ "Bg": string(th.Bg), "Border": string(th.Border), "Faint": string(th.Faint), "Text": string(th.Text), "Muted": string(th.Muted), "Live": string(th.Live), "Accent": string(th.Accent), "Accent2": string(th.Accent2), "Win": string(th.Win), "Warn": string(th.Warn), "Goal": string(th.Goal), } { if !valid(c) { t.Errorf("%s.%s = %q is not #RRGGBB", th.ID, name, c) } } } } // applyTheme must repoint both the color globals and the derived styles. func TestApplyThemeUpdatesGlobalsAndStyles(t *testing.T) { applyTheme(themes[themeIndex("uchu")]) if colText != themes[themeIndex("uchu")].Text { t.Errorf("colText not updated: %v", colText) } if got := styleScore.GetForeground(); got != colText { t.Errorf("styleScore foreground %v != colText %v", got, colText) } if colWarnHex != strings.TrimPrefix(string(colWarn), "#") { t.Errorf("colWarnHex %q out of sync with colWarn %v", colWarnHex, colWarn) } // Restore default so other tests see the original palette. applyTheme(themes[0]) } // themeIndex is forgiving: unknown / empty IDs fall back to the default (0). func TestThemeIndexFallback(t *testing.T) { if themeIndex("") != 0 || themeIndex("nope") != 0 { t.Error("unknown theme id should map to index 0") } if themeIndex("humdrum-dark") != themeIndex("humdrum-dark") { t.Error("themeIndex not stable") } } func TestPaintBackgroundFillsAndReasserts(t *testing.T) { applyTheme(themes[0]) const reset = "\x1b[0m" // A line with an inner reset (as lipgloss would emit after a styled span). in := "hi" + reset + "yo" out := paintBackground(in, 10) // Base (fg+bg truecolor) must lead the line. if !strings.HasPrefix(out, "\x1b[38;2;") { t.Fatalf("no leading base SGR: %q", out) } // The inner reset must be followed by a re-asserted background, so text // after it isn't left on the terminal's own background. if !strings.Contains(out, reset+"\x1b[38;2;") { t.Errorf("reset not followed by base re-assert: %q", out) } // Line padded to the full width (printable width == 10) and ends reset. if w := lipgloss.Width(out); w != 10 { t.Errorf("painted width = %d, want 10", w) } if !strings.HasSuffix(out, reset) { t.Errorf("line should end with reset: %q", out) } } func TestThemesForSplitsByAppearance(t *testing.T) { dark := themesFor(true) light := themesFor(false) if len(dark) != 3 || len(light) != 3 { t.Fatalf("expected 3 dark + 3 light, got %d/%d", len(dark), len(light)) } for _, i := range dark { if !themes[i].Dark { t.Errorf("themesFor(true) returned light theme %s", themes[i].ID) } } for _, i := range light { if themes[i].Dark { t.Errorf("themesFor(false) returned dark theme %s", themes[i].ID) } } } func TestResolveThemeHonorsAppearance(t *testing.T) { // Persisted dark theme, but terminal is light → fall back to a light theme. got := resolveTheme("flexoki-dark", false) if themes[got].Dark { t.Errorf("light terminal resolved to dark theme %s", themes[got].ID) } // Persisted theme matches appearance → keep it. if got := resolveTheme("uchu-dark", true); themes[got].ID != "uchu-dark" { t.Errorf("matching persisted theme not kept: %s", themes[got].ID) } // Empty → first of appearance. if got := resolveTheme("", true); !themes[got].Dark { t.Errorf("empty config on dark terminal gave light theme %s", themes[got].ID) } }