Planning engine fixes (from adversarial code review): - Bug #1: sortByLeisure tie-breaking uses totalDrivingHours - Bug #2: allDates/calculateRestDays guard-let-break prevents infinite loop - Bug #3: same-day trip no longer rejected (>= in dateRange guard) - Bug #4: ScenarioD rationale shows game count not stop count - Bug #5: ScenarioD departureDate advanced to next day after last game - Bug #6: ScenarioC date range boundary uses <= instead of < - Bug #7: DrivingConstraints clamps maxHoursPerDriverPerDay via max(1.0,...) - Bug #8: effectiveTripDuration uses inclusive day counting (+1) - Bug #9: TripWizardViewModel validates endDate >= startDate - Bug #10: allDates() uses min/max instead of first/last for robustness - Bug #12: arrivalBeforeGameStart accounts for game end time at departure - Bug #15: ScenarioBPlanner replaces force unwraps with safe unwrapping Tests: 16 regression test suites + updated existing test expectations Marketing: Remotion canvas set to 886x1920 for App Store preview spec Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
184 lines
5.8 KiB
Swift
184 lines
5.8 KiB
Swift
//
|
|
// TripPlanningEngineTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// TDD specification tests for TripPlanningEngine.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
@Suite("TripPlanningEngine")
|
|
struct TripPlanningEngineTests {
|
|
|
|
// MARK: - Test Data
|
|
|
|
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
|
private let bostonCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
|
|
private let chicagoCoord = CLLocationCoordinate2D(latitude: 41.8827, longitude: -87.6233)
|
|
private let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
|
|
|
|
// MARK: - Specification Tests: Planning Mode Selection
|
|
|
|
@Test("planningMode: dateRange is valid mode")
|
|
func planningMode_dateRange() {
|
|
let prefs = TripPreferences(
|
|
planningMode: .dateRange,
|
|
sports: [.mlb]
|
|
)
|
|
#expect(prefs.planningMode == .dateRange)
|
|
}
|
|
|
|
@Test("planningMode: gameFirst is valid mode")
|
|
func planningMode_gameFirst() {
|
|
let prefs = TripPreferences(
|
|
planningMode: .gameFirst,
|
|
sports: [.mlb],
|
|
mustSeeGameIds: ["game1"]
|
|
)
|
|
#expect(prefs.planningMode == .gameFirst)
|
|
}
|
|
|
|
@Test("planningMode: followTeam is valid mode")
|
|
func planningMode_followTeam() {
|
|
let prefs = TripPreferences(
|
|
planningMode: .followTeam,
|
|
sports: [.mlb],
|
|
followTeamId: "yankees"
|
|
)
|
|
#expect(prefs.planningMode == .followTeam)
|
|
}
|
|
|
|
@Test("planningMode: locations is valid mode")
|
|
func planningMode_locations() {
|
|
let prefs = TripPreferences(
|
|
planningMode: .locations,
|
|
startLocation: LocationInput(name: "Chicago", coordinate: chicagoCoord),
|
|
endLocation: LocationInput(name: "New York", coordinate: nycCoord),
|
|
sports: [.mlb]
|
|
)
|
|
#expect(prefs.planningMode == .locations)
|
|
}
|
|
|
|
// MARK: - Specification Tests: Driving Constraints
|
|
|
|
@Test("DrivingConstraints: calculates maxDailyDrivingHours correctly")
|
|
func drivingConstraints_maxDailyHours() {
|
|
let constraints = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 6.0)
|
|
#expect(constraints.maxDailyDrivingHours == 12.0)
|
|
}
|
|
|
|
@Test("DrivingConstraints: clamps negative drivers to 1")
|
|
func drivingConstraints_clampsNegativeDrivers() {
|
|
let constraints = DrivingConstraints(numberOfDrivers: -5, maxHoursPerDriverPerDay: 8.0)
|
|
#expect(constraints.numberOfDrivers == 1)
|
|
#expect(constraints.maxDailyDrivingHours >= 1.0)
|
|
}
|
|
|
|
@Test("DrivingConstraints: clamps zero hours to minimum")
|
|
func drivingConstraints_clampsZeroHours() {
|
|
let constraints = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 0)
|
|
#expect(constraints.maxHoursPerDriverPerDay == 1.0)
|
|
}
|
|
|
|
// MARK: - Specification Tests: Trip Preferences Computed Properties
|
|
|
|
@Test("totalDriverHoursPerDay: defaults to 8 hours when nil")
|
|
func totalDriverHoursPerDay_default() {
|
|
let prefs = TripPreferences(
|
|
numberOfDrivers: 1,
|
|
maxDrivingHoursPerDriver: nil
|
|
)
|
|
#expect(prefs.totalDriverHoursPerDay == 8.0)
|
|
}
|
|
|
|
@Test("totalDriverHoursPerDay: multiplies by number of drivers")
|
|
func totalDriverHoursPerDay_multipleDrivers() {
|
|
let prefs = TripPreferences(
|
|
numberOfDrivers: 2,
|
|
maxDrivingHoursPerDriver: 6.0
|
|
)
|
|
#expect(prefs.totalDriverHoursPerDay == 12.0)
|
|
}
|
|
|
|
@Test("effectiveTripDuration: uses explicit tripDuration when set")
|
|
func effectiveTripDuration_explicit() {
|
|
let prefs = TripPreferences(
|
|
tripDuration: 5
|
|
)
|
|
#expect(prefs.effectiveTripDuration == 5)
|
|
}
|
|
|
|
@Test("effectiveTripDuration: calculates from date range when tripDuration is nil")
|
|
func effectiveTripDuration_calculated() {
|
|
let calendar = Calendar.current
|
|
let startDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
|
let endDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 22))!
|
|
|
|
let prefs = TripPreferences(
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
tripDuration: nil
|
|
)
|
|
#expect(prefs.effectiveTripDuration == 8) // 8 days inclusive (15th through 22nd)
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
@Test("Invariant: totalDriverHoursPerDay > 0")
|
|
func invariant_totalDriverHoursPositive() {
|
|
let prefs1 = TripPreferences(numberOfDrivers: 1)
|
|
#expect(prefs1.totalDriverHoursPerDay > 0)
|
|
|
|
let prefs2 = TripPreferences(numberOfDrivers: 3, maxDrivingHoursPerDriver: 4)
|
|
#expect(prefs2.totalDriverHoursPerDay > 0)
|
|
}
|
|
|
|
@Test("Invariant: effectiveTripDuration >= 1")
|
|
func invariant_effectiveTripDurationMinimum() {
|
|
let testCases: [Int?] = [nil, 1, 5, 10]
|
|
|
|
for duration in testCases {
|
|
let prefs = TripPreferences(tripDuration: duration)
|
|
#expect(prefs.effectiveTripDuration >= 1)
|
|
}
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
private func makeStadium(
|
|
id: String,
|
|
city: String,
|
|
coordinate: CLLocationCoordinate2D
|
|
) -> Stadium {
|
|
Stadium(
|
|
id: id,
|
|
name: "\(city) Stadium",
|
|
city: city,
|
|
state: "XX",
|
|
latitude: coordinate.latitude,
|
|
longitude: coordinate.longitude,
|
|
capacity: 40000,
|
|
sport: .mlb
|
|
)
|
|
}
|
|
|
|
private func makeGame(
|
|
id: String,
|
|
stadiumId: String,
|
|
dateTime: Date
|
|
) -> Game {
|
|
Game(
|
|
id: id,
|
|
homeTeamId: "team1",
|
|
awayTeamId: "team2",
|
|
stadiumId: stadiumId,
|
|
dateTime: dateTime,
|
|
sport: .mlb,
|
|
season: "2026"
|
|
)
|
|
}
|
|
}
|