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