package dank16 import ( "encoding/json" "math" "testing" ) func TestHexToRGB(t *testing.T) { tests := []struct { name string input string expected RGB }{ { name: "black with hash", input: "#000000", expected: RGB{R: 0.0, G: 0.0, B: 0.0}, }, { name: "white with hash", input: "#ffffff", expected: RGB{R: 1.0, G: 1.0, B: 1.0}, }, { name: "red without hash", input: "ff0000", expected: RGB{R: 1.0, G: 0.0, B: 0.0}, }, { name: "purple", input: "#625690", expected: RGB{R: 0.3843137254901961, G: 0.33725490196078434, B: 0.5647058823529412}, }, { name: "mid gray", input: "#808080", expected: RGB{R: 0.5019607843137255, G: 0.5019607843137255, B: 0.5019607843137255}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := HexToRGB(tt.input) if !floatEqual(result.R, tt.expected.R) || !floatEqual(result.G, tt.expected.G) || !floatEqual(result.B, tt.expected.B) { t.Errorf("HexToRGB(%s) = %v, expected %v", tt.input, result, tt.expected) } }) } } func TestRGBToHex(t *testing.T) { tests := []struct { name string input RGB expected string }{ { name: "black", input: RGB{R: 0.0, G: 0.0, B: 0.0}, expected: "#000000", }, { name: "white", input: RGB{R: 1.0, G: 1.0, B: 1.0}, expected: "#ffffff", }, { name: "red", input: RGB{R: 1.0, G: 0.0, B: 0.0}, expected: "#ff0000", }, { name: "clamping above 1.0", input: RGB{R: 1.5, G: 0.5, B: 0.5}, expected: "#ff7f7f", }, { name: "clamping below 0.0", input: RGB{R: -0.5, G: 0.5, B: 0.5}, expected: "#007f7f", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := RGBToHex(tt.input) if result != tt.expected { t.Errorf("RGBToHex(%v) = %s, expected %s", tt.input, result, tt.expected) } }) } } func TestRGBToHSV(t *testing.T) { tests := []struct { name string input RGB expected HSV }{ { name: "black", input: RGB{R: 0.0, G: 0.0, B: 0.0}, expected: HSV{H: 0.0, S: 0.0, V: 0.0}, }, { name: "white", input: RGB{R: 1.0, G: 1.0, B: 1.0}, expected: HSV{H: 0.0, S: 0.0, V: 1.0}, }, { name: "red", input: RGB{R: 1.0, G: 0.0, B: 0.0}, expected: HSV{H: 0.0, S: 1.0, V: 1.0}, }, { name: "green", input: RGB{R: 0.0, G: 1.0, B: 0.0}, expected: HSV{H: 0.3333333333333333, S: 1.0, V: 1.0}, }, { name: "blue", input: RGB{R: 0.0, G: 0.0, B: 1.0}, expected: HSV{H: 0.6666666666666666, S: 1.0, V: 1.0}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := RGBToHSV(tt.input) if !floatEqual(result.H, tt.expected.H) || !floatEqual(result.S, tt.expected.S) || !floatEqual(result.V, tt.expected.V) { t.Errorf("RGBToHSV(%v) = %v, expected %v", tt.input, result, tt.expected) } }) } } func TestHSVToRGB(t *testing.T) { tests := []struct { name string input HSV expected RGB }{ { name: "black", input: HSV{H: 0.0, S: 0.0, V: 0.0}, expected: RGB{R: 0.0, G: 0.0, B: 0.0}, }, { name: "white", input: HSV{H: 0.0, S: 0.0, V: 1.0}, expected: RGB{R: 1.0, G: 1.0, B: 1.0}, }, { name: "red", input: HSV{H: 0.0, S: 1.0, V: 1.0}, expected: RGB{R: 1.0, G: 0.0, B: 0.0}, }, { name: "green", input: HSV{H: 0.3333333333333333, S: 1.0, V: 1.0}, expected: RGB{R: 0.0, G: 1.0, B: 0.0}, }, { name: "blue", input: HSV{H: 0.6666666666666666, S: 1.0, V: 1.0}, expected: RGB{R: 0.0, G: 0.0, B: 1.0}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := HSVToRGB(tt.input) if !floatEqual(result.R, tt.expected.R) || !floatEqual(result.G, tt.expected.G) || !floatEqual(result.B, tt.expected.B) { t.Errorf("HSVToRGB(%v) = %v, expected %v", tt.input, result, tt.expected) } }) } } func TestLuminance(t *testing.T) { tests := []struct { name string input string expected float64 }{ { name: "black", input: "#000000", expected: 0.0, }, { name: "white", input: "#ffffff", expected: 1.0, }, { name: "red", input: "#ff0000", expected: 0.2126, }, { name: "green", input: "#00ff00", expected: 0.7152, }, { name: "blue", input: "#0000ff", expected: 0.0722, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Luminance(tt.input) if !floatEqual(result, tt.expected) { t.Errorf("Luminance(%s) = %f, expected %f", tt.input, result, tt.expected) } }) } } func TestContrastRatio(t *testing.T) { tests := []struct { name string fg string bg string expected float64 }{ { name: "black on white", fg: "#000000", bg: "#ffffff", expected: 21.0, }, { name: "white on black", fg: "#ffffff", bg: "#000000", expected: 21.0, }, { name: "same color", fg: "#808080", bg: "#808080", expected: 1.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ContrastRatio(tt.fg, tt.bg) if !floatEqual(result, tt.expected) { t.Errorf("ContrastRatio(%s, %s) = %f, expected %f", tt.fg, tt.bg, result, tt.expected) } }) } } func TestEnsureContrast(t *testing.T) { tests := []struct { name string color string bg string minRatio float64 isLightMode bool }{ { name: "already sufficient contrast dark mode", color: "#ffffff", bg: "#000000", minRatio: 4.5, isLightMode: false, }, { name: "already sufficient contrast light mode", color: "#000000", bg: "#ffffff", minRatio: 4.5, isLightMode: true, }, { name: "needs adjustment dark mode", color: "#404040", bg: "#1a1a1a", minRatio: 4.5, isLightMode: false, }, { name: "needs adjustment light mode", color: "#c0c0c0", bg: "#f8f8f8", minRatio: 4.5, isLightMode: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := EnsureContrast(tt.color, tt.bg, tt.minRatio, tt.isLightMode) actualRatio := ContrastRatio(result, tt.bg) if actualRatio < tt.minRatio { t.Errorf("EnsureContrast(%s, %s, %f, %t) = %s with ratio %f, expected ratio >= %f", tt.color, tt.bg, tt.minRatio, tt.isLightMode, result, actualRatio, tt.minRatio) } }) } } func TestGeneratePalette(t *testing.T) { tests := []struct { name string base string opts PaletteOptions }{ { name: "dark theme default", base: "#625690", opts: PaletteOptions{IsLight: false}, }, { name: "light theme default", base: "#625690", opts: PaletteOptions{IsLight: true}, }, { name: "light theme with custom background", base: "#625690", opts: PaletteOptions{ IsLight: true, Background: "#fafafa", }, }, { name: "dark theme with custom background", base: "#625690", opts: PaletteOptions{ IsLight: false, Background: "#0a0a0a", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := GeneratePalette(tt.base, tt.opts) if len(result) != 16 { t.Errorf("GeneratePalette returned %d colors, expected 16", len(result)) } for i, color := range result { if len(color) != 7 || color[0] != '#' { t.Errorf("Color at index %d (%s) is not a valid hex color", i, color) } } if tt.opts.Background != "" && result[0] != tt.opts.Background { t.Errorf("Background color = %s, expected %s", result[0], tt.opts.Background) } else if !tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#1a1a1a" { t.Errorf("Dark mode background = %s, expected #1a1a1a", result[0]) } else if tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#f8f8f8" { t.Errorf("Light mode background = %s, expected #f8f8f8", result[0]) } if tt.opts.IsLight && result[15] != "#1a1a1a" { t.Errorf("Light mode foreground = %s, expected #1a1a1a", result[15]) } else if !tt.opts.IsLight && result[15] != "#ffffff" { t.Errorf("Dark mode foreground = %s, expected #ffffff", result[15]) } }) } } func TestEnrichVSCodeTheme(t *testing.T) { colors := GeneratePalette("#625690", PaletteOptions{IsLight: false}) baseTheme := map[string]interface{}{ "name": "Test Theme", "type": "dark", "colors": map[string]interface{}{ "editor.background": "#000000", }, } themeJSON, err := json.Marshal(baseTheme) if err != nil { t.Fatalf("Failed to marshal base theme: %v", err) } result, err := EnrichVSCodeTheme(themeJSON, colors) if err != nil { t.Fatalf("EnrichVSCodeTheme failed: %v", err) } var enriched map[string]interface{} if err := json.Unmarshal(result, &enriched); err != nil { t.Fatalf("Failed to unmarshal result: %v", err) } colorsMap, ok := enriched["colors"].(map[string]interface{}) if !ok { t.Fatal("colors is not a map") } terminalColors := []string{ "terminal.ansiBlack", "terminal.ansiRed", "terminal.ansiGreen", "terminal.ansiYellow", "terminal.ansiBlue", "terminal.ansiMagenta", "terminal.ansiCyan", "terminal.ansiWhite", "terminal.ansiBrightBlack", "terminal.ansiBrightRed", "terminal.ansiBrightGreen", "terminal.ansiBrightYellow", "terminal.ansiBrightBlue", "terminal.ansiBrightMagenta", "terminal.ansiBrightCyan", "terminal.ansiBrightWhite", } for i, key := range terminalColors { if val, ok := colorsMap[key]; !ok { t.Errorf("Missing terminal color: %s", key) } else if val != colors[i] { t.Errorf("%s = %s, expected %s", key, val, colors[i]) } } if colorsMap["editor.background"] != "#000000" { t.Error("Original theme colors should be preserved") } } func TestEnrichVSCodeThemeInvalidJSON(t *testing.T) { colors := GeneratePalette("#625690", PaletteOptions{IsLight: false}) invalidJSON := []byte("{invalid json") _, err := EnrichVSCodeTheme(invalidJSON, colors) if err == nil { t.Error("Expected error for invalid JSON, got nil") } } func TestRoundTripConversion(t *testing.T) { testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"} for _, hex := range testColors { t.Run(hex, func(t *testing.T) { rgb := HexToRGB(hex) result := RGBToHex(rgb) if result != hex { t.Errorf("Round trip %s -> RGB -> %s failed", hex, result) } }) } } func TestRGBHSVRoundTrip(t *testing.T) { testCases := []RGB{ {R: 0.0, G: 0.0, B: 0.0}, {R: 1.0, G: 1.0, B: 1.0}, {R: 1.0, G: 0.0, B: 0.0}, {R: 0.0, G: 1.0, B: 0.0}, {R: 0.0, G: 0.0, B: 1.0}, {R: 0.5, G: 0.5, B: 0.5}, {R: 0.3843137254901961, G: 0.33725490196078434, B: 0.5647058823529412}, } for _, rgb := range testCases { t.Run("", func(t *testing.T) { hsv := RGBToHSV(rgb) result := HSVToRGB(hsv) if !floatEqual(result.R, rgb.R) || !floatEqual(result.G, rgb.G) || !floatEqual(result.B, rgb.B) { t.Errorf("Round trip RGB->HSV->RGB failed: %v -> %v -> %v", rgb, hsv, result) } }) } } func floatEqual(a, b float64) bool { return math.Abs(a-b) < 1e-9 } func TestDeltaPhiStar(t *testing.T) { tests := []struct { name string fg string bg string negativePolarity bool minExpected float64 }{ { name: "white on black (negative polarity)", fg: "#ffffff", bg: "#000000", negativePolarity: true, minExpected: 100.0, }, { name: "black on white (positive polarity)", fg: "#000000", bg: "#ffffff", negativePolarity: false, minExpected: 100.0, }, { name: "low contrast same color", fg: "#808080", bg: "#808080", negativePolarity: false, minExpected: -40.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := DeltaPhiStar(tt.fg, tt.bg, tt.negativePolarity) if result < tt.minExpected { t.Errorf("DeltaPhiStar(%s, %s, %v) = %f, expected >= %f", tt.fg, tt.bg, tt.negativePolarity, result, tt.minExpected) } }) } } func TestDeltaPhiStarContrast(t *testing.T) { tests := []struct { name string fg string bg string isLightMode bool minExpected float64 }{ { name: "white on black (dark mode)", fg: "#ffffff", bg: "#000000", isLightMode: false, minExpected: 100.0, }, { name: "black on white (light mode)", fg: "#000000", bg: "#ffffff", isLightMode: true, minExpected: 100.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := DeltaPhiStarContrast(tt.fg, tt.bg, tt.isLightMode) if result < tt.minExpected { t.Errorf("DeltaPhiStarContrast(%s, %s, %v) = %f, expected >= %f", tt.fg, tt.bg, tt.isLightMode, result, tt.minExpected) } }) } } func TestEnsureContrastDPS(t *testing.T) { tests := []struct { name string color string bg string minLc float64 isLightMode bool }{ { name: "already sufficient contrast dark mode", color: "#ffffff", bg: "#000000", minLc: 60.0, isLightMode: false, }, { name: "already sufficient contrast light mode", color: "#000000", bg: "#ffffff", minLc: 60.0, isLightMode: true, }, { name: "needs adjustment dark mode", color: "#404040", bg: "#1a1a1a", minLc: 60.0, isLightMode: false, }, { name: "needs adjustment light mode", color: "#c0c0c0", bg: "#f8f8f8", minLc: 60.0, isLightMode: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := EnsureContrastDPS(tt.color, tt.bg, tt.minLc, tt.isLightMode) actualLc := DeltaPhiStarContrast(result, tt.bg, tt.isLightMode) if actualLc < tt.minLc { t.Errorf("EnsureContrastDPS(%s, %s, %f, %t) = %s with Lc %f, expected Lc >= %f", tt.color, tt.bg, tt.minLc, tt.isLightMode, result, actualLc, tt.minLc) } }) } } func TestGeneratePaletteWithDPS(t *testing.T) { tests := []struct { name string base string opts PaletteOptions }{ { name: "dark theme with DPS", base: "#625690", opts: PaletteOptions{IsLight: false, UseDPS: true}, }, { name: "light theme with DPS", base: "#625690", opts: PaletteOptions{IsLight: true, UseDPS: true}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := GeneratePalette(tt.base, tt.opts) if len(result) != 16 { t.Errorf("GeneratePalette returned %d colors, expected 16", len(result)) } for i, color := range result { if len(color) != 7 || color[0] != '#' { t.Errorf("Color at index %d (%s) is not a valid hex color", i, color) } } bgColor := result[0] for i := 1; i < 8; i++ { lc := DeltaPhiStarContrast(result[i], bgColor, tt.opts.IsLight) minLc := 30.0 if lc < minLc && lc > 0 { t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)", i, result[i], lc, bgColor, minLc) } } }) } } func TestDeriveContainer(t *testing.T) { tests := []struct { name string primary string isLight bool expected string }{ { name: "dark mode", primary: "#ccbdff", isLight: false, expected: "#4a3e76", }, { name: "light mode", primary: "#625690", isLight: true, expected: "#e7deff", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := DeriveContainer(tt.primary, tt.isLight) resultRGB := HexToRGB(result) expectedRGB := HexToRGB(tt.expected) rDiff := math.Abs(resultRGB.R - expectedRGB.R) gDiff := math.Abs(resultRGB.G - expectedRGB.G) bDiff := math.Abs(resultRGB.B - expectedRGB.B) tolerance := 0.02 if rDiff > tolerance || gDiff > tolerance || bDiff > tolerance { t.Errorf("DeriveContainer(%s, %v) = %s, expected %s (RGB diff: R:%.4f G:%.4f B:%.4f)", tt.primary, tt.isLight, result, tt.expected, rDiff, gDiff, bDiff) } }) } } func TestContrastAlgorithmComparison(t *testing.T) { base := "#625690" optsWCAG := PaletteOptions{IsLight: false, UseDPS: false} optsDPS := PaletteOptions{IsLight: false, UseDPS: true} paletteWCAG := GeneratePalette(base, optsWCAG) paletteDPS := GeneratePalette(base, optsDPS) if len(paletteWCAG) != 16 || len(paletteDPS) != 16 { t.Fatal("Both palettes should have 16 colors") } if paletteWCAG[0] != paletteDPS[0] { t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG[0], paletteDPS[0]) } differentCount := 0 for i := 0; i < 16; i++ { if paletteWCAG[i] != paletteDPS[i] { differentCount++ } } t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount) }