Complete rewrite of unit test suite using TDD methodology: Planning Engine Tests: - GameDAGRouterTests: Beam search, anchor games, transitions - ItineraryBuilderTests: Stop connection, validators, EV enrichment - RouteFiltersTests: Region, time window, scoring filters - ScenarioA/B/C/D PlannerTests: All planning scenarios - TravelEstimatorTests: Distance, duration, travel days - TripPlanningEngineTests: Orchestration, caching, preferences Domain Model Tests: - AchievementDefinitionsTests, AnySportTests, DivisionTests - GameTests, ProgressTests, RegionTests, StadiumTests - TeamTests, TravelSegmentTests, TripTests, TripPollTests - TripPreferencesTests, TripStopTests, SportTests Service Tests: - FreeScoreAPITests, RouteDescriptionGeneratorTests - SuggestedTripsGeneratorTests Export Tests: - ShareableContentTests (card types, themes, dimensions) Bug fixes discovered through TDD: - ShareCardDimensions: mapSnapshotSize exceeded available width (960x480) - ScenarioBPlanner: Added anchor game validation filter All tests include: - Specification tests (expected behavior) - Invariant tests (properties that must always hold) - Edge case tests (boundary conditions) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
269 lines
9.3 KiB
Swift
269 lines
9.3 KiB
Swift
//
|
|
// AchievementEngineTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// TDD specification tests for AchievementEngine types.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import SwiftUI
|
|
@testable import SportsTime
|
|
|
|
// MARK: - AchievementDelta Tests
|
|
|
|
@Suite("AchievementDelta")
|
|
struct AchievementDeltaTests {
|
|
|
|
// MARK: - Test Data
|
|
|
|
private func makeDefinition(id: String = "test_achievement") -> AchievementDefinition {
|
|
AchievementDefinition(
|
|
id: id,
|
|
name: "Test Achievement",
|
|
description: "Test description",
|
|
category: .count,
|
|
sport: nil,
|
|
iconName: "star.fill",
|
|
iconColor: .blue,
|
|
requirement: .firstVisit
|
|
)
|
|
}
|
|
|
|
private func makeDelta(
|
|
newlyEarned: [AchievementDefinition] = [],
|
|
revoked: [AchievementDefinition] = [],
|
|
stillEarned: [AchievementDefinition] = []
|
|
) -> AchievementDelta {
|
|
AchievementDelta(
|
|
newlyEarned: newlyEarned,
|
|
revoked: revoked,
|
|
stillEarned: stillEarned
|
|
)
|
|
}
|
|
|
|
// MARK: - Specification Tests: hasChanges
|
|
|
|
/// - Expected Behavior: true when newlyEarned is not empty
|
|
@Test("hasChanges: true when newlyEarned not empty")
|
|
func hasChanges_newlyEarned() {
|
|
let delta = makeDelta(newlyEarned: [makeDefinition()])
|
|
#expect(delta.hasChanges == true)
|
|
}
|
|
|
|
/// - Expected Behavior: true when revoked is not empty
|
|
@Test("hasChanges: true when revoked not empty")
|
|
func hasChanges_revoked() {
|
|
let delta = makeDelta(revoked: [makeDefinition()])
|
|
#expect(delta.hasChanges == true)
|
|
}
|
|
|
|
/// - Expected Behavior: false when both are empty (stillEarned doesn't count)
|
|
@Test("hasChanges: false when newlyEarned and revoked both empty")
|
|
func hasChanges_bothEmpty() {
|
|
let delta = makeDelta(newlyEarned: [], revoked: [], stillEarned: [makeDefinition()])
|
|
#expect(delta.hasChanges == false)
|
|
}
|
|
|
|
/// - Expected Behavior: true when both newlyEarned and revoked have items
|
|
@Test("hasChanges: true when both newlyEarned and revoked have items")
|
|
func hasChanges_bothHaveItems() {
|
|
let delta = makeDelta(
|
|
newlyEarned: [makeDefinition(id: "new")],
|
|
revoked: [makeDefinition(id: "old")]
|
|
)
|
|
#expect(delta.hasChanges == true)
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
/// - Invariant: hasChanges == (!newlyEarned.isEmpty || !revoked.isEmpty)
|
|
@Test("Invariant: hasChanges formula is correct")
|
|
func invariant_hasChangesFormula() {
|
|
let testCases: [(newlyEarned: [AchievementDefinition], revoked: [AchievementDefinition])] = [
|
|
([], []),
|
|
([makeDefinition()], []),
|
|
([], [makeDefinition()]),
|
|
([makeDefinition()], [makeDefinition()])
|
|
]
|
|
|
|
for (newlyEarned, revoked) in testCases {
|
|
let delta = makeDelta(newlyEarned: newlyEarned, revoked: revoked)
|
|
let expected = !newlyEarned.isEmpty || !revoked.isEmpty
|
|
#expect(delta.hasChanges == expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - AchievementProgress Tests
|
|
|
|
@Suite("AchievementProgress")
|
|
struct AchievementProgressTests {
|
|
|
|
// MARK: - Test Data
|
|
|
|
private func makeDefinition() -> AchievementDefinition {
|
|
AchievementDefinition(
|
|
id: "test_progress",
|
|
name: "Test Progress",
|
|
description: "Test",
|
|
category: .count,
|
|
sport: nil,
|
|
iconName: "star",
|
|
iconColor: .blue,
|
|
requirement: .visitCount(10)
|
|
)
|
|
}
|
|
|
|
private func makeProgress(
|
|
currentProgress: Int = 5,
|
|
totalRequired: Int = 10,
|
|
hasStoredAchievement: Bool = false,
|
|
earnedAt: Date? = nil
|
|
) -> AchievementProgress {
|
|
AchievementProgress(
|
|
definition: makeDefinition(),
|
|
currentProgress: currentProgress,
|
|
totalRequired: totalRequired,
|
|
hasStoredAchievement: hasStoredAchievement,
|
|
earnedAt: earnedAt
|
|
)
|
|
}
|
|
|
|
// MARK: - Specification Tests: isEarned
|
|
|
|
/// - Expected Behavior: true when hasStoredAchievement is true
|
|
@Test("isEarned: true when hasStoredAchievement")
|
|
func isEarned_hasStoredAchievement() {
|
|
let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: true)
|
|
#expect(progress.isEarned == true)
|
|
}
|
|
|
|
/// - Expected Behavior: true when progress >= total (and total > 0)
|
|
@Test("isEarned: true when progress equals total")
|
|
func isEarned_progressEqualsTotal() {
|
|
let progress = makeProgress(currentProgress: 10, totalRequired: 10, hasStoredAchievement: false)
|
|
#expect(progress.isEarned == true)
|
|
}
|
|
|
|
/// - Expected Behavior: true when progress > total
|
|
@Test("isEarned: true when progress exceeds total")
|
|
func isEarned_progressExceedsTotal() {
|
|
let progress = makeProgress(currentProgress: 15, totalRequired: 10, hasStoredAchievement: false)
|
|
#expect(progress.isEarned == true)
|
|
}
|
|
|
|
/// - Expected Behavior: false when progress < total and no stored achievement
|
|
@Test("isEarned: false when progress less than total")
|
|
func isEarned_progressLessThanTotal() {
|
|
let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: false)
|
|
#expect(progress.isEarned == false)
|
|
}
|
|
|
|
/// - Expected Behavior: false when total is 0 (edge case)
|
|
@Test("isEarned: false when total is 0")
|
|
func isEarned_totalZero() {
|
|
let progress = makeProgress(currentProgress: 5, totalRequired: 0, hasStoredAchievement: false)
|
|
#expect(progress.isEarned == false)
|
|
}
|
|
|
|
// MARK: - Specification Tests: progressPercentage
|
|
|
|
/// - Expected Behavior: Returns current/total as Double
|
|
@Test("progressPercentage: returns correct ratio")
|
|
func progressPercentage_correctRatio() {
|
|
let progress = makeProgress(currentProgress: 5, totalRequired: 10)
|
|
#expect(progress.progressPercentage == 0.5)
|
|
}
|
|
|
|
@Test("progressPercentage: 100% when complete")
|
|
func progressPercentage_complete() {
|
|
let progress = makeProgress(currentProgress: 10, totalRequired: 10)
|
|
#expect(progress.progressPercentage == 1.0)
|
|
}
|
|
|
|
@Test("progressPercentage: 0% when no progress")
|
|
func progressPercentage_noProgress() {
|
|
let progress = makeProgress(currentProgress: 0, totalRequired: 10)
|
|
#expect(progress.progressPercentage == 0.0)
|
|
}
|
|
|
|
@Test("progressPercentage: 0 when total is 0")
|
|
func progressPercentage_totalZero() {
|
|
let progress = makeProgress(currentProgress: 5, totalRequired: 0)
|
|
#expect(progress.progressPercentage == 0.0)
|
|
}
|
|
|
|
@Test("progressPercentage: can exceed 100%")
|
|
func progressPercentage_exceedsHundred() {
|
|
let progress = makeProgress(currentProgress: 15, totalRequired: 10)
|
|
#expect(progress.progressPercentage == 1.5)
|
|
}
|
|
|
|
// MARK: - Specification Tests: progressText
|
|
|
|
/// - Expected Behavior: "Completed" when earned
|
|
@Test("progressText: Completed when earned via stored")
|
|
func progressText_completedViaStored() {
|
|
let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: true)
|
|
#expect(progress.progressText == "Completed")
|
|
}
|
|
|
|
@Test("progressText: Completed when earned via progress")
|
|
func progressText_completedViaProgress() {
|
|
let progress = makeProgress(currentProgress: 10, totalRequired: 10, hasStoredAchievement: false)
|
|
#expect(progress.progressText == "Completed")
|
|
}
|
|
|
|
/// - Expected Behavior: "current/total" when not earned
|
|
@Test("progressText: shows fraction when not earned")
|
|
func progressText_showsFraction() {
|
|
let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: false)
|
|
#expect(progress.progressText == "5/10")
|
|
}
|
|
|
|
@Test("progressText: shows 0/total when no progress")
|
|
func progressText_zeroProgress() {
|
|
let progress = makeProgress(currentProgress: 0, totalRequired: 10, hasStoredAchievement: false)
|
|
#expect(progress.progressText == "0/10")
|
|
}
|
|
|
|
// MARK: - Specification Tests: id
|
|
|
|
/// - Expected Behavior: id returns definition.id
|
|
@Test("id: returns definition id")
|
|
func id_returnsDefinitionId() {
|
|
let progress = makeProgress()
|
|
#expect(progress.id == "test_progress")
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
/// - Invariant: progressPercentage == Double(current) / Double(total) when total > 0
|
|
@Test("Invariant: progressPercentage calculation correct")
|
|
func invariant_progressPercentageFormula() {
|
|
let testCases: [(current: Int, total: Int)] = [
|
|
(0, 10),
|
|
(5, 10),
|
|
(10, 10),
|
|
(15, 10),
|
|
(1, 3)
|
|
]
|
|
|
|
for (current, total) in testCases {
|
|
let progress = makeProgress(currentProgress: current, totalRequired: total)
|
|
let expected = Double(current) / Double(total)
|
|
#expect(abs(progress.progressPercentage - expected) < 0.0001)
|
|
}
|
|
}
|
|
|
|
/// - Invariant: isEarned implies progressText == "Completed"
|
|
@Test("Invariant: isEarned implies Completed text")
|
|
func invariant_earnedImpliesCompletedText() {
|
|
let earned = makeProgress(currentProgress: 10, totalRequired: 10, hasStoredAchievement: true)
|
|
if earned.isEarned {
|
|
#expect(earned.progressText == "Completed")
|
|
}
|
|
}
|
|
}
|