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:
296
SportsTimeTests/Planning/ScenarioPlannerFactoryTests.swift
Normal file
296
SportsTimeTests/Planning/ScenarioPlannerFactoryTests.swift
Normal file
@@ -0,0 +1,296 @@
|
||||
//
|
||||
// ScenarioPlannerFactoryTests.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// TDD specification tests for ScenarioPlannerFactory.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import CoreLocation
|
||||
@testable import SportsTime
|
||||
|
||||
@Suite("ScenarioPlannerFactory")
|
||||
@MainActor
|
||||
struct ScenarioPlannerFactoryTests {
|
||||
|
||||
// MARK: - Specification Tests: planner(for:)
|
||||
|
||||
@Test("planner: followTeamId set returns ScenarioDPlanner")
|
||||
func planner_followTeamId_returnsScenarioD() {
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .followTeam,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1,
|
||||
followTeamId: "team-123"
|
||||
)
|
||||
|
||||
let request = makeRequest(preferences: prefs)
|
||||
let planner = ScenarioPlannerFactory.planner(for: request)
|
||||
|
||||
#expect(planner is ScenarioDPlanner)
|
||||
}
|
||||
|
||||
@Test("planner: selectedGames not empty returns ScenarioBPlanner")
|
||||
func planner_selectedGames_returnsScenarioB() {
|
||||
let game = makeGame()
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .gameFirst,
|
||||
sports: [.mlb],
|
||||
mustSeeGameIds: [game.id],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
)
|
||||
|
||||
let request = makeRequest(preferences: prefs, games: [game])
|
||||
let planner = ScenarioPlannerFactory.planner(for: request)
|
||||
|
||||
#expect(planner is ScenarioBPlanner)
|
||||
}
|
||||
|
||||
@Test("planner: start and end locations returns ScenarioCPlanner")
|
||||
func planner_startEndLocations_returnsScenarioC() {
|
||||
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
||||
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .locations,
|
||||
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
|
||||
endLocation: LocationInput(name: "LA", coordinate: laCoord),
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
)
|
||||
|
||||
let request = makeRequest(preferences: prefs)
|
||||
let planner = ScenarioPlannerFactory.planner(for: request)
|
||||
|
||||
#expect(planner is ScenarioCPlanner)
|
||||
}
|
||||
|
||||
@Test("planner: date range only returns ScenarioAPlanner")
|
||||
func planner_dateRangeOnly_returnsScenarioA() {
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
)
|
||||
|
||||
let request = makeRequest(preferences: prefs)
|
||||
let planner = ScenarioPlannerFactory.planner(for: request)
|
||||
|
||||
#expect(planner is ScenarioAPlanner)
|
||||
}
|
||||
|
||||
@Test("planner: priority is D > B > C > A")
|
||||
func planner_priority_DoverBoverCoverA() {
|
||||
// If all conditions are met, followTeamId wins
|
||||
let game = makeGame()
|
||||
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
||||
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .followTeam,
|
||||
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
|
||||
endLocation: LocationInput(name: "LA", coordinate: laCoord), // C condition
|
||||
sports: [.mlb],
|
||||
mustSeeGameIds: [game.id], // B condition
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1,
|
||||
followTeamId: "team-123" // D condition
|
||||
)
|
||||
|
||||
let request = makeRequest(preferences: prefs, games: [game])
|
||||
let planner = ScenarioPlannerFactory.planner(for: request)
|
||||
|
||||
#expect(planner is ScenarioDPlanner, "D should take priority")
|
||||
}
|
||||
|
||||
@Test("planner: B takes priority over C")
|
||||
func planner_priority_BoverC() {
|
||||
// If B and C conditions met but not D, B wins
|
||||
let game = makeGame()
|
||||
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
||||
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .gameFirst,
|
||||
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
|
||||
endLocation: LocationInput(name: "LA", coordinate: laCoord), // C condition
|
||||
sports: [.mlb],
|
||||
mustSeeGameIds: [game.id], // B condition
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
// followTeamId: nil by default - Not D
|
||||
)
|
||||
|
||||
let request = makeRequest(preferences: prefs, games: [game])
|
||||
let planner = ScenarioPlannerFactory.planner(for: request)
|
||||
|
||||
#expect(planner is ScenarioBPlanner, "B should take priority over C")
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: classify()
|
||||
|
||||
@Test("classify: followTeamId returns scenarioD")
|
||||
func classify_followTeamId_returnsScenarioD() {
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .followTeam,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1,
|
||||
followTeamId: "team-123"
|
||||
)
|
||||
|
||||
let request = makeRequest(preferences: prefs)
|
||||
let scenario = ScenarioPlannerFactory.classify(request)
|
||||
|
||||
#expect(scenario == .scenarioD)
|
||||
}
|
||||
|
||||
@Test("classify: selectedGames returns scenarioB")
|
||||
func classify_selectedGames_returnsScenarioB() {
|
||||
let game = makeGame()
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .gameFirst,
|
||||
sports: [.mlb],
|
||||
mustSeeGameIds: [game.id],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
)
|
||||
|
||||
let request = makeRequest(preferences: prefs, games: [game])
|
||||
let scenario = ScenarioPlannerFactory.classify(request)
|
||||
|
||||
#expect(scenario == .scenarioB)
|
||||
}
|
||||
|
||||
@Test("classify: startEndLocations returns scenarioC")
|
||||
func classify_startEndLocations_returnsScenarioC() {
|
||||
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
||||
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .locations,
|
||||
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
|
||||
endLocation: LocationInput(name: "LA", coordinate: laCoord),
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
)
|
||||
|
||||
let request = makeRequest(preferences: prefs)
|
||||
let scenario = ScenarioPlannerFactory.classify(request)
|
||||
|
||||
#expect(scenario == .scenarioC)
|
||||
}
|
||||
|
||||
@Test("classify: dateRangeOnly returns scenarioA")
|
||||
func classify_dateRangeOnly_returnsScenarioA() {
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
)
|
||||
|
||||
let request = makeRequest(preferences: prefs)
|
||||
let scenario = ScenarioPlannerFactory.classify(request)
|
||||
|
||||
#expect(scenario == .scenarioA)
|
||||
}
|
||||
|
||||
// MARK: - Property Tests
|
||||
|
||||
@Test("Property: planner and classify are consistent")
|
||||
func property_plannerAndClassifyConsistent() {
|
||||
// Scenario A
|
||||
let prefsA = TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
)
|
||||
let requestA = makeRequest(preferences: prefsA)
|
||||
let plannerA = ScenarioPlannerFactory.planner(for: requestA)
|
||||
let classifyA = ScenarioPlannerFactory.classify(requestA)
|
||||
#expect(plannerA is ScenarioAPlanner && classifyA == .scenarioA)
|
||||
|
||||
// Scenario D
|
||||
let prefsD = TripPreferences(
|
||||
planningMode: .followTeam,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1,
|
||||
followTeamId: "team-123"
|
||||
)
|
||||
let requestD = makeRequest(preferences: prefsD)
|
||||
let plannerD = ScenarioPlannerFactory.planner(for: requestD)
|
||||
let classifyD = ScenarioPlannerFactory.classify(requestD)
|
||||
#expect(plannerD is ScenarioDPlanner && classifyD == .scenarioD)
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func makeRequest(
|
||||
preferences: TripPreferences,
|
||||
games: [Game] = []
|
||||
) -> PlanningRequest {
|
||||
PlanningRequest(
|
||||
preferences: preferences,
|
||||
availableGames: games,
|
||||
teams: [:],
|
||||
stadiums: [:]
|
||||
)
|
||||
}
|
||||
|
||||
private func makeGame() -> Game {
|
||||
Game(
|
||||
id: "game-\(UUID().uuidString)",
|
||||
homeTeamId: "team1",
|
||||
awayTeamId: "team2",
|
||||
stadiumId: "stadium1",
|
||||
dateTime: Date(),
|
||||
sport: .mlb,
|
||||
season: "2026",
|
||||
isPlayoff: false
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user