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>
This commit is contained in:
572
SportsTimeTests/Domain/ProgressTests.swift
Normal file
572
SportsTimeTests/Domain/ProgressTests.swift
Normal file
@@ -0,0 +1,572 @@
|
||||
//
|
||||
// ProgressTests.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// TDD specification tests for Progress models (LeagueProgress, DivisionProgress, etc.).
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import SportsTime
|
||||
|
||||
@Suite("LeagueProgress")
|
||||
struct LeagueProgressTests {
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
private func makeLeagueProgress(visited: Int, total: Int) -> LeagueProgress {
|
||||
LeagueProgress(
|
||||
sport: .mlb,
|
||||
totalStadiums: total,
|
||||
visitedStadiums: visited,
|
||||
stadiumsVisited: [],
|
||||
stadiumsRemaining: []
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: completionPercentage
|
||||
|
||||
@Test("completionPercentage: 50% when half visited")
|
||||
func completionPercentage_half() {
|
||||
let progress = makeLeagueProgress(visited: 15, total: 30)
|
||||
|
||||
#expect(progress.completionPercentage == 50.0)
|
||||
}
|
||||
|
||||
@Test("completionPercentage: 100% when all visited")
|
||||
func completionPercentage_complete() {
|
||||
let progress = makeLeagueProgress(visited: 30, total: 30)
|
||||
|
||||
#expect(progress.completionPercentage == 100.0)
|
||||
}
|
||||
|
||||
@Test("completionPercentage: 0% when none visited")
|
||||
func completionPercentage_zero() {
|
||||
let progress = makeLeagueProgress(visited: 0, total: 30)
|
||||
|
||||
#expect(progress.completionPercentage == 0.0)
|
||||
}
|
||||
|
||||
@Test("completionPercentage: 0 when total is 0")
|
||||
func completionPercentage_totalZero() {
|
||||
let progress = makeLeagueProgress(visited: 0, total: 0)
|
||||
|
||||
#expect(progress.completionPercentage == 0)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: progressFraction
|
||||
|
||||
@Test("progressFraction: 0.5 when half visited")
|
||||
func progressFraction_half() {
|
||||
let progress = makeLeagueProgress(visited: 15, total: 30)
|
||||
|
||||
#expect(progress.progressFraction == 0.5)
|
||||
}
|
||||
|
||||
@Test("progressFraction: 1.0 when complete")
|
||||
func progressFraction_complete() {
|
||||
let progress = makeLeagueProgress(visited: 30, total: 30)
|
||||
|
||||
#expect(progress.progressFraction == 1.0)
|
||||
}
|
||||
|
||||
@Test("progressFraction: 0 when total is 0")
|
||||
func progressFraction_totalZero() {
|
||||
let progress = makeLeagueProgress(visited: 5, total: 0)
|
||||
|
||||
#expect(progress.progressFraction == 0)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: isComplete
|
||||
|
||||
@Test("isComplete: true when visited >= total and total > 0")
|
||||
func isComplete_true() {
|
||||
let progress = makeLeagueProgress(visited: 30, total: 30)
|
||||
|
||||
#expect(progress.isComplete == true)
|
||||
}
|
||||
|
||||
@Test("isComplete: false when visited < total")
|
||||
func isComplete_false() {
|
||||
let progress = makeLeagueProgress(visited: 29, total: 30)
|
||||
|
||||
#expect(progress.isComplete == false)
|
||||
}
|
||||
|
||||
@Test("isComplete: false when total is 0")
|
||||
func isComplete_totalZero() {
|
||||
let progress = makeLeagueProgress(visited: 0, total: 0)
|
||||
|
||||
#expect(progress.isComplete == false)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: progressDescription
|
||||
|
||||
@Test("progressDescription: formats as visited/total")
|
||||
func progressDescription_format() {
|
||||
let progress = makeLeagueProgress(visited: 15, total: 30)
|
||||
|
||||
#expect(progress.progressDescription == "15/30")
|
||||
}
|
||||
|
||||
// MARK: - Invariant Tests
|
||||
|
||||
@Test("Invariant: completionPercentage in range [0, 100]")
|
||||
func invariant_percentageRange() {
|
||||
let testCases = [
|
||||
(visited: 0, total: 30),
|
||||
(visited: 15, total: 30),
|
||||
(visited: 30, total: 30),
|
||||
(visited: 0, total: 0),
|
||||
]
|
||||
|
||||
for (visited, total) in testCases {
|
||||
let progress = makeLeagueProgress(visited: visited, total: total)
|
||||
#expect(progress.completionPercentage >= 0)
|
||||
#expect(progress.completionPercentage <= 100)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Invariant: progressFraction in range [0, 1]")
|
||||
func invariant_fractionRange() {
|
||||
let testCases = [
|
||||
(visited: 0, total: 30),
|
||||
(visited: 15, total: 30),
|
||||
(visited: 30, total: 30),
|
||||
(visited: 0, total: 0),
|
||||
]
|
||||
|
||||
for (visited, total) in testCases {
|
||||
let progress = makeLeagueProgress(visited: visited, total: total)
|
||||
#expect(progress.progressFraction >= 0)
|
||||
#expect(progress.progressFraction <= 1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Invariant: isComplete requires total > 0")
|
||||
func invariant_isCompleteRequiresTotal() {
|
||||
// Can't be complete with 0 total
|
||||
let progress = makeLeagueProgress(visited: 0, total: 0)
|
||||
#expect(progress.isComplete == false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DivisionProgress Tests
|
||||
|
||||
@Suite("DivisionProgress")
|
||||
struct DivisionProgressTests {
|
||||
|
||||
private func makeDivisionProgress(visited: Int, total: Int) -> DivisionProgress {
|
||||
let division = Division(
|
||||
id: "test_div",
|
||||
name: "Test Division",
|
||||
conference: "Test Conference",
|
||||
conferenceId: "test_conf",
|
||||
sport: .mlb,
|
||||
teamCanonicalIds: []
|
||||
)
|
||||
|
||||
return DivisionProgress(
|
||||
division: division,
|
||||
totalStadiums: total,
|
||||
visitedStadiums: visited,
|
||||
stadiumsVisited: [],
|
||||
stadiumsRemaining: []
|
||||
)
|
||||
}
|
||||
|
||||
@Test("completionPercentage: calculates correctly")
|
||||
func completionPercentage() {
|
||||
let progress = makeDivisionProgress(visited: 3, total: 5)
|
||||
|
||||
#expect(progress.completionPercentage == 60.0)
|
||||
}
|
||||
|
||||
@Test("progressFraction: calculates correctly")
|
||||
func progressFraction() {
|
||||
let progress = makeDivisionProgress(visited: 3, total: 5)
|
||||
|
||||
#expect(progress.progressFraction == 0.6)
|
||||
}
|
||||
|
||||
@Test("isComplete: true when all visited")
|
||||
func isComplete() {
|
||||
let complete = makeDivisionProgress(visited: 5, total: 5)
|
||||
let incomplete = makeDivisionProgress(visited: 4, total: 5)
|
||||
|
||||
#expect(complete.isComplete == true)
|
||||
#expect(incomplete.isComplete == false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ConferenceProgress Tests
|
||||
|
||||
@Suite("ConferenceProgress")
|
||||
struct ConferenceProgressTests {
|
||||
|
||||
private func makeConferenceProgress(visited: Int, total: Int) -> ConferenceProgress {
|
||||
let conference = Conference(
|
||||
id: "test_conf",
|
||||
name: "Test Conference",
|
||||
abbreviation: "TC",
|
||||
sport: .mlb,
|
||||
divisionIds: []
|
||||
)
|
||||
|
||||
return ConferenceProgress(
|
||||
conference: conference,
|
||||
totalStadiums: total,
|
||||
visitedStadiums: visited,
|
||||
divisionProgress: []
|
||||
)
|
||||
}
|
||||
|
||||
@Test("completionPercentage: calculates correctly")
|
||||
func completionPercentage() {
|
||||
let progress = makeConferenceProgress(visited: 10, total: 15)
|
||||
|
||||
#expect(abs(progress.completionPercentage - 66.666) < 0.01)
|
||||
}
|
||||
|
||||
@Test("isComplete: true when all visited")
|
||||
func isComplete() {
|
||||
let complete = makeConferenceProgress(visited: 15, total: 15)
|
||||
let incomplete = makeConferenceProgress(visited: 14, total: 15)
|
||||
|
||||
#expect(complete.isComplete == true)
|
||||
#expect(incomplete.isComplete == false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OverallProgress Tests
|
||||
|
||||
@Suite("OverallProgress")
|
||||
struct OverallProgressTests {
|
||||
|
||||
@Test("overallPercentage: calculates correctly")
|
||||
func overallPercentage() {
|
||||
let progress = OverallProgress(
|
||||
leagueProgress: [],
|
||||
totalVisits: 50,
|
||||
uniqueStadiumsVisited: 46,
|
||||
totalStadiumsAcrossLeagues: 92,
|
||||
achievementsEarned: 10,
|
||||
totalAchievements: 50
|
||||
)
|
||||
|
||||
#expect(progress.overallPercentage == 50.0)
|
||||
}
|
||||
|
||||
@Test("overallPercentage: 0 when no stadiums")
|
||||
func overallPercentage_zero() {
|
||||
let progress = OverallProgress(
|
||||
leagueProgress: [],
|
||||
totalVisits: 0,
|
||||
uniqueStadiumsVisited: 0,
|
||||
totalStadiumsAcrossLeagues: 0,
|
||||
achievementsEarned: 0,
|
||||
totalAchievements: 0
|
||||
)
|
||||
|
||||
#expect(progress.overallPercentage == 0)
|
||||
}
|
||||
|
||||
@Test("progress(for:): finds league progress")
|
||||
func progressForSport() {
|
||||
let mlbProgress = LeagueProgress(
|
||||
sport: .mlb,
|
||||
totalStadiums: 30,
|
||||
visitedStadiums: 15,
|
||||
stadiumsVisited: [],
|
||||
stadiumsRemaining: []
|
||||
)
|
||||
|
||||
let overall = OverallProgress(
|
||||
leagueProgress: [mlbProgress],
|
||||
totalVisits: 15,
|
||||
uniqueStadiumsVisited: 15,
|
||||
totalStadiumsAcrossLeagues: 92,
|
||||
achievementsEarned: 5,
|
||||
totalAchievements: 50
|
||||
)
|
||||
|
||||
let found = overall.progress(for: .mlb)
|
||||
|
||||
#expect(found != nil)
|
||||
#expect(found?.visitedStadiums == 15)
|
||||
}
|
||||
|
||||
@Test("progress(for:): returns nil for missing sport")
|
||||
func progressForSport_notFound() {
|
||||
let overall = OverallProgress(
|
||||
leagueProgress: [],
|
||||
totalVisits: 0,
|
||||
uniqueStadiumsVisited: 0,
|
||||
totalStadiumsAcrossLeagues: 92,
|
||||
achievementsEarned: 0,
|
||||
totalAchievements: 50
|
||||
)
|
||||
|
||||
#expect(overall.progress(for: .mlb) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StadiumVisitStatus Tests
|
||||
|
||||
@Suite("StadiumVisitStatus")
|
||||
struct StadiumVisitStatusTests {
|
||||
|
||||
private func makeVisitSummary(date: Date) -> VisitSummary {
|
||||
VisitSummary(
|
||||
id: UUID(),
|
||||
stadium: Stadium(
|
||||
id: "stadium1",
|
||||
name: "Test Stadium",
|
||||
city: "Test City",
|
||||
state: "TS",
|
||||
latitude: 40.0,
|
||||
longitude: -74.0,
|
||||
capacity: 40000,
|
||||
sport: .mlb
|
||||
),
|
||||
visitDate: date,
|
||||
visitType: .game,
|
||||
sport: .mlb,
|
||||
homeTeamName: "Home Team",
|
||||
awayTeamName: "Away Team",
|
||||
score: "5-3",
|
||||
photoCount: 0,
|
||||
notes: nil
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: isVisited
|
||||
|
||||
@Test("isVisited: true for visited status")
|
||||
func isVisited_true() {
|
||||
let visit = makeVisitSummary(date: Date())
|
||||
let status = StadiumVisitStatus.visited(visits: [visit])
|
||||
|
||||
#expect(status.isVisited == true)
|
||||
}
|
||||
|
||||
@Test("isVisited: false for notVisited status")
|
||||
func isVisited_false() {
|
||||
let status = StadiumVisitStatus.notVisited
|
||||
|
||||
#expect(status.isVisited == false)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: visitCount
|
||||
|
||||
@Test("visitCount: returns count of visits")
|
||||
func visitCount_multiple() {
|
||||
let visits = [
|
||||
makeVisitSummary(date: Date()),
|
||||
makeVisitSummary(date: Date()),
|
||||
makeVisitSummary(date: Date()),
|
||||
]
|
||||
let status = StadiumVisitStatus.visited(visits: visits)
|
||||
|
||||
#expect(status.visitCount == 3)
|
||||
}
|
||||
|
||||
@Test("visitCount: 0 for notVisited")
|
||||
func visitCount_notVisited() {
|
||||
let status = StadiumVisitStatus.notVisited
|
||||
|
||||
#expect(status.visitCount == 0)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: latestVisit
|
||||
|
||||
@Test("latestVisit: returns visit with max date")
|
||||
func latestVisit_maxDate() {
|
||||
let calendar = Calendar.current
|
||||
let date1 = calendar.date(from: DateComponents(year: 2025, month: 1, day: 1))!
|
||||
let date2 = calendar.date(from: DateComponents(year: 2025, month: 6, day: 15))!
|
||||
let date3 = calendar.date(from: DateComponents(year: 2025, month: 3, day: 10))!
|
||||
|
||||
let visits = [
|
||||
makeVisitSummary(date: date1),
|
||||
makeVisitSummary(date: date2),
|
||||
makeVisitSummary(date: date3),
|
||||
]
|
||||
let status = StadiumVisitStatus.visited(visits: visits)
|
||||
|
||||
#expect(status.latestVisit?.visitDate == date2)
|
||||
}
|
||||
|
||||
@Test("latestVisit: nil for notVisited")
|
||||
func latestVisit_notVisited() {
|
||||
let status = StadiumVisitStatus.notVisited
|
||||
|
||||
#expect(status.latestVisit == nil)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: firstVisit
|
||||
|
||||
@Test("firstVisit: returns visit with min date")
|
||||
func firstVisit_minDate() {
|
||||
let calendar = Calendar.current
|
||||
let date1 = calendar.date(from: DateComponents(year: 2025, month: 1, day: 1))!
|
||||
let date2 = calendar.date(from: DateComponents(year: 2025, month: 6, day: 15))!
|
||||
let date3 = calendar.date(from: DateComponents(year: 2025, month: 3, day: 10))!
|
||||
|
||||
let visits = [
|
||||
makeVisitSummary(date: date1),
|
||||
makeVisitSummary(date: date2),
|
||||
makeVisitSummary(date: date3),
|
||||
]
|
||||
let status = StadiumVisitStatus.visited(visits: visits)
|
||||
|
||||
#expect(status.firstVisit?.visitDate == date1)
|
||||
}
|
||||
|
||||
@Test("firstVisit: nil for notVisited")
|
||||
func firstVisit_notVisited() {
|
||||
let status = StadiumVisitStatus.notVisited
|
||||
|
||||
#expect(status.firstVisit == nil)
|
||||
}
|
||||
|
||||
// MARK: - Invariant Tests
|
||||
|
||||
@Test("Invariant: visitCount == 0 for notVisited")
|
||||
func invariant_visitCountZeroForNotVisited() {
|
||||
let status = StadiumVisitStatus.notVisited
|
||||
|
||||
#expect(status.visitCount == 0)
|
||||
}
|
||||
|
||||
@Test("Invariant: latestVisit and firstVisit are nil for notVisited")
|
||||
func invariant_visitsNilForNotVisited() {
|
||||
let status = StadiumVisitStatus.notVisited
|
||||
|
||||
#expect(status.latestVisit == nil)
|
||||
#expect(status.firstVisit == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - VisitSummary Tests
|
||||
|
||||
@Suite("VisitSummary")
|
||||
struct VisitSummaryTests {
|
||||
|
||||
private func makeVisitSummary(
|
||||
homeTeam: String? = "Home",
|
||||
awayTeam: String? = "Away"
|
||||
) -> VisitSummary {
|
||||
VisitSummary(
|
||||
id: UUID(),
|
||||
stadium: Stadium(
|
||||
id: "stadium1",
|
||||
name: "Test Stadium",
|
||||
city: "Test City",
|
||||
state: "TS",
|
||||
latitude: 40.0,
|
||||
longitude: -74.0,
|
||||
capacity: 40000,
|
||||
sport: .mlb
|
||||
),
|
||||
visitDate: Date(),
|
||||
visitType: .game,
|
||||
sport: .mlb,
|
||||
homeTeamName: homeTeam,
|
||||
awayTeamName: awayTeam,
|
||||
score: "5-3",
|
||||
photoCount: 0,
|
||||
notes: nil
|
||||
)
|
||||
}
|
||||
|
||||
@Test("matchup: returns 'away @ home' format")
|
||||
func matchup_format() {
|
||||
let summary = makeVisitSummary(homeTeam: "Red Sox", awayTeam: "Yankees")
|
||||
|
||||
#expect(summary.matchup == "Yankees @ Red Sox")
|
||||
}
|
||||
|
||||
@Test("matchup: nil when home team is nil")
|
||||
func matchup_nilHome() {
|
||||
let summary = makeVisitSummary(homeTeam: nil, awayTeam: "Yankees")
|
||||
|
||||
#expect(summary.matchup == nil)
|
||||
}
|
||||
|
||||
@Test("matchup: nil when away team is nil")
|
||||
func matchup_nilAway() {
|
||||
let summary = makeVisitSummary(homeTeam: "Red Sox", awayTeam: nil)
|
||||
|
||||
#expect(summary.matchup == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ProgressCardData Tests
|
||||
|
||||
@Suite("ProgressCardData")
|
||||
struct ProgressCardDataTests {
|
||||
|
||||
@Test("title: includes sport display name")
|
||||
func title_includesSport() {
|
||||
let progress = LeagueProgress(
|
||||
sport: .mlb,
|
||||
totalStadiums: 30,
|
||||
visitedStadiums: 15,
|
||||
stadiumsVisited: [],
|
||||
stadiumsRemaining: []
|
||||
)
|
||||
|
||||
let cardData = ProgressCardData(
|
||||
sport: .mlb,
|
||||
progress: progress,
|
||||
username: "TestUser",
|
||||
includeMap: true,
|
||||
showDetailedStats: true
|
||||
)
|
||||
|
||||
#expect(cardData.title == "Major League Baseball Stadium Quest")
|
||||
}
|
||||
|
||||
@Test("subtitle: shows visited of total")
|
||||
func subtitle_format() {
|
||||
let progress = LeagueProgress(
|
||||
sport: .mlb,
|
||||
totalStadiums: 30,
|
||||
visitedStadiums: 15,
|
||||
stadiumsVisited: [],
|
||||
stadiumsRemaining: []
|
||||
)
|
||||
|
||||
let cardData = ProgressCardData(
|
||||
sport: .mlb,
|
||||
progress: progress,
|
||||
username: nil,
|
||||
includeMap: false,
|
||||
showDetailedStats: false
|
||||
)
|
||||
|
||||
#expect(cardData.subtitle == "15 of 30 Stadiums")
|
||||
}
|
||||
|
||||
@Test("percentageText: formats without decimals")
|
||||
func percentageText_format() {
|
||||
let progress = LeagueProgress(
|
||||
sport: .mlb,
|
||||
totalStadiums: 30,
|
||||
visitedStadiums: 15,
|
||||
stadiumsVisited: [],
|
||||
stadiumsRemaining: []
|
||||
)
|
||||
|
||||
let cardData = ProgressCardData(
|
||||
sport: .mlb,
|
||||
progress: progress,
|
||||
username: nil,
|
||||
includeMap: false,
|
||||
showDetailedStats: false
|
||||
)
|
||||
|
||||
#expect(cardData.percentageText == "50%")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user