Files
onyx/Packages/OnyxKit/Tests/MetricsProviderTests/BatteryGlyphTests.swift
Scarriffle 20963f53e2 Am Netz ohne Laden: Pausenzeichen statt Stecker
„Laden angehalten" ist die Aussage. Ein Stecker sagt nur „Kabel steckt",
und das sieht man am Kabel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:00:44 +02:00

197 lines
7.5 KiB
Swift

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)
}
}