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>
573 lines
16 KiB
Swift
573 lines
16 KiB
Swift
//
|
|
// 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%")
|
|
}
|
|
}
|