Files
Sportstime/SportsTimeTests/Domain/ProgressTests.swift
2026-02-18 13:00:15 -06:00

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: TestClock.now)
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: TestClock.now),
makeVisitSummary(date: TestClock.now),
makeVisitSummary(date: TestClock.now),
]
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 = TestClock.calendar
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 = TestClock.calendar
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: TestClock.now,
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%")
}
}