moodlit-native/Tests/UnitTests/DomainCodingTests.swift
till 07bc650170 μ-7.1: Funktionierende Hex-Color-Picker + 11 Unit-Tests
CreateMoodSheet's Hex-Picker war broken — `hexColors`-Getter retournierte
hardcoded `#7c3aed` für jede Farbe statt der gewählten. Custom-Moods
landeten so alle als 3× lila in der DB.

Fix:
- HexColor.swift: `Color.toHexString()` über PlatformColor (UIColor
  iOS / NSColor macOS) — sRGB-Roundtrip mit Komponenten-Extraktion.
- CreateMoodSheet: `[String]` (Hex) als SOT statt `[Color]`. ColorPicker
  binded über konvertierende `Binding<Color>` (Get: Hex→Color, Set:
  Color→Hex). Zusätzlich TextField pro Farbe für direkten Hex-Input
  (lowercase, mit/ohne `#`, validiert auf 6/8 Hex-Chars).
- `+ Farbe hinzufügen` generiert random Hex statt fixem Lila.

Tests/UnitTests/ neu:
- Decode-Roundtrip Mood (mit + ohne userId), MoodSequence, Preferences
  gegen API-Wire-Format aus moodlit-api Routen.
- AnimationSpeed-Enum akzeptiert alle 3 Wire-Werte (slow/normal/fast).
- DefaultMoods.all hat 24 Einträge, alle isPreset, unique IDs, deckt
  bekannte Slugs ab (fire, breath, ocean, sunrise, sunset, matrix),
  alle referenzierten Animation-Werte sind im Enum.
- HexColor.Color(hex:) smoke (#prefix, lowercase/uppercase, 8-char).

11/11 Tests grün via xcodebuild iOS-Sim. project.yml um
MoodlitNativeUnitTests-Target (bundle.unit-test, iOS-only, Bundle-ID
ev.mana.moodlit.tests) erweitert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:10:31 +02:00

172 lines
4.9 KiB
Swift

import Foundation
import SwiftUI
import Testing
@testable import MoodlitNative
/// Decode-Roundtrip gegen das Wire-Format der moodlit-api.
/// SOT: `Code/moodlit/apps/api/src/routes/{moods,sequences,preferences}.ts`.
///
/// Wenn diese Tests rot werden, hat sich entweder die API-Antwort-
/// Form geändert oder unsere Swift-Codable-Annotationen beides
/// muss sofort gefixt werden, sonst crasht der App-Mount.
struct DomainCodingTests {
// MARK: - Mood
@Test func moodDecodesFromApiResponse() throws {
let json = """
{
"id": "abc-123",
"userId": "user-xyz",
"name": "Mein Mood",
"colors": ["#ff6b35", "#ff4500"],
"animation": "candle",
"isPreset": false,
"createdAt": "2026-05-18T10:00:00.000Z",
"updatedAt": "2026-05-18T10:00:00.000Z"
}
""".data(using: .utf8)!
let mood = try JSONDecoder().decode(Mood.self, from: json)
#expect(mood.id == "abc-123")
#expect(mood.userId == "user-xyz")
#expect(mood.colors == ["#ff6b35", "#ff4500"])
#expect(mood.animation == .candle)
#expect(mood.isPreset == false)
}
@Test func moodDecodesWithNullUserId() throws {
// Presets kommen mit `userId: null` aus dem Server, falls je
// einer den `/presets`-Endpoint hinzufügt robust decoden.
let json = """
{
"id": "fire",
"userId": null,
"name": "Fire",
"colors": ["#ff6b35"],
"animation": "candle",
"isPreset": true,
"createdAt": "1970-01-01T00:00:00.000Z",
"updatedAt": "1970-01-01T00:00:00.000Z"
}
""".data(using: .utf8)!
let mood = try JSONDecoder().decode(Mood.self, from: json)
#expect(mood.userId == nil)
#expect(mood.isPreset == true)
}
// MARK: - MoodSequence
@Test func sequenceDecodesFromApiResponse() throws {
let json = """
{
"id": "seq-1",
"userId": "user-xyz",
"name": "Entspannen",
"moodIds": ["fire", "breath", "ocean"],
"durationSec": 30,
"transitionSec": 2,
"createdAt": "2026-05-18T10:00:00.000Z",
"updatedAt": "2026-05-18T10:00:00.000Z"
}
""".data(using: .utf8)!
let seq = try JSONDecoder().decode(MoodSequence.self, from: json)
#expect(seq.id == "seq-1")
#expect(seq.moodIds == ["fire", "breath", "ocean"])
#expect(seq.durationSec == 30)
#expect(seq.transitionSec == 2)
}
// MARK: - Preferences
@Test func preferencesDecodesFromApiResponse() throws {
let json = """
{
"userId": "user-xyz",
"animationSpeed": "normal",
"brightness": 80,
"autoTimerMinutes": 0,
"autoMoodSwitch": false,
"autoMoodSwitchInterval": 5,
"favoriteIds": ["fire", "ocean"],
"createdAt": "2026-05-18T10:00:00.000Z",
"updatedAt": "2026-05-18T10:00:00.000Z"
}
""".data(using: .utf8)!
let prefs = try JSONDecoder().decode(Preferences.self, from: json)
#expect(prefs.userId == "user-xyz")
#expect(prefs.animationSpeed == .normal)
#expect(prefs.brightness == 80)
#expect(prefs.favoriteIds == ["fire", "ocean"])
}
@Test func preferencesAnimationSpeedAcceptsAllThreeValues() async throws {
for value in ["slow", "normal", "fast"] {
let json = """
{
"userId": "u", "animationSpeed": "\(value)",
"brightness": 100, "autoTimerMinutes": 0,
"autoMoodSwitch": false, "autoMoodSwitchInterval": 5,
"favoriteIds": [],
"createdAt": "1970-01-01T00:00:00.000Z",
"updatedAt": "1970-01-01T00:00:00.000Z"
}
""".data(using: .utf8)!
let prefs = try JSONDecoder().decode(Preferences.self, from: json)
let decoded = prefs.animationSpeed.rawValue
#expect(decoded == value)
}
}
// MARK: - Defaults
@Test func defaultMoodsHas24Presets() throws {
#expect(DefaultMoods.all.count == 24)
}
@Test func defaultMoodsAreAllPresets() throws {
let allArePresets = DefaultMoods.all.allSatisfy { $0.isPreset }
#expect(allArePresets)
}
@Test func defaultMoodsHaveUniqueIds() throws {
let ids = DefaultMoods.all.map { $0.id }
let unique = Set(ids).count
#expect(unique == ids.count)
}
@Test func defaultMoodsContainsKnownSlugs() throws {
// Diese Slugs sind Wire-IDs, Sequences referenzieren sie.
// Ein Rebrand würde DB-Daten zerschießen.
for slug in ["fire", "breath", "ocean", "sunrise", "sunset", "matrix"] {
#expect(DefaultMoods.byId(slug) != nil, "Missing preset slug: \(slug)")
}
}
@Test func defaultMoodsCoverAllAnimationsUsedByPresets() throws {
// Jedes Preset muss eine Animation referenzieren, die im Enum
// definiert ist Swift's Enum-Decoding würde sonst beim Lift
// neuer Animation-Types brechen.
let animationsInUse = Set(DefaultMoods.all.map { $0.animation })
let isEmpty = animationsInUse.isEmpty
#expect(!isEmpty)
// Sanity: alle in-use sind valide raw-values
for anim in animationsInUse {
let roundtripped = AnimationType(rawValue: anim.rawValue)
#expect(roundtripped == anim)
}
}
// MARK: - HexColor
@Test func hexColorParsesHashPrefix() throws {
// Smoke: keine Crashes bei verschiedenen Input-Formaten
_ = Color(hex: "#ff0000")
_ = Color(hex: "ff0000")
_ = Color(hex: "#FF0000")
_ = Color(hex: "#ff0000ff")
}
}