tiki/util/gradient/gradient_test.go
2026-04-10 16:07:34 -04:00

448 lines
10 KiB
Go

package gradient
import (
"testing"
"github.com/boolean-maybe/tiki/config"
)
func TestInterpolateRGB(t *testing.T) {
tests := []struct {
name string
from [3]int
to [3]int
t float64
want [3]int
}{
{
name: "t=0 returns from color",
from: [3]int{0, 0, 0},
to: [3]int{100, 200, 250},
t: 0,
want: [3]int{0, 0, 0},
},
{
name: "t=1 returns to color",
from: [3]int{0, 0, 0},
to: [3]int{100, 200, 250},
t: 1,
want: [3]int{100, 200, 250},
},
{
name: "t=0.5 midpoint with rounding",
from: [3]int{0, 0, 0},
to: [3]int{100, 200, 250},
t: 0.5,
want: [3]int{50, 100, 125},
},
{
name: "t clamped below 0",
from: [3]int{50, 100, 150},
to: [3]int{100, 200, 250},
t: -0.5,
want: [3]int{50, 100, 150},
},
{
name: "t clamped above 1",
from: [3]int{50, 100, 150},
to: [3]int{100, 200, 250},
t: 1.5,
want: [3]int{100, 200, 250},
},
{
name: "odd value rounding",
from: [3]int{0, 0, 0},
to: [3]int{99, 99, 99},
t: 0.5,
want: [3]int{50, 50, 50},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := InterpolateRGB(tt.from, tt.to, tt.t)
if got != tt.want {
t.Errorf("InterpolateRGB(%v, %v, %v) = %v, want %v",
tt.from, tt.to, tt.t, got, tt.want)
}
})
}
}
func TestClampRGB(t *testing.T) {
tests := []struct {
name string
value int
want int
}{
{"below zero", -10, 0},
{"zero", 0, 0},
{"mid range", 128, 128},
{"max value", 255, 255},
{"above max", 300, 255},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ClampRGB(tt.value)
if got != tt.want {
t.Errorf("ClampRGB(%d) = %d, want %d", tt.value, got, tt.want)
}
})
}
}
func TestLightenRGB(t *testing.T) {
tests := []struct {
name string
rgb [3]int
ratio float64
want [3]int
}{
{
name: "ratio 0 no change",
rgb: [3]int{100, 100, 100},
ratio: 0,
want: [3]int{100, 100, 100},
},
{
name: "ratio 1 full white",
rgb: [3]int{100, 100, 100},
ratio: 1,
want: [3]int{255, 255, 255},
},
{
name: "ratio 0.5 halfway to white",
rgb: [3]int{100, 100, 100},
ratio: 0.5,
want: [3]int{178, 178, 178},
},
{
name: "already white stays white",
rgb: [3]int{255, 255, 255},
ratio: 0.5,
want: [3]int{255, 255, 255},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := LightenRGB(tt.rgb, tt.ratio)
if got != tt.want {
t.Errorf("LightenRGB(%v, %v) = %v, want %v",
tt.rgb, tt.ratio, got, tt.want)
}
})
}
}
func TestDarkenRGB(t *testing.T) {
tests := []struct {
name string
rgb [3]int
ratio float64
want [3]int
}{
{
name: "ratio 0 no change",
rgb: [3]int{100, 100, 100},
ratio: 0,
want: [3]int{100, 100, 100},
},
{
name: "ratio 1 full black",
rgb: [3]int{100, 100, 100},
ratio: 1,
want: [3]int{0, 0, 0},
},
{
name: "ratio 0.5 halfway to black",
rgb: [3]int{100, 100, 100},
ratio: 0.5,
want: [3]int{50, 50, 50},
},
{
name: "already black stays black",
rgb: [3]int{0, 0, 0},
ratio: 0.5,
want: [3]int{0, 0, 0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := DarkenRGB(tt.rgb, tt.ratio)
if got != tt.want {
t.Errorf("DarkenRGB(%v, %v) = %v, want %v",
tt.rgb, tt.ratio, got, tt.want)
}
})
}
}
func TestRenderGradientText(t *testing.T) {
gradient := config.Gradient{
Start: [3]int{0, 0, 0},
End: [3]int{255, 255, 255},
}
tests := []struct {
name string
text string
want string
}{
{
name: "empty string",
text: "",
want: "",
},
{
name: "single character",
text: "A",
want: "[#000000]A",
},
{
name: "two characters",
text: "AB",
want: "[#000000]A[#ffffff]B",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := RenderGradientText(tt.text, gradient)
if got != tt.want {
t.Errorf("RenderGradientText(%q, gradient) = %q, want %q",
tt.text, got, tt.want)
}
})
}
}
func TestGradientFromColor(t *testing.T) {
fallback := config.Gradient{
Start: [3]int{255, 0, 0},
End: [3]int{0, 255, 0},
}
t.Run("black color uses fallback", func(t *testing.T) {
black := config.NewColorRGB(0, 0, 0)
got := GradientFromColor(black, 0.5, fallback)
if got != fallback {
t.Errorf("GradientFromColor(black) should return fallback, got %v", got)
}
})
t.Run("non-black color creates gradient", func(t *testing.T) {
blue := config.NewColorRGB(0, 0, 200)
got := GradientFromColor(blue, 0.5, fallback)
// Should have base color and lighter version
if got.Start != [3]int{0, 0, 200} {
t.Errorf("Expected Start to be [0, 0, 200], got %v", got.Start)
}
// End should be lighter than Start
if got.End[2] <= got.Start[2] {
t.Errorf("Expected End[2] > Start[2], got End=%v Start=%v", got.End, got.Start)
}
})
}
func TestGradientFromColorVibrant(t *testing.T) {
fallback := config.Gradient{
Start: [3]int{255, 0, 0},
End: [3]int{0, 255, 0},
}
t.Run("black color uses fallback", func(t *testing.T) {
black := config.NewColorRGB(0, 0, 0)
got := GradientFromColorVibrant(black, 1.5, fallback)
if got != fallback {
t.Errorf("GradientFromColorVibrant(black) should return fallback, got %v", got)
}
})
t.Run("non-black color creates boosted gradient", func(t *testing.T) {
blue := config.NewColorRGB(0, 0, 100)
got := GradientFromColorVibrant(blue, 1.5, fallback)
// Should have base color and boosted version
if got.Start != [3]int{0, 0, 100} {
t.Errorf("Expected Start to be [0, 0, 100], got %v", got.Start)
}
// End should be boosted (150) and clamped to 150
if got.End[2] != 150 {
t.Errorf("Expected End[2] to be 150, got %v", got.End[2])
}
})
}
func TestInterpolateColor(t *testing.T) {
gradient := config.Gradient{
Start: [3]int{0, 0, 0},
End: [3]int{100, 200, 250},
}
color := InterpolateColor(gradient, 0.5)
r, g, b := color.RGB()
// Should match InterpolateRGB result
if r != 50 || g != 100 || b != 125 {
t.Errorf("InterpolateColor returned RGB(%d, %d, %d), want RGB(50, 100, 125)",
r, g, b)
}
}
func TestRenderAdaptiveGradientText(t *testing.T) {
gradient := config.Gradient{
Start: [3]int{30, 144, 255}, // Dodger Blue
End: [3]int{0, 191, 255}, // Deep Sky Blue
}
fallback := config.NewColorRGB(0, 191, 255) // Deep Sky Blue
tests := []struct {
name string
text string
useGradients bool
checkSolid bool // If true, verify result is a solid color
}{
{
name: "empty string with gradients enabled",
text: "",
useGradients: true,
checkSolid: false,
},
{
name: "empty string with gradients disabled",
text: "",
useGradients: false,
checkSolid: false,
},
{
name: "gradients enabled renders full gradient",
text: "TIKI-123",
useGradients: true,
checkSolid: false,
},
{
name: "gradients disabled renders solid color",
text: "TIKI-123",
useGradients: false,
checkSolid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set gradient flag
config.UseGradients = tt.useGradients
got := RenderAdaptiveGradientText(tt.text, gradient, fallback)
// Empty string should return empty
if tt.text == "" {
if got != "" {
t.Errorf("Expected empty result for empty text, got %q", got)
}
return
}
// Check if we got a solid color when gradients disabled
if tt.checkSolid {
// Solid color format: [#rrggbb]text
// Should not have multiple color codes
expected := "[#00bfff]" + tt.text // Deep Sky Blue fallback
if got != expected {
t.Errorf("Expected solid color %q, got %q", expected, got)
}
} else if tt.useGradients {
// When gradients enabled, should have multiple color codes
// Check that result contains the text and color codes
if len(got) <= len(tt.text) {
t.Errorf("Expected gradient text longer than input, got %q", got)
}
}
})
}
}
func TestGradientCacheHit(t *testing.T) {
ResetGradientCache()
grad := config.Gradient{
Start: [3]int{0, 100, 200},
End: [3]int{255, 200, 100},
}
first := RenderGradientText("TIKI-ABC", grad)
second := RenderGradientText("TIKI-ABC", grad)
if first != second {
t.Errorf("cache miss: first %q != second %q", first, second)
}
// different text should produce different result
other := RenderGradientText("TIKI-XYZ", grad)
if first == other {
t.Errorf("different text should produce different gradient output")
}
// reset should clear cache (no panic, and subsequent call still works)
ResetGradientCache()
afterReset := RenderGradientText("TIKI-ABC", grad)
if afterReset != first {
t.Errorf("result after cache reset differs: %q vs %q", afterReset, first)
}
}
func BenchmarkRenderGradientText(b *testing.B) {
grad := config.Gradient{
Start: [3]int{30, 144, 255},
End: [3]int{0, 191, 255},
}
text := "TIKI-A1B2C3"
b.Run("cold", func(b *testing.B) {
for b.Loop() {
ResetGradientCache()
RenderGradientText(text, grad)
}
})
b.Run("warm", func(b *testing.B) {
ResetGradientCache()
RenderGradientText(text, grad) // prime cache
b.ResetTimer()
for b.Loop() {
RenderGradientText(text, grad)
}
})
}
func TestAdaptiveGradientRespectConfig(t *testing.T) {
gradient := config.Gradient{
Start: [3]int{100, 100, 100},
End: [3]int{200, 200, 200},
}
fallbackColor := config.NewColorRGB(200, 200, 200)
text := "Test"
// Test toggle behavior
config.UseGradients = true
resultWithGradients := RenderAdaptiveGradientText(text, gradient, fallbackColor)
config.UseGradients = false
resultWithoutGradients := RenderAdaptiveGradientText(text, gradient, fallbackColor)
// Results should be different
if resultWithGradients == resultWithoutGradients {
t.Errorf("Expected different results when UseGradients changes, both returned: %q", resultWithGradients)
}
// Without gradients should be shorter (single color code)
if len(resultWithoutGradients) >= len(resultWithGradients) {
t.Errorf("Expected solid color result to be shorter than gradient result")
}
}