clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

main
puzzletea / theme / theme_test.go
theme_test.go 6.4K
1package theme23import (4 "fmt"5 "image/color"6 "testing"7)89// --- Embedded theme loading (P0) ---1011func TestParseEmbeddedThemes(t *testing.T) {12 themes := AllThemes()13 if len(themes) == 0 {14 t.Fatal("expected at least one embedded theme")15 }16 // Spot-check a well-known theme exists.17 found := false18 for _, th := range themes {19 if th.Name == "Dracula" {20 found = true21 break22 }23 }24 if !found {25 t.Error("expected to find Dracula theme in embedded themes")26 }27}2829func TestThemeNames(t *testing.T) {30 names := ThemeNames()31 if len(names) < 2 {32 t.Fatalf("expected at least 2 theme names, got %d", len(names))33 }34 if names[0] != DefaultThemeName {35 t.Errorf("first theme name should be %q, got %q", DefaultThemeName, names[0])36 }37}3839func TestMaxNameLen(t *testing.T) {40 maxFromAllThemes := len(DefaultThemeName)41 for _, th := range AllThemes() {42 if len(th.Name) > maxFromAllThemes {43 maxFromAllThemes = len(th.Name)44 }45 }46 if MaxNameLen != maxFromAllThemes {47 t.Errorf("MaxNameLen = %d, want %d from AllThemes", MaxNameLen, maxFromAllThemes)48 }4950 maxFromThemeNames := 051 for _, name := range ThemeNames() {52 if len(name) > maxFromThemeNames {53 maxFromThemeNames = len(name)54 }55 }56 if MaxNameLen != maxFromThemeNames {57 t.Errorf("MaxNameLen = %d, want %d from ThemeNames", MaxNameLen, maxFromThemeNames)58 }59}6061func TestLookupTheme(t *testing.T) {62 th := LookupTheme("Dracula")63 if th == nil {64 t.Fatal("expected to find Dracula theme")65 }66 if th.Name != "Dracula" {67 t.Errorf("expected name Dracula, got %q", th.Name)68 }69 if !th.Meta.IsDark {70 t.Error("expected Dracula to be a dark theme")71 }72}7374func TestLookupThemeCaseInsensitive(t *testing.T) {75 th := LookupTheme("dracula")76 if th == nil {77 t.Fatal("expected case-insensitive lookup to find Dracula")78 }79}8081func TestLookupThemeNotFound(t *testing.T) {82 th := LookupTheme("nonexistent-theme-xyz")83 if th != nil {84 t.Error("expected nil for unknown theme")85 }86}8788// --- Palette derivation (P0) ---8990func TestDefaultPaletteNonNil(t *testing.T) {91 p := defaultPalette()92 if p.BG == nil {93 t.Error("default palette BG should not be nil")94 }95 if p.FG == nil {96 t.Error("default palette FG should not be nil")97 }98 if p.Accent == nil {99 t.Error("default palette Accent should not be nil")100 }101}102103func TestDerivePaletteFromTheme(t *testing.T) {104 th := LookupTheme("Dracula")105 if th == nil {106 t.Fatal("Dracula theme not found")107 }108 p := th.Palette()109 if p.BG == nil {110 t.Error("Dracula palette BG should not be nil")111 }112 if p.Success == nil {113 t.Error("Dracula palette Success should not be nil")114 }115 // All 16 ANSI slots should be populated.116 for i, c := range p.ANSI {117 if c == nil {118 t.Errorf("Dracula palette ANSI[%d] should not be nil", i)119 }120 }121}122123// --- Apply / Current (P0) ---124125func TestApplyDefault(t *testing.T) {126 if err := Apply(""); err != nil {127 t.Fatalf("Apply empty should succeed: %v", err)128 }129 p := Current()130 if p.BG == nil {131 t.Error("current palette BG should not be nil after applying default")132 }133}134135func TestApplyNamedTheme(t *testing.T) {136 if err := Apply("Dracula"); err != nil {137 t.Fatalf("Apply Dracula should succeed: %v", err)138 }139 p := Current()140 if p.BG == nil {141 t.Error("current palette BG should not be nil after applying Dracula")142 }143 // Restore default for other tests.144 _ = Apply("")145}146147func TestApplyUnknownTheme(t *testing.T) {148 err := Apply("nonexistent-theme-xyz")149 if err == nil {150 t.Error("expected error for unknown theme")151 }152}153154// --- Contrast enforcement (P0) ---155156func TestContrastRatioKnownValues(t *testing.T) {157 // Black on white should be 21:1.158 black := color.NRGBA{R: 0, G: 0, B: 0, A: 255}159 white := color.NRGBA{R: 255, G: 255, B: 255, A: 255}160 got := contrastRatio(black, white)161 if got < 20.9 || got > 21.1 {162 t.Errorf("black-on-white contrast = %.2f, want ~21.0", got)163 }164 // Same color should be 1:1.165 got = contrastRatio(black, black)166 if got < 0.99 || got > 1.01 {167 t.Errorf("same-color contrast = %.2f, want 1.0", got)168 }169}170171func TestDefaultPaletteContrast(t *testing.T) {172 p := defaultPalette()173 assertPaletteContrast(t, "default", p)174}175176func TestAllThemePaletteContrast(t *testing.T) {177 for _, th := range AllThemes() {178 p := th.Palette()179 t.Run(th.Name, func(t *testing.T) {180 assertPaletteContrast(t, th.Name, p)181 })182 }183}184185// assertPaletteContrast checks that every critical FG/BG pairing in a palette186// meets WCAG AA minimums. Bold/large text uses 3:1; normal text uses 4.5:1.187func assertPaletteContrast(t *testing.T, name string, p Palette) {188 t.Helper()189190 type pair struct {191 label string192 fg, bg color.Color193 minRatio float64194 }195196 pairs := []pair{197 // FG tokens on main BG (normal text → 4.5:1)198 {"FG on BG", p.FG, p.BG, minContrastNormal},199 {"TextDim on BG", p.TextDim, p.BG, minContrastNormal},200 {"Accent on BG", p.Accent, p.BG, minContrastNormal},201 {"AccentSoft on BG", p.AccentSoft, p.BG, minContrastNormal},202 {"Info on BG", p.Info, p.BG, minContrastNormal},203 {"Error on BG", p.Error, p.BG, minContrastNormal},204 {"Success on BG", p.Success, p.BG, minContrastNormal},205 {"Given on BG", p.Given, p.BG, minContrastNormal},206 {"Linked on BG", p.Linked, p.BG, minContrastNormal},207 {"Secondary on BG", p.Secondary, p.BG, minContrastNormal},208 {"Tertiary on BG", p.Tertiary, p.BG, minContrastNormal},209 {"Highlight on BG", p.Highlight, p.BG, minContrastNormal},210 // FG tokens on derived BGs (bold text → 3:1)211 {"AccentText on AccentBG (cursor)", p.AccentText, p.AccentBG, minContrastLarge},212 {"SolvedFG on SuccessBG (solved text)", p.SolvedFG, p.SuccessBG, minContrastNormal},213 {"SuccessBorder on SuccessBG (solved)", p.SuccessBorder, p.SuccessBG, minContrastLarge},214 }215216 for _, pp := range pairs {217 cr := contrastRatio(pp.fg, pp.bg)218 if cr < pp.minRatio {219 t.Errorf("[%s] %s: contrast %.2f:1 < %.1f:1 minimum",220 name, pp.label, cr, pp.minRatio)221 }222 }223}224225func TestMidTone(t *testing.T) {226 black := color.NRGBA{R: 0, G: 0, B: 0, A: 255}227 white := color.NRGBA{R: 255, G: 255, B: 255, A: 255}228 mid := MidTone(black, white)229 r, g, b, _ := mid.RGBA()230 // Should be roughly 127/128 for each channel.231 got := fmt.Sprintf("%d,%d,%d", r>>8, g>>8, b>>8)232 if got != "127,127,127" {233 t.Errorf("midTone(black, white) = %s, want 127,127,127", got)234 }235}236237func TestGivenTint(t *testing.T) {238 base := color.NRGBA{R: 24, G: 80, B: 160, A: 255}239 got := GivenTint(base)240 want := Blend(base, Current().Given, givenTintBlend)241242 gr, gg, gb, ga := got.RGBA()243 wr, wg, wb, wa := want.RGBA()244 if gr != wr || gg != wg || gb != wb || ga != wa {245 t.Fatalf("GivenTint() = %v, want %v", got, want)246 }247}