package engine import ( "math" "testing" ) func TestColorConfigValidate(t *testing.T) { tests := []struct { name string config ColorConfig wantErr bool errMsg string }{ { name: "valid default config", config: DefaultColorConfig(), wantErr: false, }, { name: "invalid color saturation < 0", config: ColorConfig{ ColorSaturation: -0.1, GrayscaleSaturation: 0.0, ColorLightness: LightnessRange{Min: 0.4, Max: 0.8}, GrayscaleLightness: LightnessRange{Min: 0.3, Max: 0.9}, IconPadding: 0.08, }, wantErr: true, errMsg: "color saturation out of range", }, { name: "invalid grayscale saturation > 1", config: ColorConfig{ ColorSaturation: 0.5, GrayscaleSaturation: 1.5, ColorLightness: LightnessRange{Min: 0.4, Max: 0.8}, GrayscaleLightness: LightnessRange{Min: 0.3, Max: 0.9}, IconPadding: 0.08, }, wantErr: true, errMsg: "grayscale saturation out of range", }, { name: "invalid color lightness min > max", config: ColorConfig{ ColorSaturation: 0.5, GrayscaleSaturation: 0.0, ColorLightness: LightnessRange{Min: 0.8, Max: 0.4}, GrayscaleLightness: LightnessRange{Min: 0.3, Max: 0.9}, IconPadding: 0.08, }, wantErr: true, errMsg: "color lightness range invalid", }, { name: "invalid icon padding > 1", config: ColorConfig{ ColorSaturation: 0.5, GrayscaleSaturation: 0.0, ColorLightness: LightnessRange{Min: 0.4, Max: 0.8}, GrayscaleLightness: LightnessRange{Min: 0.3, Max: 0.9}, IconPadding: 1.5, }, wantErr: true, errMsg: "icon padding out of range", }, { name: "multiple validation errors", config: ColorConfig{ ColorSaturation: -0.1, // Invalid GrayscaleSaturation: 1.5, // Invalid ColorLightness: LightnessRange{Min: 0.4, Max: 0.8}, GrayscaleLightness: LightnessRange{Min: 0.3, Max: 0.9}, IconPadding: 0.08, }, wantErr: true, errMsg: "color saturation out of range", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.config.Validate() if tt.wantErr { if err == nil { t.Errorf("Expected error for config validation, got none") return } if tt.errMsg != "" && !containsString(err.Error(), tt.errMsg) { t.Errorf("Expected error message to contain '%s', got '%s'", tt.errMsg, err.Error()) } } else { if err != nil { t.Errorf("Unexpected error: %v", err) } } }) } } func TestDefaultColorConfig(t *testing.T) { config := DefaultColorConfig() // Test default values match JavaScript implementation if config.ColorSaturation != 0.5 { t.Errorf("ColorSaturation = %f, want 0.5", config.ColorSaturation) } if config.GrayscaleSaturation != 0.0 { t.Errorf("GrayscaleSaturation = %f, want 0.0", config.GrayscaleSaturation) } if config.ColorLightness.Min != 0.4 || config.ColorLightness.Max != 0.8 { t.Errorf("ColorLightness = {%f, %f}, want {0.4, 0.8}", config.ColorLightness.Min, config.ColorLightness.Max) } if config.GrayscaleLightness.Min != 0.3 || config.GrayscaleLightness.Max != 0.9 { t.Errorf("GrayscaleLightness = {%f, %f}, want {0.3, 0.9}", config.GrayscaleLightness.Min, config.GrayscaleLightness.Max) } if len(config.Hues) != 0 { t.Errorf("Hues should be empty by default, got %v", config.Hues) } if config.BackColor != nil { t.Error("BackColor should be nil by default") } if config.IconPadding != 0.08 { t.Errorf("IconPadding = %f, want 0.08", config.IconPadding) } } func TestLightnessRangeGetLightness(t *testing.T) { lr := LightnessRange{Min: 0.3, Max: 0.9} tests := []struct { value float64 expected float64 }{ {0.0, 0.3}, // Min value {1.0, 0.9}, // Max value {0.5, 0.6}, // Middle value: 0.3 + 0.5 * (0.9 - 0.3) = 0.6 {-0.5, 0.3}, // Below range, should clamp to min {1.5, 0.9}, // Above range, should clamp to max } for _, tt := range tests { result := lr.GetLightness(tt.value) if math.Abs(result-tt.expected) > 0.001 { t.Errorf("GetLightness(%f) = %f, want %f", tt.value, result, tt.expected) } } } func TestConfigRestrictHue(t *testing.T) { tests := []struct { name string hues []float64 originalHue float64 expectedHue float64 }{ { name: "no restriction", hues: nil, originalHue: 0.25, expectedHue: 0.25, }, { name: "empty restriction", hues: []float64{}, originalHue: 0.25, expectedHue: 0.25, }, { name: "single hue restriction", hues: []float64{180}, // 180 degrees = 0.5 turns originalHue: 0.25, expectedHue: 0.5, }, { name: "multiple hue restriction", hues: []float64{0, 120, 240}, // Red, Green, Blue originalHue: 0.1, // Should map to first hue (0 degrees) expectedHue: 0.0, }, { name: "hue normalization - negative", hues: []float64{90}, // 90 degrees = 0.25 turns originalHue: -0.5, expectedHue: 0.25, }, { name: "hue normalization - over 1", hues: []float64{270}, // 270 degrees = 0.75 turns originalHue: 1.5, expectedHue: 0.75, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := ColorConfig{Hues: tt.hues} result := config.RestrictHue(tt.originalHue) if math.Abs(result-tt.expectedHue) > 0.001 { t.Errorf("RestrictHue(%f) = %f, want %f", tt.originalHue, result, tt.expectedHue) } }) } } func TestConfigNormalize(t *testing.T) { // Test that Normalize corrects invalid values config := ColorConfig{ ColorSaturation: -0.5, // Invalid: below 0 GrayscaleSaturation: 1.5, // Invalid: above 1 ColorLightness: LightnessRange{Min: 0.8, Max: 0.2}, // Invalid: min > max GrayscaleLightness: LightnessRange{Min: -0.1, Max: 1.1}, // Invalid: out of range IconPadding: 2.0, // Invalid: above 1 } config.Normalize() if config.ColorSaturation != 0.0 { t.Errorf("ColorSaturation after validation = %f, want 0.0", config.ColorSaturation) } if config.GrayscaleSaturation != 1.0 { t.Errorf("GrayscaleSaturation after validation = %f, want 1.0", config.GrayscaleSaturation) } // Min and max should be swapped if config.ColorLightness.Min != 0.2 || config.ColorLightness.Max != 0.8 { t.Errorf("ColorLightness after validation = {%f, %f}, want {0.2, 0.8}", config.ColorLightness.Min, config.ColorLightness.Max) } // Values should be clamped if config.GrayscaleLightness.Min != 0.0 || config.GrayscaleLightness.Max != 1.0 { t.Errorf("GrayscaleLightness after validation = {%f, %f}, want {0.0, 1.0}", config.GrayscaleLightness.Min, config.GrayscaleLightness.Max) } if config.IconPadding != 1.0 { t.Errorf("IconPadding after validation = %f, want 1.0", config.IconPadding) } } func TestColorConfigBuilder(t *testing.T) { redColor := NewColorRGB(255, 0, 0) config := DefaultColorConfig() config.ColorSaturation = 0.7 config.GrayscaleSaturation = 0.1 config.ColorLightness = LightnessRange{Min: 0.2, Max: 0.8} config.GrayscaleLightness = LightnessRange{Min: 0.1, Max: 0.9} config.Hues = []float64{0, 120, 240} config.BackColor = &redColor config.IconPadding = 0.1 if config.ColorSaturation != 0.7 { t.Errorf("ColorSaturation = %f, want 0.7", config.ColorSaturation) } if config.GrayscaleSaturation != 0.1 { t.Errorf("GrayscaleSaturation = %f, want 0.1", config.GrayscaleSaturation) } if config.ColorLightness.Min != 0.2 || config.ColorLightness.Max != 0.8 { t.Errorf("ColorLightness = {%f, %f}, want {0.2, 0.8}", config.ColorLightness.Min, config.ColorLightness.Max) } if config.GrayscaleLightness.Min != 0.1 || config.GrayscaleLightness.Max != 0.9 { t.Errorf("GrayscaleLightness = {%f, %f}, want {0.1, 0.9}", config.GrayscaleLightness.Min, config.GrayscaleLightness.Max) } if len(config.Hues) != 3 || config.Hues[0] != 0 || config.Hues[1] != 120 || config.Hues[2] != 240 { t.Errorf("Hues = %v, want [0, 120, 240]", config.Hues) } if config.BackColor == nil || !config.BackColor.Equals(redColor) { t.Error("BackColor should be set to red") } if config.IconPadding != 0.1 { t.Errorf("IconPadding = %f, want 0.1", config.IconPadding) } } func TestColorConfigValidation(t *testing.T) { // Test direct config validation config := DefaultColorConfig() config.ColorSaturation = -0.5 // Invalid config.GrayscaleSaturation = 1.5 // Invalid err := config.Validate() // Should return validation error for invalid values if err == nil { t.Error("Expected validation error for invalid configuration, got nil") } if !containsString(err.Error(), "color saturation out of range") { t.Errorf("Expected error to mention color saturation validation, got: %s", err.Error()) } } // containsString checks if a string contains a substring func containsString(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }