Statt Ember-Rot ist Mint (#3DD68C) die Voreinstellung. Teal und Lime bleiben als Alternativen in der Palette. Gesucht wird sie über die Kennung, nicht über den Index: sonst würde ein Umsortieren der Palette die Standardfarbe still mitändern — und zwar nur für Neuinstallationen, was beim Suchen besonders unangenehm ist. Drei Tests halten das fest. Wer bereits eine Farbe gewählt hat, behält sie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
5.4 KiB
Swift
150 lines
5.4 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Suite("Standard-Akzentfarbe")
|
|
struct DefaultAccentTests {
|
|
@Test("Die Voreinstellung ist Mint")
|
|
func defaultIsMint() {
|
|
// Festgehalten, damit ein Umsortieren der Palette sie nicht still
|
|
// verändert. Wer sie bewusst wechselt, ändert auch diesen Test.
|
|
#expect(AccentOption.default.id == "mint")
|
|
#expect(AccentOption.default.color == RGB(hex: 0x3DD68C))
|
|
}
|
|
|
|
@Test("Die Voreinstellung steht in der Palette")
|
|
func defaultIsPartOfThePalette() {
|
|
#expect(AccentOption.palette.contains(AccentOption.default))
|
|
}
|
|
|
|
@Test("Die Voreinstellung braucht keine Kontrastkorrektur")
|
|
func defaultNeedsNoCorrection() {
|
|
let corrected = Contrast.legible(AccentOption.default.color, on: FlixyColor.canvas)
|
|
#expect(corrected == AccentOption.default.color)
|
|
}
|
|
}
|