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:
Trey t
2026-01-16 14:07:41 -06:00
parent 035dd6f5de
commit 8162b4a029
102 changed files with 13409 additions and 9883 deletions

View File

@@ -1,115 +0,0 @@
import XCTest
import SwiftData
@testable import SportsTime
@MainActor
final class GamesHistoryViewModelTests: XCTestCase {
var modelContainer: ModelContainer!
var modelContext: ModelContext!
override func setUp() async throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
modelContainer = try ModelContainer(
for: StadiumVisit.self, Achievement.self, UserPreferences.self,
configurations: config
)
modelContext = modelContainer.mainContext
}
override func tearDown() async throws {
modelContainer = nil
modelContext = nil
}
func test_GamesHistoryViewModel_GroupsVisitsByYear() async throws {
// Given: Visits in different years
let visit2026 = StadiumVisit(
stadiumId: "stadium-1",
stadiumNameAtVisit: "Stadium 2026",
visitDate: Calendar.current.date(from: DateComponents(year: 2026, month: 6, day: 15))!,
sport: .mlb,
visitType: .game,
dataSource: .fullyManual
)
let visit2025 = StadiumVisit(
stadiumId: "stadium-2",
stadiumNameAtVisit: "Stadium 2025",
visitDate: Calendar.current.date(from: DateComponents(year: 2025, month: 6, day: 15))!,
sport: .mlb,
visitType: .game,
dataSource: .fullyManual
)
modelContext.insert(visit2026)
modelContext.insert(visit2025)
try modelContext.save()
// When: Loading games history
let viewModel = GamesHistoryViewModel(modelContext: modelContext)
await viewModel.loadGames()
// Then: Visits are grouped by year
XCTAssertEqual(viewModel.visitsByYear.keys.count, 2, "Should have 2 years")
XCTAssertTrue(viewModel.visitsByYear.keys.contains(2026))
XCTAssertTrue(viewModel.visitsByYear.keys.contains(2025))
}
func test_GamesHistoryViewModel_FiltersBySport() async throws {
// Given: Visits to different sport stadiums
// Note: This requires stadiums in AppDataProvider to map stadiumId sport
let mlbVisit = StadiumVisit(
stadiumId: "yankee-stadium",
stadiumNameAtVisit: "Yankee Stadium",
visitDate: Date(),
sport: .mlb,
visitType: .game,
dataSource: .fullyManual
)
modelContext.insert(mlbVisit)
try modelContext.save()
// When: Filtering by MLB
let viewModel = GamesHistoryViewModel(modelContext: modelContext)
await viewModel.loadGames()
viewModel.selectedSports = [.mlb]
// Then: Only MLB visits shown
let filteredCount = viewModel.filteredVisits.count
XCTAssertGreaterThanOrEqual(filteredCount, 0, "Filter should work without crashing")
}
func test_GamesHistoryViewModel_SortsMostRecentFirst() async throws {
// Given: Visits on different dates
let oldVisit = StadiumVisit(
stadiumId: "stadium-1",
stadiumNameAtVisit: "Old Stadium",
visitDate: Date().addingTimeInterval(-86400 * 30), // 30 days ago
sport: .mlb,
visitType: .game,
dataSource: .fullyManual
)
let newVisit = StadiumVisit(
stadiumId: "stadium-2",
stadiumNameAtVisit: "New Stadium",
visitDate: Date(),
sport: .mlb,
visitType: .game,
dataSource: .fullyManual
)
modelContext.insert(oldVisit)
modelContext.insert(newVisit)
try modelContext.save()
// When: Loading games
let viewModel = GamesHistoryViewModel(modelContext: modelContext)
await viewModel.loadGames()
// Then: Most recent first within year
let visits = viewModel.allVisits
XCTAssertEqual(visits.first?.stadiumNameAtVisit, "New Stadium", "Most recent should be first")
}
}

View File

@@ -1,48 +0,0 @@
import XCTest
import MapKit
@testable import SportsTime
final class ProgressMapViewTests: XCTestCase {
@MainActor
func test_MapViewModel_TracksUserInteraction() {
// Given: A map view model
let viewModel = MapInteractionViewModel()
// When: User interacts with map (zoom/pan)
viewModel.userDidInteract()
// Then: Interaction is tracked
XCTAssertTrue(viewModel.hasUserInteracted, "Should track user interaction")
XCTAssertTrue(viewModel.shouldShowResetButton, "Should show reset button after interaction")
}
@MainActor
func test_MapViewModel_ResetClearsInteraction() {
// Given: A map with user interaction
let viewModel = MapInteractionViewModel()
viewModel.userDidInteract()
// When: User resets the view
viewModel.resetToDefault()
// Then: Interaction flag is cleared
XCTAssertFalse(viewModel.hasUserInteracted, "Should clear interaction flag after reset")
XCTAssertFalse(viewModel.shouldShowResetButton, "Should hide reset button after reset")
}
@MainActor
func test_MapViewModel_ZoomToStadium_SetsCorrectRegion() {
// Given: A map view model
let viewModel = MapInteractionViewModel()
// When: Zooming to a stadium location
let stadiumCoord = CLLocationCoordinate2D(latitude: 40.8296, longitude: -73.9262) // Yankee Stadium
viewModel.zoomToStadium(at: stadiumCoord)
// Then: Region is set to city-level zoom
XCTAssertEqual(viewModel.region.center.latitude, stadiumCoord.latitude, accuracy: 0.001)
XCTAssertEqual(viewModel.region.center.longitude, stadiumCoord.longitude, accuracy: 0.001)
XCTAssertEqual(viewModel.region.span.latitudeDelta, 0.01, accuracy: 0.005, "Should use city-level zoom span")
}
}

View File

@@ -1,116 +0,0 @@
import XCTest
import SwiftData
@testable import SportsTime
@MainActor
final class VisitListTests: XCTestCase {
var modelContainer: ModelContainer!
var modelContext: ModelContext!
override func setUp() async throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
modelContainer = try ModelContainer(
for: StadiumVisit.self, Achievement.self, UserPreferences.self,
configurations: config
)
modelContext = modelContainer.mainContext
}
override func tearDown() async throws {
modelContainer = nil
modelContext = nil
}
func test_VisitsForStadium_ReturnsAllVisitsSortedByDate() async throws {
// Given: Multiple visits to the same stadium
let stadiumId = "yankee-stadium"
let visit1 = StadiumVisit(
stadiumId: stadiumId,
stadiumNameAtVisit: "Yankee Stadium",
visitDate: Date().addingTimeInterval(-86400 * 30), // 30 days ago
sport: .mlb,
visitType: .game,
dataSource: .fullyManual
)
let visit2 = StadiumVisit(
stadiumId: stadiumId,
stadiumNameAtVisit: "Yankee Stadium",
visitDate: Date().addingTimeInterval(-86400 * 7), // 7 days ago
sport: .mlb,
visitType: .game,
dataSource: .fullyManual
)
let visit3 = StadiumVisit(
stadiumId: stadiumId,
stadiumNameAtVisit: "Yankee Stadium",
visitDate: Date(), // today
sport: .mlb,
visitType: .tour,
dataSource: .fullyManual
)
modelContext.insert(visit1)
modelContext.insert(visit2)
modelContext.insert(visit3)
try modelContext.save()
// When: Fetching visits for that stadium
let descriptor = FetchDescriptor<StadiumVisit>(
predicate: #Predicate { $0.stadiumId == stadiumId },
sortBy: [SortDescriptor(\.visitDate, order: .reverse)]
)
let visits = try modelContext.fetch(descriptor)
// Then: All visits returned, most recent first
XCTAssertEqual(visits.count, 3, "Should return all 3 visits")
XCTAssertEqual(visits[0].visitType, .tour, "Most recent visit should be first")
XCTAssertEqual(visits[2].visitType, .game, "Oldest visit should be last")
}
func test_VisitCountForStadium_ReturnsCorrectCount() async throws {
// Given: 3 visits to one stadium, 1 to another
let stadium1 = "yankee-stadium"
let stadium2 = "fenway-park"
for i in 0..<3 {
let visit = StadiumVisit(
stadiumId: stadium1,
stadiumNameAtVisit: "Yankee Stadium",
visitDate: Date().addingTimeInterval(Double(-i * 86400)),
sport: .mlb,
visitType: .game,
dataSource: .fullyManual
)
modelContext.insert(visit)
}
let fenwayVisit = StadiumVisit(
stadiumId: stadium2,
stadiumNameAtVisit: "Fenway Park",
visitDate: Date(),
sport: .mlb,
visitType: .game,
dataSource: .fullyManual
)
modelContext.insert(fenwayVisit)
try modelContext.save()
// When: Counting visits per stadium
let yankeeDescriptor = FetchDescriptor<StadiumVisit>(
predicate: #Predicate { $0.stadiumId == stadium1 }
)
let fenwayDescriptor = FetchDescriptor<StadiumVisit>(
predicate: #Predicate { $0.stadiumId == stadium2 }
)
let yankeeCount = try modelContext.fetchCount(yankeeDescriptor)
let fenwayCount = try modelContext.fetchCount(fenwayDescriptor)
// Then: Correct counts
XCTAssertEqual(yankeeCount, 3)
XCTAssertEqual(fenwayCount, 1)
}
}