import Testing @testable import FlixyDesign @Suite("Kontrastkorrektur") struct ContrastTests { private let canvas = FlixyColor.canvas @Test("Schwarz zu Weiß ergibt das Maximum von 21:1") func extremesProduceMaximumRatio() { let ratio = Contrast.ratio(RGB(hex: 0x000000), RGB(hex: 0xFFFFFF)) #expect(abs(ratio - 21) < 0.01) } @Test("Gleiche Farben ergeben 1:1") func identicalColorsProduceMinimumRatio() { #expect(abs(Contrast.ratio(canvas, canvas) - 1) < 0.0001) } @Test("Farben mit genug Kontrast bleiben unangetastet") func sufficientColorsAreLeftAlone() { // Wichtig, weil die kuratierte Palette sonst beim Zeichnen verfälscht // würde — die Kachel in den Einstellungen und die Farbe in der App // müssen übereinstimmen. let amber = RGB(hex: 0xFFB224) #expect(Contrast.legible(amber, on: canvas) == amber) } @Test( "Zu dunkle Farben werden bis zur Lesbarkeit angehoben", arguments: [0x00008B, 0x1A1A2E, 0x2D0A31, 0x003300, 0x4A0000, 0x111111] ) func darkColorsAreLifted(hex: Int) { let original = RGB(hex: UInt32(hex)) let corrected = Contrast.legible(original, on: canvas) #expect(Contrast.ratio(original, canvas) < Contrast.minimumForText) #expect(Contrast.ratio(corrected, canvas) >= Contrast.minimumForText) } @Test("Die Korrektur hebt nur so weit an wie nötig") func correctionIsMinimal() { // Ein Sprung auf Weiß wäre die einfache Lösung, würde aber jede // dunkle Wunschfarbe in dieselbe Farbe verwandeln. let corrected = Contrast.legible(RGB(hex: 0x00008B), on: canvas) #expect(Contrast.ratio(corrected, canvas) < Contrast.minimumForText + 0.5) #expect(corrected != RGB(hex: 0xFFFFFF)) } @Test("Der Farbton überlebt die Korrektur") func correctionPreservesHue() { let darkBlue = RGB(hex: 0x00008B) let corrected = Contrast.legible(darkBlue, on: canvas) // Blau muss blau bleiben. Ein Abstand von 0,02 im Farbkreis sind rund // sieben Grad — für das Auge dieselbe Farbe. #expect(abs(HSB(corrected).hue - HSB(darkBlue).hue) < 0.02) } @Test("Selbst Schwarz wird lesbar gemacht") func blackIsHandled() { // Schwarz hat keinen Farbton, an dem sich festhalten ließe — hier // muss die Sättigungsstufe greifen, sonst käme die Suche nie an. let corrected = Contrast.legible(RGB(hex: 0x000000), on: canvas) #expect(Contrast.ratio(corrected, canvas) >= Contrast.minimumForText) } } @Suite("Akzentpalette") struct AccentPaletteTests { @Test("Jede kuratierte Farbe ist ohne Korrektur lesbar") func curatedColorsAreLegibleAsIs() { // Das ist der eigentliche Zweck des Kuratierens. Fällt eine neue Farbe // hier durch, gehört sie nicht in die Palette — nicht in die // Korrektur. for option in AccentOption.palette { let ratio = Contrast.ratio(option.color, FlixyColor.canvas) #expect( ratio >= Contrast.minimumForText, "\(option.id) erreicht nur \(String(format: "%.2f", ratio)):1" ) } } @Test("Die Kennungen sind eindeutig") func identifiersAreUnique() { // Sie dienen als Schlüssel in den Voreinstellungen; ein Duplikat // hieße, dass eine Auswahl still eine andere überschreibt. let identifiers = AccentOption.palette.map(\.id) #expect(Set(identifiers).count == identifiers.count) } @Test("Keine kuratierte Farbe nimmt die Kennung der eigenen Farbe ein") func paletteDoesNotCollideWithCustom() { #expect(!AccentOption.palette.contains { $0.id == AccentOption.customIdentifier }) } } @Suite("Farbwerte") struct RGBTests { @Test( "Hex-Eingaben werden gelesen", arguments: [ ("#1DB954", 0x1DB954), ("1db954", 0x1DB954), (" #FFB224 ", 0xFFB224), // Kurzform: #ABC meint #AABBCC. ("#ABC", 0xAABBCC), ] ) func validHexStringsParse(input: String, expected: Int) { #expect(RGB(string: input) == RGB(hex: UInt32(expected))) } @Test( "Unbrauchbare Eingaben werden abgewiesen", arguments: ["", "#", "xyz", "#12345", "#GGGGGG", "1234567"] ) func invalidHexStringsFail(input: String) { #expect(RGB(string: input) == nil) } @Test("Hex hin und zurück ergibt denselben Wert") func hexRoundTrips() { for option in AccentOption.palette { #expect(RGB(string: option.color.hexString) == option.color) } } }