Files
Sportstime/SportsTimeTests/Domain/AchievementDefinitionsTests.swift
Trey t 8162b4a029 refactor(tests): TDD rewrite of all unit tests with spec documentation
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>
2026-01-16 14:07:41 -06:00

365 lines
12 KiB
Swift

//
// AchievementDefinitionsTests.swift
// SportsTimeTests
//
// TDD specification tests for AchievementRegistry and related models.
//
import Testing
import SwiftUI
@testable import SportsTime
@Suite("AchievementCategory")
struct AchievementCategoryTests {
@Test("Property: all cases have displayName")
func property_allHaveDisplayName() {
for category in AchievementCategory.allCases {
#expect(!category.displayName.isEmpty)
}
}
@Test("displayName: count returns 'Milestones'")
func displayName_count() {
#expect(AchievementCategory.count.displayName == "Milestones")
}
@Test("displayName: division returns 'Divisions'")
func displayName_division() {
#expect(AchievementCategory.division.displayName == "Divisions")
}
}
// MARK: - AchievementDefinition Tests
@Suite("AchievementDefinition")
struct AchievementDefinitionTests {
@Test("equality: based on id only")
func equality_basedOnId() {
let def1 = AchievementDefinition(
id: "test_achievement",
name: "Name A",
description: "Description A",
category: .count,
iconName: "icon.a",
iconColor: .blue,
requirement: .visitCount(5)
)
let def2 = AchievementDefinition(
id: "test_achievement",
name: "Different Name",
description: "Different Description",
category: .division,
iconName: "icon.b",
iconColor: .red,
requirement: .visitCount(10)
)
#expect(def1 == def2, "Achievements with same id should be equal")
}
@Test("inequality: different ids")
func inequality_differentIds() {
let def1 = AchievementDefinition(
id: "achievement_1",
name: "Same Name",
description: "Same Description",
category: .count,
iconName: "icon",
iconColor: .blue,
requirement: .visitCount(5)
)
let def2 = AchievementDefinition(
id: "achievement_2",
name: "Same Name",
description: "Same Description",
category: .count,
iconName: "icon",
iconColor: .blue,
requirement: .visitCount(5)
)
#expect(def1 != def2, "Achievements with different ids should not be equal")
}
@Test("Property: divisionId can be set")
func property_divisionId() {
let def = AchievementDefinition(
id: "test_div_ach",
name: "Division Achievement",
description: "Complete a division",
category: .division,
sport: .mlb,
iconName: "baseball.fill",
iconColor: .red,
requirement: .completeDivision("mlb_al_east"),
divisionId: "mlb_al_east"
)
#expect(def.divisionId == "mlb_al_east")
}
@Test("Property: conferenceId can be set")
func property_conferenceId() {
let def = AchievementDefinition(
id: "test_conf_ach",
name: "Conference Achievement",
description: "Complete a conference",
category: .conference,
sport: .mlb,
iconName: "baseball.fill",
iconColor: .red,
requirement: .completeConference("mlb_al"),
conferenceId: "mlb_al"
)
#expect(def.conferenceId == "mlb_al")
}
}
// MARK: - AchievementRegistry Tests
@Suite("AchievementRegistry")
struct AchievementRegistryTests {
// MARK: - Specification Tests: all
@Test("all: is sorted by sortOrder ascending")
func all_sortedBySortOrder() {
let achievements = AchievementRegistry.all
for i in 1..<achievements.count {
#expect(
achievements[i].sortOrder >= achievements[i - 1].sortOrder,
"Achievement at index \(i) should have sortOrder >= previous"
)
}
}
@Test("all: contains count achievements")
func all_containsCountAchievements() {
let countAchievements = AchievementRegistry.all.filter { $0.category == .count }
#expect(countAchievements.count >= 5, "Should have multiple count achievements")
}
@Test("all: contains division achievements")
func all_containsDivisionAchievements() {
let divisionAchievements = AchievementRegistry.all.filter { $0.category == .division }
#expect(divisionAchievements.count >= 16, "Should have division achievements for MLB, NBA, NHL")
}
// MARK: - Specification Tests: achievement(byId:)
@Test("achievement(byId:): finds existing achievement")
func achievementById_found() {
let achievement = AchievementRegistry.achievement(byId: "first_visit")
#expect(achievement != nil)
#expect(achievement?.name == "First Pitch")
}
@Test("achievement(byId:): returns nil for unknown ID")
func achievementById_notFound() {
let achievement = AchievementRegistry.achievement(byId: "nonexistent_achievement")
#expect(achievement == nil)
}
// MARK: - Specification Tests: achievements(forCategory:)
@Test("achievements(forCategory:): filters by exact category")
func achievementsForCategory_filters() {
let countAchievements = AchievementRegistry.achievements(forCategory: .count)
for achievement in countAchievements {
#expect(achievement.category == .count)
}
}
@Test("achievements(forCategory:): returns all division achievements")
func achievementsForCategory_division() {
let divisionAchievements = AchievementRegistry.achievements(forCategory: .division)
#expect(!divisionAchievements.isEmpty)
for achievement in divisionAchievements {
#expect(achievement.category == .division)
}
}
// MARK: - Specification Tests: achievements(forSport:)
@Test("achievements(forSport:): includes sport-specific achievements")
func achievementsForSport_includesSportSpecific() {
let mlbAchievements = AchievementRegistry.achievements(forSport: .mlb)
let sportSpecific = mlbAchievements.filter { $0.sport == .mlb }
#expect(!sportSpecific.isEmpty, "Should include MLB-specific achievements")
}
@Test("achievements(forSport:): includes cross-sport achievements")
func achievementsForSport_includesCrossSport() {
let mlbAchievements = AchievementRegistry.achievements(forSport: .mlb)
let crossSport = mlbAchievements.filter { $0.sport == nil }
#expect(!crossSport.isEmpty, "Should include cross-sport achievements (sport == nil)")
}
// MARK: - Specification Tests: divisionAchievements(forSport:)
@Test("divisionAchievements(forSport:): filters to division category AND sport")
func divisionAchievementsForSport_filters() {
let mlbDivisionAchievements = AchievementRegistry.divisionAchievements(forSport: .mlb)
#expect(mlbDivisionAchievements.count == 6, "MLB has 6 divisions")
for achievement in mlbDivisionAchievements {
#expect(achievement.category == .division)
#expect(achievement.sport == .mlb)
}
}
@Test("divisionAchievements(forSport:): NBA has 6 divisions")
func divisionAchievementsForSport_nba() {
let nbaDivisionAchievements = AchievementRegistry.divisionAchievements(forSport: .nba)
#expect(nbaDivisionAchievements.count == 6)
}
@Test("divisionAchievements(forSport:): NHL has 4 divisions")
func divisionAchievementsForSport_nhl() {
let nhlDivisionAchievements = AchievementRegistry.divisionAchievements(forSport: .nhl)
#expect(nhlDivisionAchievements.count == 4)
}
// MARK: - Specification Tests: conferenceAchievements(forSport:)
@Test("conferenceAchievements(forSport:): filters to conference category AND sport")
func conferenceAchievementsForSport_filters() {
let mlbConferenceAchievements = AchievementRegistry.conferenceAchievements(forSport: .mlb)
#expect(mlbConferenceAchievements.count == 2, "MLB has 2 leagues (AL, NL)")
for achievement in mlbConferenceAchievements {
#expect(achievement.category == .conference)
#expect(achievement.sport == .mlb)
}
}
// MARK: - Invariant Tests
@Test("Invariant: all achievement IDs are unique")
func invariant_uniqueIds() {
let ids = AchievementRegistry.all.map { $0.id }
let uniqueIds = Set(ids)
#expect(ids.count == uniqueIds.count, "All achievement IDs should be unique")
}
@Test("Invariant: division achievements have non-nil divisionId")
func invariant_divisionAchievementsHaveDivisionId() {
let divisionAchievements = AchievementRegistry.achievements(forCategory: .division)
for achievement in divisionAchievements {
#expect(
achievement.divisionId != nil,
"Division achievement \(achievement.id) should have divisionId"
)
}
}
@Test("Invariant: conference achievements have non-nil conferenceId")
func invariant_conferenceAchievementsHaveConferenceId() {
let conferenceAchievements = AchievementRegistry.achievements(forCategory: .conference)
for achievement in conferenceAchievements {
#expect(
achievement.conferenceId != nil,
"Conference achievement \(achievement.id) should have conferenceId"
)
}
}
@Test("Invariant: all achievements have non-empty name")
func invariant_allHaveNames() {
for achievement in AchievementRegistry.all {
#expect(!achievement.name.isEmpty, "Achievement \(achievement.id) should have a name")
}
}
@Test("Invariant: all achievements have non-empty description")
func invariant_allHaveDescriptions() {
for achievement in AchievementRegistry.all {
#expect(!achievement.description.isEmpty, "Achievement \(achievement.id) should have a description")
}
}
@Test("Invariant: all achievements have non-empty iconName")
func invariant_allHaveIconNames() {
for achievement in AchievementRegistry.all {
#expect(!achievement.iconName.isEmpty, "Achievement \(achievement.id) should have an iconName")
}
}
}
// MARK: - AchievementRequirement Tests
@Suite("AchievementRequirement")
struct AchievementRequirementTests {
@Test("Property: visitCount requirement stores count")
func visitCount_storesValue() {
let requirement = AchievementRequirement.visitCount(10)
if case .visitCount(let count) = requirement {
#expect(count == 10)
} else {
Issue.record("Should be visitCount case")
}
}
@Test("Property: completeDivision requirement stores division ID")
func completeDivision_storesId() {
let requirement = AchievementRequirement.completeDivision("mlb_al_east")
if case .completeDivision(let divisionId) = requirement {
#expect(divisionId == "mlb_al_east")
} else {
Issue.record("Should be completeDivision case")
}
}
@Test("Property: visitsInDays requirement stores both values")
func visitsInDays_storesValues() {
let requirement = AchievementRequirement.visitsInDays(5, days: 7)
if case .visitsInDays(let visits, let days) = requirement {
#expect(visits == 5)
#expect(days == 7)
} else {
Issue.record("Should be visitsInDays case")
}
}
@Test("hashable: same requirements are equal")
func hashable_equality() {
let req1 = AchievementRequirement.visitCount(10)
let req2 = AchievementRequirement.visitCount(10)
#expect(req1 == req2)
}
@Test("hashable: different requirements are not equal")
func hashable_inequality() {
let req1 = AchievementRequirement.visitCount(10)
let req2 = AchievementRequirement.visitCount(20)
#expect(req1 != req2)
}
}