Files
Flaro/Packages/FlixyKit/Tests/FlixyDesignTests/ContrastTests.swift
T
GuidoandClaude Opus 5 1ed64bf166 Phase 0: Projektgerüst, Design-Tokens und Navigation
Legt das Fundament für den Plex-Client an:

- XcodeGen-Projekt nach dem Muster von Onyx (com.scarriffleservices,
  strict concurrency, Projektdatei nicht eingecheckt)
- FlixyKit mit vier Modulen: PlexKit, FlixyDesign, FlixyPlayback,
  FlixyDownloads. macOS steht als Plattform mit drin, damit die
  Logik-Tests ohne Simulator laufen.
- FlixyDesign: dunkle Farb-Tokens, Schriftskala, Poster- und
  Querformat-Kacheln, Aufmacher-Bild. Die Akzentfarbe fließt über die
  Umgebung und wird per WCAG-Rechnung lesbar gehalten.
- PlexKit: Kopfzeilen-Identität, Transport-Protokoll, Fehlerarten und
  die Medienmodelle samt nachsichtigem Decoder.
- FlixyPlayback: VideoPlayerEngine-Protokoll und PlaybackDecider — die
  eine Funktion, an der Phase 2 das Transcoding andockt.
- Ausfahrbare Seitenleiste auf dem iPhone, Split-View auf dem iPad.
- 45 Tests, alle grün.

VLCKit 4.0.0-a24 ist vorab geprüft: baut über SPM unter Swift 6 mit
strict concurrency. Die iOS-Slice ist mit 239 MB allerdings gewichtig,
deshalb bekommt sie in Phase 1 ein eigenes Target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:04:40 +02:00

128 lines
4.6 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)
}
}
}