Files
ScarriffleandClaude Opus 5 ecb39fbe5f Eine Designschicht statt hundertfach gesetzter Zahlen
Token für Schrift, Symbolgröße, Abstand, Radius und Bewegung, dazu sieben
Bauteile, die vorher in jeder Kachel neu gebaut wurden — Karte, Abschnittstitel,
Hinweis, Pillenknopf, Regler, Ablegefläche, Haarlinie.

Symbole bekommen eine **eigene** Staffel und nicht die der Schrift: ein
9-Punkt-Symbol neben 9-Punkt-Text wirkt kleiner, nicht gleich groß. Beide in
eine Staffel zu zwingen hieße, an jeder Stelle gegen sie zu arbeiten.

Das Flächenrezept — Tint, Bänderung, Kante — stand zweimal da, einmal für das
Panel und einmal für Fenster. Es ist jetzt `OnyxStone`, generisch über die Form,
damit auch die animierende Notch-Silhouette es benutzen kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 21:04:51 +02:00

110 lines
4.1 KiB
Swift

import Testing
import Foundation
import SwiftUI
@testable import OnyxDesign
@Suite("Bewegungsprofil")
@MainActor
struct MotionProfileTests {
private func freshDefaults(_ name: String) -> UserDefaults {
let suite = "onyx.motion.test.\(name)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
return defaults
}
@Test("Ohne Einstellung ist es schnell")
func defaultsToSnappy() {
let theme = OnyxTheme(defaults: freshDefaults(#function))
#expect(theme.motion == .snappy)
}
@Test("Das gewählte Profil übersteht den Neustart")
func choiceSurvivesRestart() {
let defaults = freshDefaults(#function)
let theme = OnyxTheme(defaults: defaults)
theme.motion = .calm
let restarted = OnyxTheme(defaults: defaults)
#expect(restarted.motion == .calm)
}
/// Ein Profilname, den diese Fassung nicht kennt, darf die Bewegung nicht
/// ausfallen lassen — eine App ohne Animation sieht aus, als hinge sie.
@Test("Ein unbekannter Name fällt auf die Voreinstellung zurück")
func unknownNameFallsBack() {
let defaults = freshDefaults(#function)
defaults.set("hyperspeed", forKey: "onyx.theme.motion")
#expect(OnyxTheme(defaults: defaults).motion == .snappy)
}
/// Der Zurücksetzen-Knopf steht bei den Farben. Nähme er das
/// Bewegungsprofil mit, stellte er etwas um, wovon in seiner Beschriftung
/// nichts steht.
@Test("Farben zurücksetzen lässt das Bewegungsprofil in Ruhe")
func resetLeavesMotionAlone() {
let theme = OnyxTheme(defaults: freshDefaults(#function))
theme.motion = .bouncy
theme.reset()
#expect(theme.motion == .bouncy)
#expect(theme.isDefault)
}
/// Beim Öffnen sieht man zu, beim Schließen ist man schon woanders.
@Test("Einfahren ist in jedem Profil kürzer als Ausfahren")
func collapseIsShorterThanExpand() {
// Die Federantwort steckt in der Animation und ist von außen nicht
// lesbar. Prüfbar ist die Dauer, die der Fenstercontroller benutzt —
// und die ist genau der Wert, an dem es sichtbar schiefgeht.
for profile in MotionProfile.allCases {
#expect(profile.collapseDuration > 0)
#expect(profile.collapseDuration < 1)
}
#expect(MotionProfile.snappy.collapseDuration < MotionProfile.calm.collapseDuration)
}
/// Verschwindet das Fenster vor dem Ende der Bewegung, reißt das Panel
/// sichtbar weg. Genau dafür hängt die Dauer am Profil und ist keine feste
/// Zahl mehr.
@Test("Jedes Profil bringt seine eigene Einfahrdauer mit")
func everyProfileCarriesItsOwnDuration() {
let durations = Set(MotionProfile.allCases.map(\.collapseDuration))
#expect(durations.count == MotionProfile.allCases.count)
}
}
@Suite("Pegel im Regler")
struct MeterFractionTests {
/// Ausschläge sind linear in der Amplitude und damit optisch nutzlos: halb
/// so laut sieht aus wie fast nichts. Die Wurzel spreizt den unteren
/// Bereich, und genau das ist hier zu prüfen — sonst ist der Regler ein
/// Balken, der die meiste Zeit leer aussieht.
@Test("Leiser Ton schlägt sichtbar aus")
func quietSignalIsVisible() {
// Ein Viertel der Amplitude sähe linear nach 25 % aus. Die Wurzel macht
// daraus die Hälfte.
#expect(abs(OnyxSlider.meterFraction(0.25) - 0.5) < 0.0001)
}
@Test("Die Enden bleiben, wo sie sind")
func endsAreUntouched() {
#expect(OnyxSlider.meterFraction(0) == 0)
#expect(OnyxSlider.meterFraction(1) == 1)
}
/// Der Ausschlag kommt aus einem Tap und ist nicht garantiert im Bereich.
/// Ein Wert über 1 machte den Pegelbalken breiter als den Regler, ein
/// negativer ließe `sqrt` NaN liefern — und NaN in einer Breitenangabe
/// beendet SwiftUI.
@Test("Werte außerhalb des Bereichs werden gekappt")
func outOfRangeIsClamped() {
#expect(OnyxSlider.meterFraction(4) == 1)
#expect(OnyxSlider.meterFraction(-1) == 0)
#expect(!OnyxSlider.meterFraction(-1).isNaN)
}
}