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>
294 lines
8.3 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|