„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>
197 lines
7.5 KiB
Swift
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)
|
|
}
|
|
}
|