import Testing import CoreGraphics import AppKit @testable import MetricsProvider // SF Symbols kennt fünf Füllstufen und keine Blitz-Varianten außer bei 100 %. // Beides passt nicht zu dem, was macOS selbst zeigt: eine stufenlose Füllung // und einen Blitz **im** Akku. Also selbst gezeichnet. @Suite("Akkuglyph") struct BatteryGlyphTests { private let inner: CGFloat = 20 @Test("Die Füllung folgt dem Ladestand stufenlos") func fillIsProportional() { // Der eigentliche Gewinn gegenüber fünf festen Stufen: 60 % sieht aus // wie 60 % und nicht wie 50 %. #expect(BatteryGlyph.fillWidth(charge: 0.5, inner: inner) == 10) #expect(BatteryGlyph.fillWidth(charge: 1.0, inner: inner) == 20) } @Test("Ein leerer Akku bleibt leer") func emptyStaysEmpty() { #expect(BatteryGlyph.fillWidth(charge: 0, inner: inner) == 0) } @Test("Ein fast leerer Akku ist trotzdem zu sehen") func nearlyEmptyIsStillVisible() { // Zwei Prozent ergäben 0,4 Punkte — unsichtbar, und damit // ununterscheidbar von „leer". Ein Rest bleibt stehen. let width = BatteryGlyph.fillWidth(charge: 0.02, inner: inner) #expect(width >= BatteryGlyph.minimumFill) } @Test("Über hundert Prozent läuft nichts über") func neverOverflows() { #expect(BatteryGlyph.fillWidth(charge: 2, inner: inner) == inner) #expect(BatteryGlyph.fillWidth(charge: -1, inner: inner) == 0) } @Test("Wenig Ladung gilt als kritisch") func lowIsFlagged() { // Dieselbe Schwelle wie in macOS: ab zwanzig Prozent abwärts wird die // Farbe zum Signal. #expect(BatteryGlyph.isLow(charge: 0.19)) #expect(!BatteryGlyph.isLow(charge: 0.21)) } @Test("Beim Laden gilt nichts als kritisch") func chargingIsNeverLow() { // Ein roter Akku, der gerade lädt, wäre eine Warnung vor etwas, das // sich schon erledigt. #expect(!BatteryGlyph.isLow(charge: 0.05, isCharging: true)) } @Test("Das Glyph hat die Maße der Menüleiste") func sizeFitsMenuBar() { // Breiter als hoch, wie ein Akku aussieht — und niedrig genug, dass es // in die 22 Punkte der Leiste passt. #expect(BatteryGlyph.size.width > BatteryGlyph.size.height) #expect(BatteryGlyph.size.height <= 14) } } // Am Netz, aber es tut sich nichts — weil das Ladelimit erreicht ist. Ohne // eigenes Zeichen sieht das genauso aus wie Akkubetrieb, und man fragt sich, // ob das Kabel wirklich steckt. @Suite("Ladezustand im Glyph") struct BatteryIndicatorTests { @Test("Beim Laden der Blitz") func chargingShowsBolt() { #expect(BatteryGlyph.indicator(isCharging: true, isPluggedIn: true) == .bolt) } @Test("Am Netz ohne Laden das Pausenzeichen") func pluggedButIdleShowsPause() { // Der Fall, um den es geht: Ladelimit erreicht. „Laden angehalten" ist // die Aussage — ein Stecker sagte nur „Kabel steckt", und das sieht man // am Kabel. #expect(BatteryGlyph.indicator(isCharging: false, isPluggedIn: true) == .paused) } @Test("Im Akkubetrieb gar nichts") func onBatteryShowsNothing() { #expect(BatteryGlyph.indicator(isCharging: false, isPluggedIn: false) == .none) } @Test("Laden schlägt alles andere") func chargingWins() { // Sollte der Netzzustand einmal widersprüchlich gemeldet werden, ist // „lädt" die Auskunft, die zählt. #expect(BatteryGlyph.indicator(isCharging: true, isPluggedIn: false) == .bolt) } @Test("Jedes Zeichen gibt es auch wirklich") func everyIndicatorResolves() { for indicator in BatteryGlyph.Indicator.allCases { guard let name = indicator.symbolName else { continue } #expect(NSImage(systemSymbolName: name, accessibilityDescription: nil) != nil, "\(name) gibt es nicht") } } } // Der Blitz ist ein Loch in der Füllung. Über dem gefüllten Teil liest man ihn, // über dem leeren ist es Transparenz auf Transparenz — bei wenig Ladung sitzt // er fast ganz im Leeren und verschwindet. // // Also geteilt zeichnen: über der Füllung ausgestanzt, daneben ausgemalt. @Suite("Blitz über Füllung und Leerraum") struct BatteryBoltSplitTests { private let inner = CGRect(x: 10, y: 0, width: 20, height: 8) @Test("Bei voller Ladung liegt alles auf der Füllung") func fullIsAllFilled() { let split = BatteryGlyph.boltSplit(charge: 1, inner: inner) #expect(split.filled == inner) #expect(split.empty.width == 0) } @Test("Bei leerem Akku liegt alles im Leerraum") func emptyIsAllEmpty() { let split = BatteryGlyph.boltSplit(charge: 0, inner: inner) #expect(split.filled.width == 0) #expect(split.empty == inner) } @Test("Dazwischen teilt sich der Blitz an der Kante der Füllung") func splitFollowsFill() { let split = BatteryGlyph.boltSplit(charge: 0.5, inner: inner) #expect(split.filled.maxX == split.empty.minX) #expect(split.filled.width + split.empty.width == inner.width) } @Test("Die beiden Teile stoßen aneinander, statt sich zu überlappen") func partsMeetWithoutOverlap() { // Sonst wird der Blitz an der Nahtstelle doppelt gezeichnet und sieht // dort dicker aus. Geprüft wird die Kante selbst und nicht // `intersects`: das meldet bei einem Teil der Breite null trotzdem // eine Überschneidung. for percent in stride(from: 0.0, through: 1.0, by: 0.05) { let split = BatteryGlyph.boltSplit(charge: percent, inner: inner) #expect(split.filled.maxX <= split.empty.minX + 0.001) } } @Test("Zusammen decken sie den ganzen Innenraum") func partsCoverEverything() { for percent in stride(from: 0.0, through: 1.0, by: 0.05) { let split = BatteryGlyph.boltSplit(charge: percent, inner: inner) #expect(abs(split.filled.width + split.empty.width - inner.width) < 0.001) } } } @Suite("Blitz beim Laden") struct BatteryBoltFadeTests { @Test("Die Kurve läuft von Weiß nach Schwarz und zurück") func fadeIsAPingPong() { #expect(BatteryGlyph.fadeAmount(phase: 0) == 0) #expect(BatteryGlyph.fadeAmount(phase: 0.5) == 1) #expect(abs(BatteryGlyph.fadeAmount(phase: 1) - 0) < 0.0001) } @Test("Sie ist an den Umkehrpunkten weich") func endsAreSmooth() { // Ein linearer Ping-Pong knickt an den Enden sichtbar. Der Kosinus // wird dort flach — das Faden wirkt dadurch ruhig statt gehetzt. let nearStart = BatteryGlyph.fadeAmount(phase: 0.02) let nearMiddle = BatteryGlyph.fadeAmount(phase: 0.5) - BatteryGlyph.fadeAmount(phase: 0.48) #expect(nearStart < nearMiddle) } @Test("Die Phase läuft rundherum weiter") func phaseWrapsAround() { // Sonst gäbe es nach einer Umdrehung einen Sprung. #expect(abs(BatteryGlyph.fadeAmount(phase: 1.25) - BatteryGlyph.fadeAmount(phase: 0.25)) < 0.0001) } @Test("Die Enden sind wirklich Schwarz und Weiß") func extremesAreBlackAndWhite() { // Genau darum geht es: bei wenig Ladung sitzt der Blitz im leeren Teil // und braucht Weiß, über der Füllung braucht er Schwarz. Ein Faden // dazwischen ist in beiden Fällen irgendwann lesbar. #expect(BatteryGlyph.boltWhiteness(phase: 0) == 1) #expect(BatteryGlyph.boltWhiteness(phase: 0.5) == 0) } }