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:
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user