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>
110 lines
4.1 KiB
Swift
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)
|
|
}
|
|
}
|