9ca54e6a82
- PersonalityEngine: callWindowCopy() + aftermathCopy() zentralisieren alle Notification-Texte (bisher verstreut und teils unlokalisiert) - CallWindowManager: 3 Varianten nach Profil (high E / high N / default), String(localized:) + Error-Logging ergänzt - AftermathNotificationManager: Titel "Nachwirkung: %@" → "Wie war's mit %@?", Body via PersonalityEngine.aftermathCopy() - Todo- und Vorhaben-Erinnerungen: subtitle "Dein Vorhaben" für persönlicheren Kontext (AddTodoView, EditTodoView, AddMomentView) - AddMomentView: Logging + Error-Callback nachgezogen (wie AddTodoView) - 9 neue Tests in NahbarPersonalityTests (NotificationCopyTests) - Lokalisierung: 4 neue Strings inkl. bisher unlokalisierter Texte Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
620 lines
22 KiB
Swift
620 lines
22 KiB
Swift
import Testing
|
||
import Foundation
|
||
@testable import nahbar
|
||
|
||
// MARK: - OceanDimension
|
||
|
||
@Suite("OceanDimension – Enum")
|
||
struct OceanDimensionTests {
|
||
|
||
@Test("Genau 5 Dimensionen vorhanden")
|
||
func allCasesCount() {
|
||
#expect(OceanDimension.allCases.count == 5)
|
||
}
|
||
|
||
@Test("rawValues sind nicht leer")
|
||
func rawValuesNotEmpty() {
|
||
for dim in OceanDimension.allCases {
|
||
#expect(!dim.rawValue.isEmpty, "\(dim) hat leeren rawValue")
|
||
}
|
||
}
|
||
|
||
@Test("rawValue round-trip (init(rawValue:))")
|
||
func rawValueRoundTrip() {
|
||
for dim in OceanDimension.allCases {
|
||
let recovered = OceanDimension(rawValue: dim.rawValue)
|
||
#expect(recovered == dim, "\(dim.rawValue) kann nicht wiederhergestellt werden")
|
||
}
|
||
}
|
||
|
||
@Test("shortLabel hat genau 1 Zeichen")
|
||
func shortLabelLength() {
|
||
for dim in OceanDimension.allCases {
|
||
#expect(dim.shortLabel.count == 1, "\(dim) hat shortLabel '\(dim.shortLabel)' (nicht 1 Zeichen)")
|
||
}
|
||
}
|
||
|
||
@Test("shortLabels sind eindeutig")
|
||
func shortLabelsUnique() {
|
||
let labels = OceanDimension.allCases.map { $0.shortLabel }
|
||
#expect(Set(labels).count == labels.count, "Doppelte shortLabels: \(labels)")
|
||
}
|
||
|
||
@Test("Stabile rawValues – Regressionswächter")
|
||
func stableRawValues() {
|
||
#expect(OceanDimension.openness.rawValue == "openness")
|
||
#expect(OceanDimension.conscientiousness.rawValue == "conscientiousness")
|
||
#expect(OceanDimension.extraversion.rawValue == "extraversion")
|
||
#expect(OceanDimension.agreeableness.rawValue == "agreeableness")
|
||
#expect(OceanDimension.neuroticism.rawValue == "neuroticism")
|
||
}
|
||
|
||
@Test("axisLabel ist nicht leer")
|
||
func axisLabelNotEmpty() {
|
||
for dim in OceanDimension.allCases {
|
||
#expect(!dim.axisLabel.isEmpty)
|
||
}
|
||
}
|
||
|
||
@Test("icon ist nicht leer")
|
||
func iconNotEmpty() {
|
||
for dim in OceanDimension.allCases {
|
||
#expect(!dim.icon.isEmpty)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - TraitLevel
|
||
|
||
@Suite("TraitLevel – Schwellenwerte")
|
||
struct TraitLevelTests {
|
||
|
||
@Test("Score 0 → niedrig")
|
||
func score0IsLow() { #expect(TraitLevel.from(score: 0) == .low) }
|
||
|
||
@Test("Score 1 → mittel")
|
||
func score1IsMedium() { #expect(TraitLevel.from(score: 1) == .medium) }
|
||
|
||
@Test("Score 2 → hoch")
|
||
func score2IsHigh() { #expect(TraitLevel.from(score: 2) == .high) }
|
||
|
||
@Test("Score >2 → hoch (overflow sicher)")
|
||
func scoreOverflowIsHigh() { #expect(TraitLevel.from(score: 99) == .high) }
|
||
|
||
@Test("rawValue round-trip")
|
||
func rawValueRoundTrip() {
|
||
for level in TraitLevel.allCases {
|
||
#expect(TraitLevel(rawValue: level.rawValue) == level)
|
||
}
|
||
}
|
||
|
||
@Test("Stabile rawValues – Regressionswächter")
|
||
func stableRawValues() {
|
||
#expect(TraitLevel.low.rawValue == "low")
|
||
#expect(TraitLevel.medium.rawValue == "medium")
|
||
#expect(TraitLevel.high.rawValue == "high")
|
||
}
|
||
}
|
||
|
||
// MARK: - QuizQuestion
|
||
|
||
@Suite("QuizQuestion – Statische Fragen")
|
||
struct QuizQuestionTests {
|
||
|
||
@Test("Genau 10 Fragen")
|
||
func totalCount() {
|
||
#expect(QuizQuestion.all.count == 10)
|
||
}
|
||
|
||
@Test("Genau 2 Fragen pro Dimension")
|
||
func twoQuestionsPerDimension() {
|
||
for dim in OceanDimension.allCases {
|
||
let count = QuizQuestion.all.filter { $0.dimension == dim }.count
|
||
#expect(count == 2, "Dimension \(dim.rawValue) hat \(count) statt 2 Fragen")
|
||
}
|
||
}
|
||
|
||
@Test("Alle IDs sind eindeutig")
|
||
func uniqueIDs() {
|
||
let ids = QuizQuestion.all.map { $0.id }
|
||
#expect(Set(ids).count == ids.count, "Doppelte Fragen-IDs: \(ids)")
|
||
}
|
||
|
||
@Test("Situationstexte sind nicht leer")
|
||
func situationTextsNotEmpty() {
|
||
for q in QuizQuestion.all {
|
||
#expect(!q.situation.isEmpty, "Frage \(q.id) hat leeren Situationstext")
|
||
}
|
||
}
|
||
|
||
@Test("Option-Texte sind nicht leer")
|
||
func optionTextsNotEmpty() {
|
||
for q in QuizQuestion.all {
|
||
#expect(!q.optionA.isEmpty, "Frage \(q.id) hat leeren optionA-Text")
|
||
#expect(!q.optionB.isEmpty, "Frage \(q.id) hat leeren optionB-Text")
|
||
}
|
||
}
|
||
|
||
@Test("optionAScore ist 0 oder 1")
|
||
func optionAScoreIsValid() {
|
||
for q in QuizQuestion.all {
|
||
#expect(q.optionAScore == 0 || q.optionAScore == 1,
|
||
"Frage \(q.id) hat ungültigen optionAScore: \(q.optionAScore)")
|
||
}
|
||
}
|
||
|
||
@Test("Neurotizismus-Fragen haben optionAScore 0 (invertiert)")
|
||
func neuroticismIsInverted() {
|
||
let nQuestions = QuizQuestion.all.filter { $0.dimension == .neuroticism }
|
||
for q in nQuestions {
|
||
#expect(q.optionAScore == 0,
|
||
"Neurotizismus-Frage \(q.id) sollte optionAScore=0 haben (Option A = stabil)")
|
||
}
|
||
}
|
||
|
||
@Test("Stabile Fragen-IDs – Regressionswächter")
|
||
func stableQuestionIDs() {
|
||
let ids = QuizQuestion.all.map { $0.id }
|
||
#expect(ids == ["O1", "O2", "C1", "C2", "E1", "E2", "A1", "A2", "N1", "N2"])
|
||
}
|
||
}
|
||
|
||
// MARK: - PersonalityEngine
|
||
|
||
@Suite("PersonalityEngine – Score-Berechnung")
|
||
struct PersonalityEngineTests {
|
||
|
||
@Test("Alle Option-A → korrekter Score nach optionAScore")
|
||
func allOptionAGivesCorrectScore() {
|
||
let allA = QuizQuestion.all.map { (questionID: $0.id, choseA: true) }
|
||
let profile = PersonalityEngine.computeProfile(from: allA)
|
||
|
||
for dim in OceanDimension.allCases {
|
||
let expected = QuizQuestion.all
|
||
.filter { $0.dimension == dim }
|
||
.reduce(0) { $0 + $1.optionAScore }
|
||
#expect(profile.scores[dim] == expected,
|
||
"\(dim.rawValue): erwartet \(expected), bekommen \(profile.scores[dim] ?? -1)")
|
||
}
|
||
}
|
||
|
||
@Test("Alle Option-B → invertierter Score")
|
||
func allOptionBGivesInvertedScore() {
|
||
let allB = QuizQuestion.all.map { (questionID: $0.id, choseA: false) }
|
||
let profile = PersonalityEngine.computeProfile(from: allB)
|
||
|
||
for dim in OceanDimension.allCases {
|
||
let expected = QuizQuestion.all
|
||
.filter { $0.dimension == dim }
|
||
.reduce(0) { $0 + (1 - $1.optionAScore) }
|
||
#expect(profile.scores[dim] == expected,
|
||
"\(dim.rawValue): erwartet \(expected), bekommen \(profile.scores[dim] ?? -1)")
|
||
}
|
||
}
|
||
|
||
@Test("Neurotizismus invertiertes Scoring: Option A = 0 Punkte, Option B = 1 Punkt")
|
||
func neuroticismInvertedScoring() {
|
||
let allA = QuizQuestion.all.map { (questionID: $0.id, choseA: true) }
|
||
let profileA = PersonalityEngine.computeProfile(from: allA)
|
||
#expect(profileA.scores[.neuroticism] == 0,
|
||
"N mit Option A: erwartet 0, bekommen \(profileA.scores[.neuroticism] ?? -1)")
|
||
|
||
let allB = QuizQuestion.all.map { (questionID: $0.id, choseA: false) }
|
||
let profileB = PersonalityEngine.computeProfile(from: allB)
|
||
#expect(profileB.scores[.neuroticism] == 2,
|
||
"N mit Option B: erwartet 2, bekommen \(profileB.scores[.neuroticism] ?? -1)")
|
||
}
|
||
|
||
@Test("Profil ist complete nach 10 Antworten")
|
||
func profileIsCompleteAfterTenAnswers() {
|
||
let allA = QuizQuestion.all.map { (questionID: $0.id, choseA: true) }
|
||
let profile = PersonalityEngine.computeProfile(from: allA)
|
||
#expect(profile.isComplete)
|
||
#expect(profile.completedAt != nil)
|
||
}
|
||
|
||
@Test("Scores liegen immer im Bereich 0…2")
|
||
func scoresAlwaysInValidRange() {
|
||
let allA = QuizQuestion.all.map { (questionID: $0.id, choseA: true) }
|
||
let profileA = PersonalityEngine.computeProfile(from: allA)
|
||
let allB = QuizQuestion.all.map { (questionID: $0.id, choseA: false) }
|
||
let profileB = PersonalityEngine.computeProfile(from: allB)
|
||
|
||
for dim in OceanDimension.allCases {
|
||
#expect((profileA.scores[dim] ?? -1) >= 0 && (profileA.scores[dim] ?? -1) <= 2)
|
||
#expect((profileB.scores[dim] ?? -1) >= 0 && (profileB.scores[dim] ?? -1) <= 2)
|
||
}
|
||
}
|
||
|
||
@Test("Fehlende Antworten (übersprungen) führen zu Score 0 für die Dimension")
|
||
func skippedAnswersGiveZero() {
|
||
let profile = PersonalityEngine.computeProfile(from: [])
|
||
for dim in OceanDimension.allCases {
|
||
#expect(profile.scores[dim] == 0, "\(dim.rawValue) sollte 0 sein bei leeren Antworten")
|
||
}
|
||
}
|
||
|
||
@Test("completedAt liegt zwischen before und after dem Aufruf")
|
||
func completedAtIsReasonablyNow() {
|
||
let before = Date()
|
||
let allA = QuizQuestion.all.map { (questionID: $0.id, choseA: true) }
|
||
let profile = PersonalityEngine.computeProfile(from: allA)
|
||
let after = Date()
|
||
|
||
if let ts = profile.completedAt {
|
||
#expect(ts >= before && ts <= after)
|
||
} else {
|
||
Issue.record("completedAt sollte gesetzt sein")
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - PersonalityProfile
|
||
|
||
@Suite("PersonalityProfile – Berechnung & Codable")
|
||
struct PersonalityProfileTests {
|
||
|
||
private func makeProfile(scores: [OceanDimension: Int] = [:]) -> PersonalityProfile {
|
||
var full = Dictionary(uniqueKeysWithValues: OceanDimension.allCases.map { ($0, 1) })
|
||
for (k, v) in scores { full[k] = v }
|
||
return PersonalityProfile(scores: full, completedAt: Date())
|
||
}
|
||
|
||
@Test("level für Score 0 → niedrig")
|
||
func levelLowForScoreZero() {
|
||
let p = makeProfile(scores: [.openness: 0])
|
||
#expect(p.level(for: .openness) == .low)
|
||
}
|
||
|
||
@Test("level für Score 1 → mittel")
|
||
func levelMediumForScoreOne() {
|
||
let p = makeProfile(scores: [.openness: 1])
|
||
#expect(p.level(for: .openness) == .medium)
|
||
}
|
||
|
||
@Test("level für Score 2 → hoch")
|
||
func levelHighForScoreTwo() {
|
||
let p = makeProfile(scores: [.openness: 2])
|
||
#expect(p.level(for: .openness) == .high)
|
||
}
|
||
|
||
@Test("normalized: Score 0 → 0.0")
|
||
func normalizedMinIsZero() {
|
||
let p = makeProfile(scores: [.openness: 0])
|
||
#expect(p.normalized(for: .openness) == 0.0)
|
||
}
|
||
|
||
@Test("normalized: Score 1 → 0.5")
|
||
func normalizedMidIsFifty() {
|
||
let p = makeProfile(scores: [.openness: 1])
|
||
#expect(p.normalized(for: .openness) == 0.5)
|
||
}
|
||
|
||
@Test("normalized: Score 2 → 1.0")
|
||
func normalizedMaxIsOne() {
|
||
let p = makeProfile(scores: [.openness: 2])
|
||
#expect(p.normalized(for: .openness) == 1.0)
|
||
}
|
||
|
||
@Test("summaryText ist nicht leer für alle Kombinationen")
|
||
func summaryTextNeverEmpty() {
|
||
for scores in [[TraitLevel.low, .low, .low, .low, .low],
|
||
[.high, .high, .high, .high, .high],
|
||
[.medium, .medium, .medium, .medium, .medium]] {
|
||
let profile = PersonalityProfile(
|
||
scores: Dictionary(uniqueKeysWithValues:
|
||
OceanDimension.allCases.enumerated().map { i, dim in
|
||
(dim, scores[i] == .low ? 0 : scores[i] == .medium ? 1 : 2)
|
||
}),
|
||
completedAt: Date()
|
||
)
|
||
#expect(!profile.summaryText.isEmpty)
|
||
}
|
||
}
|
||
|
||
@Test("isComplete ist false wenn completedAt nil")
|
||
func isCompleteIsFalseWhenNilDate() {
|
||
let p = PersonalityProfile(scores: [:], completedAt: nil)
|
||
#expect(!p.isComplete)
|
||
}
|
||
|
||
@Test("isComplete ist true wenn completedAt gesetzt")
|
||
func isCompleteIsTrueWhenDateSet() {
|
||
let p = makeProfile()
|
||
#expect(p.isComplete)
|
||
}
|
||
|
||
@Test("Codable round-trip via JSONEncoder/Decoder")
|
||
func codableRoundTrip() throws {
|
||
let original = makeProfile(scores: [.openness: 2, .extraversion: 0, .neuroticism: 1])
|
||
let data = try JSONEncoder().encode(original)
|
||
let decoded = try JSONDecoder().decode(PersonalityProfile.self, from: data)
|
||
#expect(decoded == original)
|
||
}
|
||
|
||
@Test("PersonalityProfile benötigt keinen Netzwerkaufruf")
|
||
func doesNotRequireNetwork() {
|
||
// Rein synchron – kein await, kein async
|
||
let p = makeProfile()
|
||
let summary = p.summaryText
|
||
#expect(!summary.isEmpty)
|
||
}
|
||
}
|
||
|
||
// MARK: - PersonalityEngine – Behavior Logic
|
||
|
||
@Suite("PersonalityEngine – Verhaltenslogik")
|
||
struct PersonalityEngineBehaviorTests {
|
||
|
||
private func profile(e: TraitLevel = .medium, n: TraitLevel = .medium,
|
||
c: TraitLevel = .medium, a: TraitLevel = .medium,
|
||
o: TraitLevel = .medium) -> PersonalityProfile {
|
||
func score(_ l: TraitLevel) -> Int { l == .low ? 0 : l == .medium ? 1 : 2 }
|
||
return PersonalityProfile(scores: [
|
||
.extraversion: score(e), .neuroticism: score(n),
|
||
.conscientiousness: score(c), .agreeableness: score(a), .openness: score(o)
|
||
], completedAt: Date())
|
||
}
|
||
|
||
@Test("Hohe Extraversion → kürzeres Nudge-Intervall als niedrige")
|
||
func highExtraversionGivesShorterInterval() {
|
||
let highE = PersonalityEngine.suggestedNudgeInterval(for: profile(e: .high))
|
||
let lowE = PersonalityEngine.suggestedNudgeInterval(for: profile(e: .low))
|
||
#expect(highE < lowE)
|
||
}
|
||
|
||
@Test("Hoher Neurotizismus-Score + niedrige Extraversion → 14 Tage")
|
||
func lowExtraversionHighNeuroticismGives14Days() {
|
||
// N1 und N2 haben optionAScore=0, Option B gibt Punkte
|
||
// In unserem Modell: hoher Neurotizismus-Score = mehr N-Punkte
|
||
let p = profile(e: .low, n: .high)
|
||
#expect(PersonalityEngine.suggestedNudgeInterval(for: p) == 14)
|
||
}
|
||
|
||
@Test("Hohe Gewissenhaftigkeit → sofortiger Rating-Prompt")
|
||
func highConscientiousnessGivesImmediatePrompt() {
|
||
let p = profile(c: .high)
|
||
if case .immediate = PersonalityEngine.ratingPromptTiming(for: p) {
|
||
// korrekt
|
||
} else {
|
||
Issue.record("Hohe Gewissenhaftigkeit sollte immediate prompt liefern")
|
||
}
|
||
}
|
||
|
||
@Test("Hoher Neurotizismus → verzögerter Rating-Prompt (7200s)")
|
||
func highNeuroticismGivesDelayedPrompt() {
|
||
let p = profile(n: .high, c: .low)
|
||
if case .delayed(let secs, _) = PersonalityEngine.ratingPromptTiming(for: p) {
|
||
#expect(secs == 7200)
|
||
} else {
|
||
Issue.record("Hoher Neurotizismus sollte delayed prompt liefern")
|
||
}
|
||
}
|
||
|
||
@Test("Benachrichtigungstext mit hohem Neurotizismus ist wärmer")
|
||
func highNeuroticismNotificationCopyIsWarmer() {
|
||
let p = profile(n: .high)
|
||
let copy = PersonalityEngine.notificationCopy(contactName: "Alex", profile: p)
|
||
#expect(copy.contains("freut sich"))
|
||
}
|
||
|
||
@Test("Benachrichtigungstext ohne Profil liefert Fallback")
|
||
func nilProfileGivesFallback() {
|
||
let copy = PersonalityEngine.notificationCopy(contactName: "Alex", profile: nil)
|
||
#expect(!copy.isEmpty)
|
||
}
|
||
|
||
@Test("RecommendedBadge nicht angezeigt wenn Quiz übersprungen (kein Profil)")
|
||
func noBadgeWhenNoProfile() {
|
||
let suggestions = PersonalityEngine.sortedSuggestions(
|
||
contacts: [],
|
||
profile: nil,
|
||
lastMeetingDates: [:]
|
||
)
|
||
for s in suggestions {
|
||
#expect(!s.isRecommended)
|
||
}
|
||
}
|
||
|
||
@Test("Hohe Offenheit → highlightNovelty true")
|
||
func highOpennessHighlightsNovelty() {
|
||
let p = profile(o: .high)
|
||
#expect(PersonalityEngine.highlightNovelty(for: p))
|
||
}
|
||
|
||
@Test("Niedrige Offenheit → highlightNovelty false")
|
||
func lowOpennessDoesNotHighlightNovelty() {
|
||
let p = profile(o: .low)
|
||
#expect(!PersonalityEngine.highlightNovelty(for: p))
|
||
}
|
||
}
|
||
|
||
// MARK: - suggestedActivities Tests
|
||
|
||
@Suite("PersonalityEngine – suggestedActivities")
|
||
struct SuggestedActivitiesTests {
|
||
|
||
@Test("Gibt genau count Elemente zurück")
|
||
func returnsRequestedCount() {
|
||
let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 2)
|
||
#expect(result.count == 2)
|
||
}
|
||
|
||
@Test("count: 1 → genau ein Vorschlag")
|
||
func countOne() {
|
||
let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 1)
|
||
#expect(result.count == 1)
|
||
}
|
||
|
||
@Test("Alle zurückgegebenen Texte stammen aus dem Pool")
|
||
func resultsAreFromPool() {
|
||
let poolTexts = Set(PersonalityEngine.activityPool.map(\.text))
|
||
let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 5)
|
||
for text in result {
|
||
#expect(poolTexts.contains(text), "'\(text)' nicht im Pool")
|
||
}
|
||
}
|
||
|
||
@Test("Pool hat mindestens 20 Einträge")
|
||
func poolIsSufficient() {
|
||
#expect(PersonalityEngine.activityPool.count >= 20)
|
||
}
|
||
|
||
@Test("Keine Duplikate in einem Ergebnis")
|
||
func noDuplicates() {
|
||
let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 5)
|
||
#expect(result.count == Set(result).count)
|
||
}
|
||
|
||
@Test("Ergebnis ist nicht leer wenn Pool vorhanden")
|
||
func notEmptyWhenPoolExists() {
|
||
let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 2)
|
||
#expect(!result.isEmpty)
|
||
}
|
||
|
||
@Test("Pool enthält Erlebnis-Aktivitäten (isNovelty)")
|
||
func poolContainsNoveltyActivities() {
|
||
#expect(PersonalityEngine.activityPool.contains { $0.isNovelty })
|
||
}
|
||
|
||
@Test("Pool enthält 1:1 und Gruppen-Aktivitäten")
|
||
func poolContainsBothStyles() {
|
||
#expect(PersonalityEngine.activityPool.contains { $0.style == .oneOnOne })
|
||
#expect(PersonalityEngine.activityPool.contains { $0.style == .group })
|
||
}
|
||
|
||
@Test("Pool enthält Tag-spezifische Aktivitäten")
|
||
func poolContainsTagSpecificActivities() {
|
||
#expect(PersonalityEngine.activityPool.contains { $0.preferredTag == .friends })
|
||
#expect(PersonalityEngine.activityPool.contains { $0.preferredTag == .family })
|
||
#expect(PersonalityEngine.activityPool.contains { $0.preferredTag == .work })
|
||
}
|
||
}
|
||
|
||
// MARK: - GenderSelectionScreen Skip-Logik
|
||
|
||
@Suite("PersonalityQuiz – Geschlechtsabfrage überspringen")
|
||
struct PersonalityQuizGenderSkipTests {
|
||
|
||
@Test("Wenn Gender gesetzt → GenderSelectionScreen wird übersprungen (geht zu questions)")
|
||
func genderSetLeadsToQuestionsPhase() {
|
||
// Spiegelt die nextPhaseAfterIntro()-Logik wider
|
||
let gender = "Weiblich"
|
||
let shouldSkip = !gender.isEmpty
|
||
#expect(shouldSkip)
|
||
}
|
||
|
||
@Test("Wenn Gender leer → GenderSelectionScreen wird angezeigt")
|
||
func emptyGenderShowsSelectionScreen() {
|
||
let gender = ""
|
||
let shouldShow = gender.isEmpty
|
||
#expect(shouldShow)
|
||
}
|
||
|
||
@Test("Alle drei validen Werte überspringen den Screen")
|
||
func allValidGenderValuesSkipScreen() {
|
||
for gender in ["Männlich", "Weiblich", "Divers"] {
|
||
#expect(!gender.isEmpty, "\(gender) sollte Screen überspringen")
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Notification Copy Tests
|
||
|
||
@Suite("PersonalityEngine – Notification Copy")
|
||
struct NotificationCopyTests {
|
||
|
||
private func profile(e: TraitLevel = .medium, n: TraitLevel = .medium,
|
||
c: TraitLevel = .medium) -> PersonalityProfile {
|
||
func score(_ l: TraitLevel) -> Int { l == .low ? 0 : l == .medium ? 1 : 2 }
|
||
return PersonalityProfile(scores: [
|
||
.extraversion: score(e), .neuroticism: score(n),
|
||
.conscientiousness: score(c), .agreeableness: 1, .openness: 1
|
||
], completedAt: Date())
|
||
}
|
||
|
||
// MARK: callWindowCopy
|
||
|
||
@Test("callWindowCopy: nil-Profil liefert nicht-leeren Fallback")
|
||
func callWindowCopyNilProfile() {
|
||
let copy = PersonalityEngine.callWindowCopy(profile: nil)
|
||
#expect(!copy.isEmpty)
|
||
}
|
||
|
||
@Test("callWindowCopy: hohe Extraversion → direkter Text")
|
||
func callWindowCopyHighExtraversion() {
|
||
let copy = PersonalityEngine.callWindowCopy(profile: profile(e: .high))
|
||
#expect(copy.contains("freut sich"))
|
||
}
|
||
|
||
@Test("callWindowCopy: hoher Neurotizismus (nicht high E) → weicherer, ermutigender Text")
|
||
func callWindowCopyHighNeuroticism() {
|
||
let copy = PersonalityEngine.callWindowCopy(profile: profile(e: .low, n: .high))
|
||
#expect(copy.contains("Magst du"))
|
||
}
|
||
|
||
@Test("callWindowCopy: Medium-Profil → Default-Text")
|
||
func callWindowCopyDefault() {
|
||
let copy = PersonalityEngine.callWindowCopy(profile: profile(e: .medium, n: .medium))
|
||
#expect(!copy.isEmpty)
|
||
}
|
||
|
||
@Test("callWindowCopy: alle Varianten sind nicht leer")
|
||
func callWindowCopyAllVariantsNonEmpty() {
|
||
for e in TraitLevel.allCases {
|
||
for n in TraitLevel.allCases {
|
||
let copy = PersonalityEngine.callWindowCopy(profile: profile(e: e, n: n))
|
||
#expect(!copy.isEmpty, "callWindowCopy leer für e=\(e), n=\(n)")
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: aftermathCopy
|
||
|
||
@Test("aftermathCopy: nil-Profil liefert nicht-leeren Fallback")
|
||
func aftermathCopyNilProfile() {
|
||
let copy = PersonalityEngine.aftermathCopy(profile: nil)
|
||
#expect(!copy.isEmpty)
|
||
}
|
||
|
||
@Test("aftermathCopy: hoher Neurotizismus → weicher, einladender Text")
|
||
func aftermathCopyHighNeuroticism() {
|
||
let copy = PersonalityEngine.aftermathCopy(profile: profile(n: .high))
|
||
#expect(copy.contains("Wenn du magst"))
|
||
}
|
||
|
||
@Test("aftermathCopy: niedriger Neurotizismus → direkter Text")
|
||
func aftermathCopyLowNeuroticism() {
|
||
let copy = PersonalityEngine.aftermathCopy(profile: profile(n: .low))
|
||
#expect(copy.contains("Wie wirkt"))
|
||
}
|
||
|
||
@Test("aftermathCopy: alle Varianten sind nicht leer")
|
||
func aftermathCopyAllVariantsNonEmpty() {
|
||
for n in TraitLevel.allCases {
|
||
let copy = PersonalityEngine.aftermathCopy(profile: profile(n: n))
|
||
#expect(!copy.isEmpty, "aftermathCopy leer für n=\(n)")
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - OnboardingStep – Regressionswächter (nach Quiz-Erweiterung)
|
||
|
||
@Suite("OnboardingStep – RawValues (Quiz-Erweiterung)")
|
||
struct OnboardingStepQuizTests {
|
||
|
||
@Test("RawValues sind aufsteigend 0–4")
|
||
@MainActor func rawValuesSequential() {
|
||
#expect(OnboardingStep.profile.rawValue == 0)
|
||
#expect(OnboardingStep.quiz.rawValue == 1)
|
||
#expect(OnboardingStep.contacts.rawValue == 2)
|
||
#expect(OnboardingStep.tour.rawValue == 3)
|
||
#expect(OnboardingStep.complete.rawValue == 4)
|
||
}
|
||
|
||
@Test("allCases enthält genau 5 Schritte")
|
||
@MainActor func allCasesCountIsFive() {
|
||
#expect(OnboardingStep.allCases.count == 5)
|
||
}
|
||
}
|