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>
365 lines
12 KiB
Swift
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)
|
|
}
|
|
}
|