Files
Sportstime/SportsTimeTests/Domain/TripPreferencesTests.swift
Trey t 8162b4a029 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>
2026-01-16 14:07:41 -06:00

294 lines
8.3 KiB
Swift

//
// TripPreferencesTests.swift
// SportsTimeTests
//
// TDD specification tests for TripPreferences and related enums.
//
import Testing
import Foundation
import CoreLocation
@testable import SportsTime
@Suite("TripPreferences")
struct TripPreferencesTests {
// MARK: - Specification Tests: totalDriverHoursPerDay
@Test("totalDriverHoursPerDay: uses default 8 hours when maxDrivingHoursPerDriver is 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: nil
)
#expect(prefs.totalDriverHoursPerDay == 16.0) // 8 * 2
}
@Test("totalDriverHoursPerDay: uses custom maxDrivingHoursPerDriver")
func totalDriverHoursPerDay_custom() {
let prefs = TripPreferences(
numberOfDrivers: 2,
maxDrivingHoursPerDriver: 6.0
)
#expect(prefs.totalDriverHoursPerDay == 12.0) // 6 * 2
}
// MARK: - Specification Tests: effectiveTripDuration
@Test("effectiveTripDuration: uses tripDuration when set")
func effectiveTripDuration_explicit() {
let prefs = TripPreferences(
startDate: Date(),
endDate: Date().addingTimeInterval(86400 * 14),
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 == 7) // 7 days between 15th and 22nd
}
@Test("effectiveTripDuration: minimum is 1")
func effectiveTripDuration_minimum() {
let date = Date()
let prefs = TripPreferences(
startDate: date,
endDate: date,
tripDuration: nil
)
#expect(prefs.effectiveTripDuration >= 1)
}
// MARK: - Invariant Tests
@Test("Invariant: totalDriverHoursPerDay > 0")
func invariant_totalDriverHoursPositive() {
// With 1 driver and default
let prefs1 = TripPreferences(numberOfDrivers: 1)
#expect(prefs1.totalDriverHoursPerDay > 0)
// With multiple drivers
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: - LeisureLevel Tests
@Suite("LeisureLevel")
struct LeisureLevelTests {
@Test("restDaysPerWeek: packed = 0.5")
func restDaysPerWeek_packed() {
#expect(LeisureLevel.packed.restDaysPerWeek == 0.5)
}
@Test("restDaysPerWeek: moderate = 1.5")
func restDaysPerWeek_moderate() {
#expect(LeisureLevel.moderate.restDaysPerWeek == 1.5)
}
@Test("restDaysPerWeek: relaxed = 2.5")
func restDaysPerWeek_relaxed() {
#expect(LeisureLevel.relaxed.restDaysPerWeek == 2.5)
}
@Test("maxGamesPerWeek: packed = 7")
func maxGamesPerWeek_packed() {
#expect(LeisureLevel.packed.maxGamesPerWeek == 7)
}
@Test("maxGamesPerWeek: moderate = 5")
func maxGamesPerWeek_moderate() {
#expect(LeisureLevel.moderate.maxGamesPerWeek == 5)
}
@Test("maxGamesPerWeek: relaxed = 3")
func maxGamesPerWeek_relaxed() {
#expect(LeisureLevel.relaxed.maxGamesPerWeek == 3)
}
@Test("Invariant: restDaysPerWeek increases from packed to relaxed")
func invariant_restDaysOrdering() {
#expect(LeisureLevel.packed.restDaysPerWeek < LeisureLevel.moderate.restDaysPerWeek)
#expect(LeisureLevel.moderate.restDaysPerWeek < LeisureLevel.relaxed.restDaysPerWeek)
}
@Test("Invariant: maxGamesPerWeek decreases from packed to relaxed")
func invariant_maxGamesOrdering() {
#expect(LeisureLevel.packed.maxGamesPerWeek > LeisureLevel.moderate.maxGamesPerWeek)
#expect(LeisureLevel.moderate.maxGamesPerWeek > LeisureLevel.relaxed.maxGamesPerWeek)
}
}
// MARK: - RoutePreference Tests
@Suite("RoutePreference")
struct RoutePreferenceTests {
@Test("scenicWeight: direct = 0.0")
func scenicWeight_direct() {
#expect(RoutePreference.direct.scenicWeight == 0.0)
}
@Test("scenicWeight: scenic = 1.0")
func scenicWeight_scenic() {
#expect(RoutePreference.scenic.scenicWeight == 1.0)
}
@Test("scenicWeight: balanced = 0.5")
func scenicWeight_balanced() {
#expect(RoutePreference.balanced.scenicWeight == 0.5)
}
@Test("Invariant: scenicWeight is in range [0, 1]")
func invariant_scenicWeightRange() {
for pref in RoutePreference.allCases {
#expect(pref.scenicWeight >= 0.0)
#expect(pref.scenicWeight <= 1.0)
}
}
}
// MARK: - LocationInput Tests
@Suite("LocationInput")
struct LocationInputTests {
@Test("isResolved: true when coordinate is set")
func isResolved_true() {
let input = LocationInput(
name: "New York",
coordinate: .init(latitude: 40.7, longitude: -74.0)
)
#expect(input.isResolved == true)
}
@Test("isResolved: false when coordinate is nil")
func isResolved_false() {
let input = LocationInput(name: "New York", coordinate: nil)
#expect(input.isResolved == false)
}
@Test("Invariant: isResolved equals (coordinate != nil)")
func invariant_isResolvedConsistent() {
let withCoord = LocationInput(name: "A", coordinate: .init(latitude: 0, longitude: 0))
let withoutCoord = LocationInput(name: "B", coordinate: nil)
#expect(withCoord.isResolved == (withCoord.coordinate != nil))
#expect(withoutCoord.isResolved == (withoutCoord.coordinate != nil))
}
}
// MARK: - PlanningMode Tests
@Suite("PlanningMode")
struct PlanningModeTests {
@Test("Property: all cases have displayName")
func property_allHaveDisplayName() {
for mode in PlanningMode.allCases {
#expect(!mode.displayName.isEmpty)
}
}
@Test("Property: all cases have description")
func property_allHaveDescription() {
for mode in PlanningMode.allCases {
#expect(!mode.description.isEmpty)
}
}
@Test("Property: all cases have iconName")
func property_allHaveIconName() {
for mode in PlanningMode.allCases {
#expect(!mode.iconName.isEmpty)
}
}
@Test("Property: id equals rawValue")
func property_idEqualsRawValue() {
for mode in PlanningMode.allCases {
#expect(mode.id == mode.rawValue)
}
}
}
// MARK: - TravelMode Tests
@Suite("TravelMode")
struct TravelModeTests {
@Test("Property: all cases have displayName")
func property_allHaveDisplayName() {
for mode in TravelMode.allCases {
#expect(!mode.displayName.isEmpty)
}
}
@Test("Property: all cases have iconName")
func property_allHaveIconName() {
for mode in TravelMode.allCases {
#expect(!mode.iconName.isEmpty)
}
}
}
// MARK: - LodgingType Tests
@Suite("LodgingType")
struct LodgingTypeTests {
@Test("Property: all cases have displayName")
func property_allHaveDisplayName() {
for type in LodgingType.allCases {
#expect(!type.displayName.isEmpty)
}
}
@Test("Property: all cases have iconName")
func property_allHaveIconName() {
for type in LodgingType.allCases {
#expect(!type.iconName.isEmpty)
}
}
}