Stabilize unit and UI tests for SportsTime
This commit is contained in:
@@ -331,10 +331,9 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -369,10 +368,9 @@
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -514,7 +512,7 @@
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SportsTime.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SportsTime";
|
||||
};
|
||||
@@ -536,7 +534,7 @@
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SportsTime.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SportsTime";
|
||||
};
|
||||
@@ -556,7 +554,7 @@
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = SportsTime;
|
||||
};
|
||||
@@ -576,7 +574,7 @@
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = SportsTime;
|
||||
};
|
||||
|
||||
@@ -41,6 +41,7 @@ enum UIDesignStyle: String, CaseIterable, Identifiable, Codable {
|
||||
// MARK: - Design Style Manager
|
||||
|
||||
@Observable
|
||||
@MainActor
|
||||
final class DesignStyleManager {
|
||||
static let shared = DesignStyleManager()
|
||||
|
||||
|
||||
@@ -556,6 +556,15 @@ final class CanonicalSport {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sendable Conformance
|
||||
|
||||
// These SwiftData models are passed across actor boundaries during sync operations.
|
||||
// Access is still coordinated by SwiftData contexts and higher-level sync orchestration.
|
||||
extension StadiumAlias: @unchecked Sendable {}
|
||||
extension TeamAlias: @unchecked Sendable {}
|
||||
extension LeagueStructureModel: @unchecked Sendable {}
|
||||
extension CanonicalSport: @unchecked Sendable {}
|
||||
|
||||
// MARK: - Bundled Data Timestamps
|
||||
|
||||
/// Timestamps for bundled data files.
|
||||
|
||||
@@ -78,8 +78,9 @@ final class LocationPermissionManager: NSObject {
|
||||
|
||||
extension LocationPermissionManager: CLLocationManagerDelegate {
|
||||
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
let newStatus = manager.authorizationStatus
|
||||
Task { @MainActor in
|
||||
self.authorizationStatus = manager.authorizationStatus
|
||||
self.authorizationStatus = newStatus
|
||||
self.isRequestingPermission = false
|
||||
|
||||
// Auto-request location if newly authorized
|
||||
|
||||
@@ -7,6 +7,8 @@ import Foundation
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
|
||||
extension MKPolyline: @unchecked Sendable {}
|
||||
|
||||
actor LocationService {
|
||||
static let shared = LocationService()
|
||||
|
||||
@@ -158,7 +160,7 @@ actor LocationService {
|
||||
|
||||
// MARK: - Route Info
|
||||
|
||||
struct RouteInfo {
|
||||
struct RouteInfo: @unchecked Sendable {
|
||||
let distance: CLLocationDistance // meters
|
||||
let expectedTravelTime: TimeInterval // seconds
|
||||
let polyline: MKPolyline?
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
nonisolated final class SyncLogger {
|
||||
final class SyncLogger: @unchecked Sendable {
|
||||
static let shared = SyncLogger()
|
||||
|
||||
private let fileURL: URL
|
||||
|
||||
@@ -108,7 +108,7 @@ final class VisitPhotoService {
|
||||
try modelContext.save()
|
||||
|
||||
// Queue background upload
|
||||
Task.detached { [weak self] in
|
||||
Task { [weak self] in
|
||||
await self?.uploadPhoto(metadata: metadata, image: image)
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ enum AppTheme: String, CaseIterable, Identifiable {
|
||||
// MARK: - Theme Manager
|
||||
|
||||
@Observable
|
||||
final class ThemeManager {
|
||||
final class ThemeManager: @unchecked Sendable {
|
||||
static let shared = ThemeManager()
|
||||
|
||||
var currentTheme: AppTheme {
|
||||
@@ -129,7 +129,7 @@ enum AppearanceMode: String, CaseIterable, Identifiable {
|
||||
// MARK: - Appearance Manager
|
||||
|
||||
@Observable
|
||||
final class AppearanceManager {
|
||||
final class AppearanceManager: @unchecked Sendable {
|
||||
static let shared = AppearanceManager()
|
||||
|
||||
var currentMode: AppearanceMode {
|
||||
|
||||
@@ -821,7 +821,7 @@ final class ExportService {
|
||||
trip: Trip,
|
||||
games: [String: RichGame],
|
||||
itineraryItems: [ItineraryItem]? = nil,
|
||||
progressCallback: ((PDFAssetPrefetcher.PrefetchProgress) async -> Void)? = nil
|
||||
progressCallback: (@Sendable (PDFAssetPrefetcher.PrefetchProgress) async -> Void)? = nil
|
||||
) async throws -> URL {
|
||||
// Prefetch all assets
|
||||
let assets = await assetPrefetcher.prefetchAssets(
|
||||
|
||||
@@ -64,7 +64,7 @@ actor PDFAssetPrefetcher {
|
||||
func prefetchAssets(
|
||||
for trip: Trip,
|
||||
games: [String: RichGame],
|
||||
progressCallback: ((PrefetchProgress) async -> Void)? = nil
|
||||
progressCallback: (@Sendable (PrefetchProgress) async -> Void)? = nil
|
||||
) async -> PrefetchedAssets {
|
||||
var progress = PrefetchProgress()
|
||||
|
||||
|
||||
@@ -511,13 +511,6 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
ItineraryReorderingLogic.travelRow(in: flatItems, forDay: day)
|
||||
}
|
||||
|
||||
deinit {
|
||||
#if DEBUG
|
||||
displayLink?.invalidate()
|
||||
displayLink = nil
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Marketing Video Auto-Scroll
|
||||
|
||||
#if DEBUG
|
||||
|
||||
@@ -51,7 +51,7 @@ struct AnySportTests {
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
private var calendar: Calendar { Calendar.current }
|
||||
private var calendar: Calendar { TestClock.calendar }
|
||||
|
||||
private func date(month: Int) -> Date {
|
||||
calendar.date(from: DateComponents(year: 2026, month: month, day: 15))!
|
||||
|
||||
@@ -35,7 +35,7 @@ struct DynamicSportTests {
|
||||
)
|
||||
}
|
||||
|
||||
private var calendar: Calendar { Calendar.current }
|
||||
private var calendar: Calendar { TestClock.calendar }
|
||||
|
||||
private func date(month: Int) -> Date {
|
||||
calendar.date(from: DateComponents(year: 2026, month: month, day: 15))!
|
||||
|
||||
@@ -32,7 +32,7 @@ struct GameTests {
|
||||
|
||||
@Test("gameDate returns start of day for dateTime")
|
||||
func gameDate_returnsStartOfDay() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
// Game at 7:05 PM
|
||||
let dateTime = calendar.date(from: DateComponents(
|
||||
@@ -54,7 +54,7 @@ struct GameTests {
|
||||
|
||||
@Test("gameDate is same for games on same calendar day")
|
||||
func gameDate_sameDay() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
// Morning game
|
||||
let morningTime = calendar.date(from: DateComponents(
|
||||
@@ -76,7 +76,7 @@ struct GameTests {
|
||||
|
||||
@Test("gameDate differs for games on different calendar days")
|
||||
func gameDate_differentDays() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
let day1 = calendar.date(from: DateComponents(
|
||||
year: 2026, month: 6, day: 15, hour: 19
|
||||
@@ -95,7 +95,7 @@ struct GameTests {
|
||||
|
||||
@Test("startTime is alias for dateTime")
|
||||
func startTime_isAliasForDateTime() {
|
||||
let dateTime = Date()
|
||||
let dateTime = TestClock.now
|
||||
let game = makeGame(dateTime: dateTime)
|
||||
|
||||
#expect(game.startTime == game.dateTime)
|
||||
@@ -105,7 +105,7 @@ struct GameTests {
|
||||
|
||||
@Test("equality based on id only")
|
||||
func equality_basedOnId() {
|
||||
let dateTime = Date()
|
||||
let dateTime = TestClock.now
|
||||
|
||||
let game1 = Game(
|
||||
id: "game1",
|
||||
@@ -135,7 +135,7 @@ struct GameTests {
|
||||
|
||||
@Test("inequality when ids differ")
|
||||
func inequality_differentIds() {
|
||||
let dateTime = Date()
|
||||
let dateTime = TestClock.now
|
||||
|
||||
let game1 = Game(
|
||||
id: "game1",
|
||||
@@ -166,7 +166,7 @@ struct GameTests {
|
||||
|
||||
@Test("Invariant: gameDate is always at midnight")
|
||||
func invariant_gameDateAtMidnight() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
// Test various times throughout the day
|
||||
let times = [0, 6, 12, 18, 23].map { hour in
|
||||
@@ -185,7 +185,7 @@ struct GameTests {
|
||||
@Test("Invariant: startTime equals dateTime")
|
||||
func invariant_startTimeEqualsDateTime() {
|
||||
for _ in 0..<10 {
|
||||
let dateTime = Date().addingTimeInterval(Double.random(in: -86400...86400))
|
||||
let dateTime = TestClock.now.addingTimeInterval(Double.random(in: -86400...86400))
|
||||
let game = makeGame(dateTime: dateTime)
|
||||
#expect(game.startTime == game.dateTime)
|
||||
}
|
||||
@@ -195,7 +195,7 @@ struct GameTests {
|
||||
|
||||
@Test("Property: gameDate is in same calendar day as dateTime")
|
||||
func property_gameDateSameCalendarDay() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
let dateTime = calendar.date(from: DateComponents(
|
||||
year: 2026, month: 7, day: 4, hour: 19, minute: 5
|
||||
|
||||
@@ -344,7 +344,7 @@ struct StadiumVisitStatusTests {
|
||||
|
||||
@Test("isVisited: true for visited status")
|
||||
func isVisited_true() {
|
||||
let visit = makeVisitSummary(date: Date())
|
||||
let visit = makeVisitSummary(date: TestClock.now)
|
||||
let status = StadiumVisitStatus.visited(visits: [visit])
|
||||
|
||||
#expect(status.isVisited == true)
|
||||
@@ -362,9 +362,9 @@ struct StadiumVisitStatusTests {
|
||||
@Test("visitCount: returns count of visits")
|
||||
func visitCount_multiple() {
|
||||
let visits = [
|
||||
makeVisitSummary(date: Date()),
|
||||
makeVisitSummary(date: Date()),
|
||||
makeVisitSummary(date: Date()),
|
||||
makeVisitSummary(date: TestClock.now),
|
||||
makeVisitSummary(date: TestClock.now),
|
||||
makeVisitSummary(date: TestClock.now),
|
||||
]
|
||||
let status = StadiumVisitStatus.visited(visits: visits)
|
||||
|
||||
@@ -382,7 +382,7 @@ struct StadiumVisitStatusTests {
|
||||
|
||||
@Test("latestVisit: returns visit with max date")
|
||||
func latestVisit_maxDate() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let date1 = calendar.date(from: DateComponents(year: 2025, month: 1, day: 1))!
|
||||
let date2 = calendar.date(from: DateComponents(year: 2025, month: 6, day: 15))!
|
||||
let date3 = calendar.date(from: DateComponents(year: 2025, month: 3, day: 10))!
|
||||
@@ -408,7 +408,7 @@ struct StadiumVisitStatusTests {
|
||||
|
||||
@Test("firstVisit: returns visit with min date")
|
||||
func firstVisit_minDate() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let date1 = calendar.date(from: DateComponents(year: 2025, month: 1, day: 1))!
|
||||
let date2 = calendar.date(from: DateComponents(year: 2025, month: 6, day: 15))!
|
||||
let date3 = calendar.date(from: DateComponents(year: 2025, month: 3, day: 10))!
|
||||
@@ -469,7 +469,7 @@ struct VisitSummaryTests {
|
||||
capacity: 40000,
|
||||
sport: .mlb
|
||||
),
|
||||
visitDate: Date(),
|
||||
visitDate: TestClock.now,
|
||||
visitType: .game,
|
||||
sport: .mlb,
|
||||
homeTeamName: homeTeam,
|
||||
|
||||
@@ -67,7 +67,7 @@ struct SportTests {
|
||||
|
||||
@Test("MLB: isInSeason returns true for months 3-10")
|
||||
func mlb_isInSeason_normalRange() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
// In season: March through October
|
||||
for month in 3...10 {
|
||||
@@ -86,7 +86,7 @@ struct SportTests {
|
||||
|
||||
@Test("NBA: isInSeason returns true for months 10-12 and 1-6 (wrap-around)")
|
||||
func nba_isInSeason_wrapAround() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
// In season: October through June (wraps)
|
||||
let inSeasonMonths = [10, 11, 12, 1, 2, 3, 4, 5, 6]
|
||||
@@ -104,7 +104,7 @@ struct SportTests {
|
||||
|
||||
@Test("NFL: isInSeason returns true for months 9-12 and 1-2 (wrap-around)")
|
||||
func nfl_isInSeason_wrapAround() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
// In season: September through February (wraps)
|
||||
let inSeasonMonths = [9, 10, 11, 12, 1, 2]
|
||||
@@ -124,7 +124,7 @@ struct SportTests {
|
||||
|
||||
@Test("isInSeason boundary: first and last day of season month")
|
||||
func isInSeason_boundaryDays() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
// MLB: First day of March (in season)
|
||||
let marchFirst = calendar.date(from: DateComponents(year: 2026, month: 3, day: 1))!
|
||||
|
||||
@@ -24,8 +24,8 @@ struct TripPollTests {
|
||||
preferences: TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7)
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7)
|
||||
),
|
||||
stops: stops
|
||||
)
|
||||
@@ -96,7 +96,7 @@ struct TripPollTests {
|
||||
|
||||
@Test("computeTripHash: different trips produce different hashes")
|
||||
func computeTripHash_differentTrips() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let date1 = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
let date2 = calendar.date(from: DateComponents(year: 2026, month: 6, day: 16))!
|
||||
|
||||
@@ -266,8 +266,8 @@ struct PollResultsTests {
|
||||
preferences: TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7)
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ struct TripPreferencesTests {
|
||||
@Test("effectiveTripDuration: uses tripDuration when set")
|
||||
func effectiveTripDuration_explicit() {
|
||||
let prefs = TripPreferences(
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 14),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 14),
|
||||
tripDuration: 5
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ struct TripPreferencesTests {
|
||||
|
||||
@Test("effectiveTripDuration: calculates from date range when tripDuration is nil")
|
||||
func effectiveTripDuration_calculated() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let startDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
let endDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 22))!
|
||||
|
||||
@@ -75,7 +75,7 @@ struct TripPreferencesTests {
|
||||
|
||||
@Test("effectiveTripDuration: minimum is 1")
|
||||
func effectiveTripDuration_minimum() {
|
||||
let date = Date()
|
||||
let date = TestClock.now
|
||||
let prefs = TripPreferences(
|
||||
startDate: date,
|
||||
endDate: date,
|
||||
|
||||
@@ -33,7 +33,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("stayDuration: same day arrival and departure returns 1")
|
||||
func stayDuration_sameDay() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let arrivalDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
let departureDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
|
||||
@@ -44,7 +44,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("stayDuration: 2-day stay returns 2")
|
||||
func stayDuration_twoDays() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let arrivalDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
let departureDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 16))!
|
||||
|
||||
@@ -55,7 +55,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("stayDuration: week-long stay")
|
||||
func stayDuration_weekLong() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let arrivalDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
let departureDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 22))!
|
||||
|
||||
@@ -66,7 +66,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("stayDuration: minimum is 1 even if dates are reversed")
|
||||
func stayDuration_minimumIsOne() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let arrivalDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 20))!
|
||||
let departureDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
|
||||
@@ -79,7 +79,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("hasGames: true when games array is non-empty")
|
||||
func hasGames_true() {
|
||||
let now = Date()
|
||||
let now = TestClock.now
|
||||
let stop = makeStop(arrivalDate: now, departureDate: now, games: ["game1", "game2"])
|
||||
|
||||
#expect(stop.hasGames == true)
|
||||
@@ -87,7 +87,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("hasGames: false when games array is empty")
|
||||
func hasGames_false() {
|
||||
let now = Date()
|
||||
let now = TestClock.now
|
||||
let stop = makeStop(arrivalDate: now, departureDate: now, games: [])
|
||||
|
||||
#expect(stop.hasGames == false)
|
||||
@@ -97,7 +97,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("formattedDateRange: single date for 1-day stay")
|
||||
func formattedDateRange_singleDay() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let date = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
|
||||
let stop = makeStop(arrivalDate: date, departureDate: date)
|
||||
@@ -108,7 +108,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("formattedDateRange: range for multi-day stay")
|
||||
func formattedDateRange_multiDay() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let arrival = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
let departure = calendar.date(from: DateComponents(year: 2026, month: 6, day: 18))!
|
||||
|
||||
@@ -126,8 +126,8 @@ struct TripStopTests {
|
||||
stopNumber: 1,
|
||||
city: "Boston",
|
||||
state: "MA",
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date()
|
||||
arrivalDate: TestClock.now,
|
||||
departureDate: TestClock.now
|
||||
)
|
||||
|
||||
#expect(stop.locationDescription == "Boston, MA")
|
||||
@@ -137,7 +137,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("Invariant: stayDuration >= 1")
|
||||
func invariant_stayDurationAtLeastOne() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
// Test various date combinations
|
||||
let testCases: [(arrival: DateComponents, departure: DateComponents)] = [
|
||||
@@ -158,7 +158,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("Invariant: hasGames equals !games.isEmpty")
|
||||
func invariant_hasGamesConsistent() {
|
||||
let now = Date()
|
||||
let now = TestClock.now
|
||||
|
||||
let stopWithGames = makeStop(arrivalDate: now, departureDate: now, games: ["game1"])
|
||||
#expect(stopWithGames.hasGames == !stopWithGames.games.isEmpty)
|
||||
@@ -171,7 +171,7 @@ struct TripStopTests {
|
||||
|
||||
@Test("Property: isRestDay defaults to false")
|
||||
func property_isRestDayDefault() {
|
||||
let now = Date()
|
||||
let now = TestClock.now
|
||||
let stop = makeStop(arrivalDate: now, departureDate: now)
|
||||
|
||||
#expect(stop.isRestDay == false)
|
||||
@@ -183,8 +183,8 @@ struct TripStopTests {
|
||||
stopNumber: 1,
|
||||
city: "City",
|
||||
state: "ST",
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date(),
|
||||
arrivalDate: TestClock.now,
|
||||
departureDate: TestClock.now,
|
||||
isRestDay: true
|
||||
)
|
||||
|
||||
@@ -198,8 +198,8 @@ struct TripStopTests {
|
||||
city: "City",
|
||||
state: "ST",
|
||||
coordinate: nil,
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date(),
|
||||
arrivalDate: TestClock.now,
|
||||
departureDate: TestClock.now,
|
||||
stadium: nil,
|
||||
lodging: nil,
|
||||
notes: nil
|
||||
|
||||
@@ -14,14 +14,14 @@ struct TripTests {
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
private var calendar: Calendar { Calendar.current }
|
||||
private var calendar: Calendar { TestClock.calendar }
|
||||
|
||||
private func makePreferences() -> TripPreferences {
|
||||
TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7)
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ struct TripTests {
|
||||
|
||||
@Test("tripDuration: minimum is 1 day")
|
||||
func tripDuration_minimumIsOne() {
|
||||
let date = Date()
|
||||
let date = TestClock.now
|
||||
let stop = makeStop(city: "NYC", arrivalDate: date, departureDate: date)
|
||||
|
||||
let trip = Trip(
|
||||
@@ -160,7 +160,7 @@ struct TripTests {
|
||||
|
||||
@Test("cities: returns deduplicated list preserving order")
|
||||
func cities_deduplicatedPreservingOrder() {
|
||||
let date = Date()
|
||||
let date = TestClock.now
|
||||
|
||||
let stop1 = makeStop(city: "NYC", arrivalDate: date, departureDate: date)
|
||||
let stop2 = makeStop(city: "Boston", arrivalDate: date, departureDate: date)
|
||||
@@ -191,7 +191,7 @@ struct TripTests {
|
||||
|
||||
@Test("displayName: uses arrow separator between cities")
|
||||
func displayName_arrowSeparator() {
|
||||
let date = Date()
|
||||
let date = TestClock.now
|
||||
|
||||
let stop1 = makeStop(city: "NYC", arrivalDate: date, departureDate: date)
|
||||
let stop2 = makeStop(city: "Boston", arrivalDate: date, departureDate: date)
|
||||
@@ -267,7 +267,7 @@ struct TripTests {
|
||||
|
||||
@Test("Invariant: cities has no duplicates")
|
||||
func invariant_citiesNoDuplicates() {
|
||||
let date = Date()
|
||||
let date = TestClock.now
|
||||
|
||||
// Create stops with duplicate cities
|
||||
let stops = [
|
||||
|
||||
@@ -26,7 +26,7 @@ final class ItineraryReorderingLogicTests: XCTestCase {
|
||||
for element in elements {
|
||||
switch element {
|
||||
case .day(let num):
|
||||
let date = Calendar.current.date(byAdding: .day, value: num - 1, to: testDate)!
|
||||
let date = TestClock.calendar.date(byAdding: .day, value: num - 1, to: testDate)!
|
||||
items.append(.dayHeader(dayNumber: num, date: date))
|
||||
|
||||
case .game(let city, let day):
|
||||
|
||||
@@ -14,15 +14,15 @@ struct ItinerarySectionBuilderTests {
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func makeTripDays(count: Int, startDate: Date = Date()) -> [Date] {
|
||||
private func makeTripDays(count: Int, startDate: Date = TestClock.now) -> [Date] {
|
||||
(0..<count).map {
|
||||
Calendar.current.date(byAdding: .day, value: $0, to: Calendar.current.startOfDay(for: startDate))!
|
||||
TestClock.calendar.date(byAdding: .day, value: $0, to: TestClock.calendar.startOfDay(for: startDate))!
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTrip(
|
||||
cities: [String] = ["New York", "Boston"],
|
||||
startDate: Date = Date(),
|
||||
startDate: Date = TestClock.now,
|
||||
daysPerStop: Int = 1,
|
||||
gameIds: [[String]] = []
|
||||
) -> (Trip, [Date]) {
|
||||
@@ -50,7 +50,7 @@ struct ItinerarySectionBuilderTests {
|
||||
|
||||
@Test("builds one section per day")
|
||||
func buildsSectionsForEachDay() {
|
||||
let startDate = Calendar.current.startOfDay(for: Date())
|
||||
let startDate = TestClock.calendar.startOfDay(for: TestClock.now)
|
||||
let (trip, days) = makeTrip(cities: ["New York", "Boston", "Philadelphia"], startDate: startDate)
|
||||
|
||||
let sections = ItinerarySectionBuilder.build(
|
||||
@@ -72,7 +72,7 @@ struct ItinerarySectionBuilderTests {
|
||||
|
||||
@Test("games filtered correctly by date")
|
||||
func gamesOnFiltersCorrectly() {
|
||||
let startDate = Calendar.current.startOfDay(for: Date())
|
||||
let startDate = TestClock.calendar.startOfDay(for: TestClock.now)
|
||||
let gameDate = startDate
|
||||
let game = TestFixtures.game(sport: .mlb, city: "New York", dateTime: gameDate)
|
||||
let richGame = TestFixtures.richGame(game: game, homeCity: "New York")
|
||||
@@ -108,7 +108,7 @@ struct ItinerarySectionBuilderTests {
|
||||
|
||||
@Test("travel segments appear in sections")
|
||||
func travelSegmentsAppear() {
|
||||
let startDate = Calendar.current.startOfDay(for: Date())
|
||||
let startDate = TestClock.calendar.startOfDay(for: TestClock.now)
|
||||
let travel = TestFixtures.travelSegment(from: "New York", to: "Boston")
|
||||
let (baseTrip, days) = makeTrip(
|
||||
cities: ["New York", "Boston"],
|
||||
@@ -153,7 +153,7 @@ struct ItinerarySectionBuilderTests {
|
||||
|
||||
@Test("custom items included when allowCustomItems is true")
|
||||
func customItemsIncluded() {
|
||||
let startDate = Calendar.current.startOfDay(for: Date())
|
||||
let startDate = TestClock.calendar.startOfDay(for: TestClock.now)
|
||||
let (trip, days) = makeTrip(cities: ["New York"], startDate: startDate)
|
||||
|
||||
let customItem = ItineraryItem(
|
||||
|
||||
@@ -249,13 +249,13 @@ final class ItinerarySemanticTravelTests: XCTestCase {
|
||||
/// For each proposedRow, simulate → compute (day, sortOrder) → constraints.isValidPosition must match.
|
||||
func test_E_computeValidDestinationRows_matchesConstraintsValidation() {
|
||||
let gameA = H.makeRichGame(city: "CityA", hour: 19, baseDate: testDate)
|
||||
let gameBDate = Calendar.current.date(byAdding: .day, value: 3, to: testDate)!
|
||||
let gameBDate = TestClock.calendar.date(byAdding: .day, value: 3, to: testDate)!
|
||||
let gameB = H.makeRichGame(city: "CityB", hour: 19, baseDate: gameBDate)
|
||||
let travel = H.makeTravelSegment(from: "CityA", to: "CityB")
|
||||
|
||||
let day2Date = Calendar.current.date(byAdding: .day, value: 1, to: testDate)!
|
||||
let day3Date = Calendar.current.date(byAdding: .day, value: 2, to: testDate)!
|
||||
let day4Date = Calendar.current.date(byAdding: .day, value: 3, to: testDate)!
|
||||
let day2Date = TestClock.calendar.date(byAdding: .day, value: 1, to: testDate)!
|
||||
let day3Date = TestClock.calendar.date(byAdding: .day, value: 2, to: testDate)!
|
||||
let day4Date = TestClock.calendar.date(byAdding: .day, value: 3, to: testDate)!
|
||||
|
||||
let items: [ItineraryRowItem] = [
|
||||
.dayHeader(dayNumber: 1, date: testDate),
|
||||
@@ -316,7 +316,7 @@ final class ItinerarySemanticTravelTests: XCTestCase {
|
||||
func test_E_customItemValidDestinations_matchesConstraints() {
|
||||
let game = H.makeRichGame(city: "Detroit", hour: 19, baseDate: testDate)
|
||||
let customItem = H.makeCustomItem(day: 1, sortOrder: 2.0, title: "Lunch")
|
||||
let day2Date = Calendar.current.date(byAdding: .day, value: 1, to: testDate)!
|
||||
let day2Date = TestClock.calendar.date(byAdding: .day, value: 1, to: testDate)!
|
||||
|
||||
let items: [ItineraryRowItem] = [
|
||||
.dayHeader(dayNumber: 1, date: testDate),
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
/// Shared test fixtures for itinerary tests
|
||||
enum ItineraryTestHelpers {
|
||||
static let testTripId = UUID()
|
||||
static let testDate = Date()
|
||||
static let testDate = TestClock.now
|
||||
|
||||
// MARK: - Day Helpers
|
||||
|
||||
@@ -20,7 +20,7 @@ enum ItineraryTestHelpers {
|
||||
ItineraryDayData(
|
||||
id: i + 1,
|
||||
dayNumber: i + 1,
|
||||
date: Calendar.current.date(byAdding: .day, value: i, to: baseDate)!,
|
||||
date: TestClock.calendar.date(byAdding: .day, value: i, to: baseDate)!,
|
||||
games: [],
|
||||
items: [],
|
||||
travelBefore: nil
|
||||
@@ -29,7 +29,7 @@ enum ItineraryTestHelpers {
|
||||
}
|
||||
|
||||
static func dayAfter(_ date: Date) -> Date {
|
||||
Calendar.current.date(byAdding: .day, value: 1, to: date)!
|
||||
TestClock.calendar.date(byAdding: .day, value: 1, to: date)!
|
||||
}
|
||||
|
||||
// MARK: - Travel Helpers
|
||||
@@ -56,9 +56,9 @@ enum ItineraryTestHelpers {
|
||||
// MARK: - Game Helpers
|
||||
|
||||
static func makeRichGame(city: String, hour: Int, baseDate: Date = testDate) -> RichGame {
|
||||
var dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: baseDate)
|
||||
var dateComponents = TestClock.calendar.dateComponents([.year, .month, .day], from: baseDate)
|
||||
dateComponents.hour = hour
|
||||
let gameTime = Calendar.current.date(from: dateComponents)!
|
||||
let gameTime = TestClock.calendar.date(from: dateComponents)!
|
||||
|
||||
let game = Game(
|
||||
id: "game-\(city)-\(UUID().uuidString.prefix(4))",
|
||||
|
||||
@@ -14,7 +14,7 @@ final class TravelPlacementTests: XCTestCase {
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private let calendar = Calendar.current
|
||||
private let calendar = TestClock.calendar
|
||||
|
||||
/// Create a date for May 2026 at a given day number.
|
||||
private func may(_ day: Int) -> Date {
|
||||
|
||||
48
SportsTimeTests/Helpers/TestClock.swift
Normal file
48
SportsTimeTests/Helpers/TestClock.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// TestClock.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// Centralized time utilities for deterministic tests.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TestClock {
|
||||
static let timeZone = TimeZone.current
|
||||
static let locale = Locale(identifier: "en_US_POSIX")
|
||||
|
||||
static let calendar: Calendar = {
|
||||
var calendar = Calendar.current
|
||||
calendar.timeZone = timeZone
|
||||
calendar.locale = locale
|
||||
return calendar
|
||||
}()
|
||||
|
||||
static let baseDate: Date = {
|
||||
let components = DateComponents(
|
||||
calendar: calendar,
|
||||
timeZone: timeZone,
|
||||
year: 2026,
|
||||
month: 1,
|
||||
day: 15,
|
||||
hour: 12,
|
||||
minute: 0,
|
||||
second: 0
|
||||
)
|
||||
return calendar.date(from: components) ?? Date(timeIntervalSince1970: 0)
|
||||
}()
|
||||
|
||||
static var now: Date { baseDate }
|
||||
|
||||
static func startOfDay(for date: Date = baseDate) -> Date {
|
||||
calendar.startOfDay(for: date)
|
||||
}
|
||||
|
||||
static func addingDays(_ days: Int, to date: Date = baseDate) -> Date {
|
||||
calendar.date(byAdding: .day, value: days, to: date) ?? date
|
||||
}
|
||||
|
||||
static func addingHours(_ hours: Int, to date: Date = baseDate) -> Date {
|
||||
calendar.date(byAdding: .hour, value: hours, to: date) ?? date
|
||||
}
|
||||
}
|
||||
@@ -86,13 +86,15 @@ enum TestFixtures {
|
||||
season: String = "2026",
|
||||
isPlayoff: Bool = false
|
||||
) -> Game {
|
||||
let actualDateTime = dateTime ?? Calendar.current.date(byAdding: .day, value: 1, to: Date())!
|
||||
let actualDateTime = dateTime ?? TestClock.calendar.date(byAdding: .day, value: 1, to: TestClock.now)!
|
||||
let homeId = homeTeamId ?? "team_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))"
|
||||
let awayId = awayTeamId ?? "team_\(sport.rawValue.lowercased())_visitor"
|
||||
let stadId = stadiumId ?? "stadium_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))"
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMdd"
|
||||
formatter.timeZone = TestClock.timeZone
|
||||
formatter.locale = TestClock.locale
|
||||
let dateStr = formatter.string(from: actualDateTime)
|
||||
|
||||
let actualId = id ?? "game_\(sport.rawValue.lowercased())_\(season)_\(awayId.split(separator: "_").last ?? "vis")_\(homeId.split(separator: "_").last ?? "home")_\(dateStr)"
|
||||
@@ -119,12 +121,12 @@ enum TestFixtures {
|
||||
count: Int,
|
||||
sport: Sport = .mlb,
|
||||
cities: [String] = ["New York", "Boston", "Chicago", "Los Angeles"],
|
||||
startDate: Date = Date(),
|
||||
startDate: Date = TestClock.now,
|
||||
daySpread: Int = 1
|
||||
) -> [Game] {
|
||||
(0..<count).map { i in
|
||||
let city = cities[i % cities.count]
|
||||
let gameDate = Calendar.current.date(byAdding: .day, value: i * daySpread, to: startDate)!
|
||||
let gameDate = TestClock.calendar.date(byAdding: .day, value: i * daySpread, to: startDate)!
|
||||
return game(sport: sport, city: city, dateTime: gameDate)
|
||||
}
|
||||
}
|
||||
@@ -132,12 +134,12 @@ enum TestFixtures {
|
||||
/// Creates games for same-day conflict testing.
|
||||
static func sameDayGames(
|
||||
cities: [String],
|
||||
date: Date = Date(),
|
||||
date: Date = TestClock.now,
|
||||
sport: Sport = .mlb
|
||||
) -> [Game] {
|
||||
cities.enumerated().map { index, city in
|
||||
// Stagger times by 3 hours
|
||||
let time = Calendar.current.date(byAdding: .hour, value: 13 + (index * 3), to: Calendar.current.startOfDay(for: date))!
|
||||
let time = TestClock.calendar.date(byAdding: .hour, value: 13 + (index * 3), to: TestClock.calendar.startOfDay(for: date))!
|
||||
return game(sport: sport, city: city, dateTime: time)
|
||||
}
|
||||
}
|
||||
@@ -241,8 +243,8 @@ enum TestFixtures {
|
||||
) -> TripStop {
|
||||
let coordinate = coordinates[city]
|
||||
let actualState = state ?? states[city] ?? "NY"
|
||||
let arrival = arrivalDate ?? Date()
|
||||
let departure = departureDate ?? Calendar.current.date(byAdding: .day, value: 1, to: arrival)!
|
||||
let arrival = arrivalDate ?? TestClock.now
|
||||
let departure = departureDate ?? TestClock.calendar.date(byAdding: .day, value: 1, to: arrival)!
|
||||
|
||||
return TripStop(
|
||||
stopNumber: stopNumber,
|
||||
@@ -259,14 +261,14 @@ enum TestFixtures {
|
||||
/// Creates a sequence of trip stops for a multi-city trip.
|
||||
static func tripStops(
|
||||
cities: [String],
|
||||
startDate: Date = Date(),
|
||||
startDate: Date = TestClock.now,
|
||||
daysPerStop: Int = 1
|
||||
) -> [TripStop] {
|
||||
var stops: [TripStop] = []
|
||||
var currentDate = startDate
|
||||
|
||||
for (index, city) in cities.enumerated() {
|
||||
let departure = Calendar.current.date(byAdding: .day, value: daysPerStop, to: currentDate)!
|
||||
let departure = TestClock.calendar.date(byAdding: .day, value: daysPerStop, to: currentDate)!
|
||||
stops.append(tripStop(
|
||||
stopNumber: index + 1,
|
||||
city: city,
|
||||
@@ -317,8 +319,8 @@ enum TestFixtures {
|
||||
needsEVCharging: Bool = false,
|
||||
maxDrivingHoursPerDriver: Double? = nil
|
||||
) -> TripPreferences {
|
||||
let start = startDate ?? Date()
|
||||
let end = endDate ?? Calendar.current.date(byAdding: .day, value: 7, to: start)!
|
||||
let start = startDate ?? TestClock.now
|
||||
let end = endDate ?? TestClock.calendar.date(byAdding: .day, value: 7, to: start)!
|
||||
|
||||
return TripPreferences(
|
||||
planningMode: mode,
|
||||
@@ -416,12 +418,12 @@ enum TestFixtures {
|
||||
components.hour = hour
|
||||
components.minute = minute
|
||||
components.timeZone = TimeZone(identifier: "America/New_York")
|
||||
return Calendar.current.date(from: components)!
|
||||
return TestClock.calendar.date(from: components)!
|
||||
}
|
||||
|
||||
/// Creates dates for a range of days.
|
||||
static func dateRange(start: Date = Date(), days: Int) -> (start: Date, end: Date) {
|
||||
let end = Calendar.current.date(byAdding: .day, value: days, to: start)!
|
||||
static func dateRange(start: Date = TestClock.now, days: Int) -> (start: Date, end: Date) {
|
||||
let end = TestClock.calendar.date(byAdding: .day, value: days, to: start)!
|
||||
return (start, end)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ struct GameDAGRouterTests {
|
||||
private let laCoord = CLLocationCoordinate2D(latitude: 34.0141, longitude: -118.2879)
|
||||
private let seattleCoord = CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3316)
|
||||
|
||||
private let calendar = Calendar.current
|
||||
private let calendar = TestClock.calendar
|
||||
|
||||
// MARK: - Specification Tests: Edge Cases
|
||||
|
||||
@@ -40,7 +40,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: single game with no anchors returns single-game route")
|
||||
func findRoutes_singleGame_noAnchors_returnsSingleRoute() {
|
||||
let (game, stadium) = makeGameAndStadium(city: "New York", date: Date())
|
||||
let (game, stadium) = makeGameAndStadium(city: "New York", date: TestClock.now)
|
||||
|
||||
let routes = GameDAGRouter.findRoutes(
|
||||
games: [game],
|
||||
@@ -55,7 +55,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: single game matching anchor returns single-game route")
|
||||
func findRoutes_singleGame_matchingAnchor_returnsSingleRoute() {
|
||||
let (game, stadium) = makeGameAndStadium(city: "New York", date: Date())
|
||||
let (game, stadium) = makeGameAndStadium(city: "New York", date: TestClock.now)
|
||||
|
||||
let routes = GameDAGRouter.findRoutes(
|
||||
games: [game],
|
||||
@@ -70,7 +70,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: single game not matching anchor returns empty")
|
||||
func findRoutes_singleGame_notMatchingAnchor_returnsEmpty() {
|
||||
let (game, stadium) = makeGameAndStadium(city: "New York", date: Date())
|
||||
let (game, stadium) = makeGameAndStadium(city: "New York", date: TestClock.now)
|
||||
|
||||
let routes = GameDAGRouter.findRoutes(
|
||||
games: [game],
|
||||
@@ -86,7 +86,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: two feasible games returns combined route")
|
||||
func findRoutes_twoFeasibleGames_returnsCombinedRoute() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let game1Date = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: today)!
|
||||
let game2Date = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: today)!
|
||||
|
||||
@@ -112,7 +112,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: two infeasible same-day games returns separate routes when no anchors")
|
||||
func findRoutes_twoInfeasibleGames_noAnchors_returnsSeparateRoutes() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let game1Date = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: today)!
|
||||
let game2Date = calendar.date(bySettingHour: 15, minute: 0, second: 0, of: today)! // Only 2 hours later
|
||||
|
||||
@@ -135,7 +135,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: two infeasible games with both as anchors returns empty")
|
||||
func findRoutes_twoInfeasibleGames_bothAnchors_returnsEmpty() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let game1Date = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: today)!
|
||||
let game2Date = calendar.date(bySettingHour: 15, minute: 0, second: 0, of: today)!
|
||||
|
||||
@@ -158,7 +158,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: routes contain all anchor games")
|
||||
func findRoutes_routesContainAllAnchors() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let dates = (0..<5).map { dayOffset in
|
||||
calendar.date(byAdding: .day, value: dayOffset, to: today)!
|
||||
}
|
||||
@@ -199,7 +199,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: allowRepeatCities=false excludes routes with duplicate cities")
|
||||
func findRoutes_disallowRepeatCities_excludesDuplicates() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let dates = (0..<3).map { dayOffset in
|
||||
calendar.date(byAdding: .day, value: dayOffset, to: today)!
|
||||
}
|
||||
@@ -228,7 +228,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: allowRepeatCities=true allows routes with duplicate cities")
|
||||
func findRoutes_allowRepeatCities_allowsDuplicates() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let dates = (0..<3).map { dayOffset in
|
||||
calendar.date(byAdding: .day, value: dayOffset, to: today)!
|
||||
}
|
||||
@@ -270,7 +270,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: all routes are chronologically ordered")
|
||||
func findRoutes_allRoutesChronological() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let dates = (0..<5).map { dayOffset in
|
||||
calendar.date(byAdding: .day, value: dayOffset, to: today)!
|
||||
}
|
||||
@@ -307,7 +307,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: respects maxDailyDrivingHours for same-day games")
|
||||
func findRoutes_respectsSameDayDrivingLimit() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let game1Time = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: today)!
|
||||
let game2Time = calendar.date(bySettingHour: 20, minute: 0, second: 0, of: today)!
|
||||
|
||||
@@ -331,7 +331,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: multi-day trips allow longer total driving")
|
||||
func findRoutes_multiDayTrips_allowLongerDriving() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let game1Date = today
|
||||
let game2Date = calendar.date(byAdding: .day, value: 2, to: today)! // 2 days later
|
||||
|
||||
@@ -355,7 +355,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("findRoutes: anchor routes can span gaps larger than 5 days")
|
||||
func findRoutes_anchorRoutesAllowLongDateGaps() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let day0 = today
|
||||
let day1 = calendar.date(byAdding: .day, value: 1, to: today)!
|
||||
let day8 = calendar.date(byAdding: .day, value: 8, to: today)!
|
||||
@@ -387,7 +387,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("Property: route count never exceeds maxOptions (75)")
|
||||
func property_routeCountNeverExceedsMax() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
|
||||
// Create many games to stress test
|
||||
var games: [Game] = []
|
||||
@@ -413,7 +413,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("Property: all routes satisfy constraints")
|
||||
func property_allRoutesSatisfyConstraints() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let dates = (0..<5).map { calendar.date(byAdding: .day, value: $0, to: today)! }
|
||||
|
||||
let gamesAndStadiums = [
|
||||
@@ -469,7 +469,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("Edge: games at same stadium always feasible")
|
||||
func edge_sameStadium_alwaysFeasible() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let game1Time = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: today)!
|
||||
let game2Time = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: today)! // Doubleheader
|
||||
|
||||
@@ -489,7 +489,7 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("Edge: games out of order are sorted chronologically")
|
||||
func edge_unsortedGames_areSorted() {
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let today = calendar.startOfDay(for: TestClock.now)
|
||||
let game1Date = calendar.date(byAdding: .day, value: 2, to: today)!
|
||||
let game2Date = today
|
||||
let game3Date = calendar.date(byAdding: .day, value: 1, to: today)!
|
||||
@@ -517,8 +517,8 @@ struct GameDAGRouterTests {
|
||||
|
||||
@Test("Edge: missing stadium for game is handled gracefully")
|
||||
func edge_missingStadium_handledGracefully() {
|
||||
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: Date(), coord: nycCoord)
|
||||
let game2 = makeGame(stadiumId: "nonexistent-stadium", date: Date().addingTimeInterval(86400))
|
||||
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: TestClock.now, coord: nycCoord)
|
||||
let game2 = makeGame(stadiumId: "nonexistent-stadium", date: TestClock.now.addingTimeInterval(86400))
|
||||
|
||||
// Only provide stadium for game1
|
||||
let stadiums = [stadium1.id: stadium1]
|
||||
|
||||
@@ -22,7 +22,7 @@ struct ItineraryBuilderTests {
|
||||
private let chicagoCoord = CLLocationCoordinate2D(latitude: 41.8827, longitude: -87.6233)
|
||||
private let seattleCoord = CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3316)
|
||||
|
||||
private let calendar = Calendar.current
|
||||
private let calendar = TestClock.calendar
|
||||
|
||||
// MARK: - Specification Tests: build()
|
||||
|
||||
@@ -203,7 +203,7 @@ struct ItineraryBuilderTests {
|
||||
|
||||
@Test("arrivalBeforeGameStart: sufficient time passes")
|
||||
func arrivalBeforeGameStart_sufficientTime_passes() {
|
||||
let now = Date()
|
||||
let now = TestClock.now
|
||||
let tomorrow = calendar.date(byAdding: .day, value: 1, to: now)!
|
||||
let gameTime = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: tomorrow)!
|
||||
|
||||
@@ -232,7 +232,7 @@ struct ItineraryBuilderTests {
|
||||
|
||||
@Test("arrivalBeforeGameStart: insufficient time fails")
|
||||
func arrivalBeforeGameStart_insufficientTime_fails() {
|
||||
let now = Date()
|
||||
let now = TestClock.now
|
||||
let gameTime = now.addingTimeInterval(2 * 3600) // Game in 2 hours
|
||||
|
||||
let stop1 = makeStop(
|
||||
@@ -349,7 +349,7 @@ struct ItineraryBuilderTests {
|
||||
private func makeStop(
|
||||
city: String,
|
||||
coordinate: CLLocationCoordinate2D?,
|
||||
departureDate: Date = Date(),
|
||||
departureDate: Date = TestClock.now,
|
||||
firstGameStart: Date? = nil
|
||||
) -> ItineraryStop {
|
||||
ItineraryStop(
|
||||
|
||||
@@ -306,8 +306,8 @@ struct PlanningModelsTests {
|
||||
state: "XX",
|
||||
coordinate: nil,
|
||||
games: games,
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date(),
|
||||
arrivalDate: TestClock.now,
|
||||
departureDate: TestClock.now,
|
||||
location: LocationInput(name: "Test", coordinate: nil),
|
||||
firstGameStart: nil
|
||||
)
|
||||
@@ -348,8 +348,8 @@ struct PlanningModelsTests {
|
||||
state: "NY",
|
||||
coordinate: nil,
|
||||
games: ["g1"],
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date(),
|
||||
arrivalDate: TestClock.now,
|
||||
departureDate: TestClock.now,
|
||||
location: LocationInput(name: "NY", coordinate: nil),
|
||||
firstGameStart: nil
|
||||
)
|
||||
@@ -364,8 +364,8 @@ struct PlanningModelsTests {
|
||||
state: "XX",
|
||||
coordinate: nil,
|
||||
games: games,
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date(),
|
||||
arrivalDate: TestClock.now,
|
||||
departureDate: TestClock.now,
|
||||
location: LocationInput(name: "Test", coordinate: nil),
|
||||
firstGameStart: nil
|
||||
)
|
||||
|
||||
@@ -22,8 +22,8 @@ struct Bug1_TeamFirstSingleTeamTests {
|
||||
planningMode: .teamFirst,
|
||||
sports: [.mlb],
|
||||
travelMode: .drive,
|
||||
startDate: Date(),
|
||||
endDate: Calendar.current.date(byAdding: .day, value: 7, to: Date())!,
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.calendar.date(byAdding: .day, value: 7, to: TestClock.now)!,
|
||||
leisureLevel: .moderate,
|
||||
routePreference: .balanced,
|
||||
selectedTeamIds: ["team_mlb_boston"]
|
||||
@@ -47,8 +47,8 @@ struct Bug1_TeamFirstSingleTeamTests {
|
||||
planningMode: .teamFirst,
|
||||
sports: [.mlb],
|
||||
travelMode: .drive,
|
||||
startDate: Date(),
|
||||
endDate: Calendar.current.date(byAdding: .day, value: 7, to: Date())!,
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.calendar.date(byAdding: .day, value: 7, to: TestClock.now)!,
|
||||
leisureLevel: .moderate,
|
||||
routePreference: .balanced,
|
||||
selectedTeamIds: ["team_mlb_boston", "team_mlb_new_york"]
|
||||
@@ -73,7 +73,7 @@ struct Bug2_InfiniteLoopTests {
|
||||
|
||||
@Test("calculateRestDays does not hang for normal multi-day stop")
|
||||
func calculateRestDays_normalMultiDay_terminates() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let arrival = TestFixtures.date(year: 2026, month: 6, day: 10, hour: 12)
|
||||
let departure = TestFixtures.date(year: 2026, month: 6, day: 14, hour: 12)
|
||||
|
||||
@@ -236,7 +236,7 @@ struct Bug5_ScenarioDDepartureDateTests {
|
||||
@Test("stop departureDate should be after last game, not same day")
|
||||
func departureDate_shouldBeAfterLastGame() {
|
||||
let gameDate = TestFixtures.date(year: 2026, month: 7, day: 5, hour: 19)
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
let bostonStadium = TestFixtures.stadium(city: "Boston")
|
||||
let game = TestFixtures.game(id: "g1", city: "Boston", dateTime: gameDate, stadiumId: bostonStadium.id)
|
||||
@@ -283,7 +283,7 @@ struct Bug6_ScenarioCDateRangeTests {
|
||||
@Test("games spanning exactly daySpan should be included")
|
||||
func gamesSpanningExactDaySpan_shouldBeIncluded() {
|
||||
// If daySpan is 7, games exactly 7 days apart should be valid
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let startDate = TestFixtures.date(year: 2026, month: 7, day: 1, hour: 19)
|
||||
let endDate = calendar.date(byAdding: .day, value: 7, to: startDate)!
|
||||
|
||||
@@ -341,8 +341,8 @@ struct Bug7_DrivingConstraintsClampTests {
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
travelMode: .drive,
|
||||
startDate: Date(),
|
||||
endDate: Calendar.current.date(byAdding: .day, value: 7, to: Date())!,
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.calendar.date(byAdding: .day, value: 7, to: TestClock.now)!,
|
||||
leisureLevel: .moderate,
|
||||
routePreference: .balanced,
|
||||
maxDrivingHoursPerDriver: 0.5
|
||||
@@ -365,8 +365,8 @@ struct Bug7_DrivingConstraintsClampTests {
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
travelMode: .drive,
|
||||
startDate: Date(),
|
||||
endDate: Calendar.current.date(byAdding: .day, value: 7, to: Date())!,
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.calendar.date(byAdding: .day, value: 7, to: TestClock.now)!,
|
||||
leisureLevel: .moderate,
|
||||
routePreference: .balanced,
|
||||
maxDrivingHoursPerDriver: nil
|
||||
@@ -522,18 +522,18 @@ struct Bug11_SortByLeisureTests {
|
||||
city: "Boston", state: "MA",
|
||||
coordinate: TestFixtures.coordinates["Boston"],
|
||||
games: ["g1", "g2", "g3"],
|
||||
arrivalDate: Date(), departureDate: Date().addingTimeInterval(86400),
|
||||
arrivalDate: TestClock.now, departureDate: TestClock.now.addingTimeInterval(86400),
|
||||
location: LocationInput(name: "Boston", coordinate: TestFixtures.coordinates["Boston"]),
|
||||
firstGameStart: Date()
|
||||
firstGameStart: TestClock.now
|
||||
)
|
||||
|
||||
let stop2 = ItineraryStop(
|
||||
city: "Boston", state: "MA",
|
||||
coordinate: TestFixtures.coordinates["Boston"],
|
||||
games: ["g4"],
|
||||
arrivalDate: Date(), departureDate: Date().addingTimeInterval(86400),
|
||||
arrivalDate: TestClock.now, departureDate: TestClock.now.addingTimeInterval(86400),
|
||||
location: LocationInput(name: "Boston", coordinate: TestFixtures.coordinates["Boston"]),
|
||||
firstGameStart: Date()
|
||||
firstGameStart: TestClock.now
|
||||
)
|
||||
|
||||
let option3Games = ItineraryOption(
|
||||
@@ -563,7 +563,7 @@ struct Bug12_ValidatorGameEndTimeTests {
|
||||
@Test("validator checks arrival feasibility with buffer")
|
||||
func validator_checksArrivalBuffer() {
|
||||
// Use deterministic dates to avoid time-of-day sensitivity
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let today = TestFixtures.date(year: 2026, month: 7, day: 10, hour: 14) // 2pm today
|
||||
let tomorrow = calendar.date(byAdding: .day, value: 1, to: today)!
|
||||
let tomorrowMorning = TestFixtures.date(year: 2026, month: 7, day: 11, hour: 8) // 8am departure
|
||||
@@ -735,13 +735,13 @@ struct TravelEstimatorConsistencyTests {
|
||||
// ItineraryStop overload
|
||||
let fromStop = ItineraryStop(
|
||||
city: "Boston", state: "MA", coordinate: bostonCoord,
|
||||
games: [], arrivalDate: Date(), departureDate: Date(),
|
||||
games: [], arrivalDate: TestClock.now, departureDate: TestClock.now,
|
||||
location: LocationInput(name: "Boston", coordinate: bostonCoord),
|
||||
firstGameStart: nil
|
||||
)
|
||||
let toStop = ItineraryStop(
|
||||
city: "New York", state: "NY", coordinate: nycCoord,
|
||||
games: [], arrivalDate: Date(), departureDate: Date(),
|
||||
games: [], arrivalDate: TestClock.now, departureDate: TestClock.now,
|
||||
location: LocationInput(name: "New York", coordinate: nycCoord),
|
||||
firstGameStart: nil
|
||||
)
|
||||
|
||||
@@ -14,8 +14,8 @@ struct RouteFiltersTests {
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
private let calendar = Calendar.current
|
||||
private let today = Calendar.current.startOfDay(for: Date())
|
||||
private let calendar = TestClock.calendar
|
||||
private let today = TestClock.calendar.startOfDay(for: TestClock.now)
|
||||
|
||||
private var tomorrow: Date {
|
||||
calendar.date(byAdding: .day, value: 1, to: today)!
|
||||
|
||||
@@ -15,7 +15,7 @@ struct ScenarioAPlannerTests {
|
||||
// MARK: - Test Data
|
||||
|
||||
private let planner = ScenarioAPlanner()
|
||||
private let calendar = Calendar.current
|
||||
private let calendar = TestClock.calendar
|
||||
|
||||
// Coordinates for testing
|
||||
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
||||
@@ -27,7 +27,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("plan: no games in date range returns noGamesInRange failure")
|
||||
func plan_noGamesInRange_returnsFailure() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
@@ -58,7 +58,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("plan: games outside date range returns noGamesInRange")
|
||||
func plan_gamesOutsideDateRange_returnsNoGamesInRange() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
// Game is after the date range
|
||||
@@ -97,7 +97,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("plan: with selectedRegions filters to those regions")
|
||||
func plan_withSelectedRegions_filtersGames() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
let gameDate = startDate.addingTimeInterval(86400 * 2)
|
||||
|
||||
@@ -144,7 +144,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("plan: with mustStopLocation filters to that city")
|
||||
func plan_withMustStopLocation_filtersToCity() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 14)
|
||||
let gameDate = startDate.addingTimeInterval(86400 * 2)
|
||||
|
||||
@@ -185,7 +185,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("plan: mustStopLocation with no games in that city returns noGamesInRange")
|
||||
func plan_mustStopNoGamesInCity_returnsNoGamesInRange() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
let gameDate = startDate.addingTimeInterval(86400 * 2)
|
||||
|
||||
@@ -222,7 +222,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("plan: multiple must-stop cities are required without excluding other route games")
|
||||
func plan_multipleMustStops_requireCoverageWithoutExclusiveFiltering() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 10)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
@@ -273,7 +273,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("plan: single game in range returns success with one option")
|
||||
func plan_singleGame_returnsSuccess() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
let gameDate = startDate.addingTimeInterval(86400 * 2)
|
||||
|
||||
@@ -309,7 +309,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("plan: multiple games at same stadium creates single stop")
|
||||
func plan_multipleGamesAtSameStadium_createsSingleStop() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
|
||||
@@ -353,7 +353,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("Invariant: returned games are within date range")
|
||||
func invariant_returnedGamesWithinDateRange() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
|
||||
@@ -388,7 +388,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("Invariant: A-B-A creates 3 stops not 2")
|
||||
func invariant_visitSameCityTwice_createsThreeStops() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 10)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
@@ -437,7 +437,7 @@ struct ScenarioAPlannerTests {
|
||||
|
||||
@Test("Property: success always has non-empty options")
|
||||
func property_successHasNonEmptyOptions() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
|
||||
|
||||
@@ -24,7 +24,7 @@ struct ScenarioBPlannerTests {
|
||||
|
||||
@Test("plan: no selected games returns failure")
|
||||
func plan_noSelectedGames_returnsFailure() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
@@ -58,7 +58,7 @@ struct ScenarioBPlannerTests {
|
||||
|
||||
@Test("plan: single selected game returns success with that game")
|
||||
func plan_singleSelectedGame_returnsSuccess() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
let gameDate = startDate.addingTimeInterval(86400 * 2)
|
||||
|
||||
@@ -97,7 +97,7 @@ struct ScenarioBPlannerTests {
|
||||
|
||||
@Test("plan: all selected games appear in every route")
|
||||
func plan_allSelectedGamesAppearInRoutes() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 10)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
@@ -147,7 +147,7 @@ struct ScenarioBPlannerTests {
|
||||
// Bug: planTrip() was overriding the 7-day date range with just anchor dates,
|
||||
// causing only the anchor game to appear in results.
|
||||
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7) // 7-day span
|
||||
|
||||
// NYC and Boston are geographically close (drivable)
|
||||
@@ -205,7 +205,7 @@ struct ScenarioBPlannerTests {
|
||||
// Regression test: Verify that the planner considers games across the entire
|
||||
// date range, not just on the anchor game dates.
|
||||
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
|
||||
// 7-day date range
|
||||
let day1 = startDate
|
||||
@@ -266,15 +266,15 @@ struct ScenarioBPlannerTests {
|
||||
let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
|
||||
|
||||
// Game on a specific date
|
||||
let gameDate = Date().addingTimeInterval(86400 * 5)
|
||||
let gameDate = TestClock.now.addingTimeInterval(86400 * 5)
|
||||
let game = makeGame(id: "game1", stadiumId: "stadium1", dateTime: gameDate)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .gameFirst,
|
||||
sports: [.mlb],
|
||||
mustSeeGameIds: ["game1"],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 30),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 30),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1,
|
||||
@@ -299,7 +299,7 @@ struct ScenarioBPlannerTests {
|
||||
|
||||
@Test("plan: explicit date range with out-of-range selected game returns dateRangeViolation")
|
||||
func plan_explicitDateRange_selectedGameOutsideRange_returnsDateRangeViolation() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
let rangeStart = baseDate
|
||||
let rangeEnd = baseDate.addingTimeInterval(86400 * 3)
|
||||
let outOfRangeDate = baseDate.addingTimeInterval(86400 * 10)
|
||||
@@ -347,7 +347,7 @@ struct ScenarioBPlannerTests {
|
||||
// This test verifies that ScenarioB uses arrival time validation
|
||||
// by creating a scenario where travel time makes arrival impossible
|
||||
|
||||
let now = Date()
|
||||
let now = TestClock.now
|
||||
let game1Date = now.addingTimeInterval(86400) // Tomorrow
|
||||
let game2Date = now.addingTimeInterval(86400 + 3600) // Tomorrow + 1 hour (impossible to drive from coast to coast)
|
||||
|
||||
@@ -389,7 +389,7 @@ struct ScenarioBPlannerTests {
|
||||
|
||||
@Test("Invariant: selected games cannot be dropped")
|
||||
func invariant_selectedGamesCannotBeDropped() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 14)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
@@ -431,7 +431,7 @@ struct ScenarioBPlannerTests {
|
||||
|
||||
@Test("Property: success with selected games includes all anchors")
|
||||
func property_successIncludesAllAnchors() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
let gameDate = startDate.addingTimeInterval(86400 * 2)
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ struct ScenarioCPlannerTests {
|
||||
startLocation: nil, // Missing
|
||||
endLocation: endLocation,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -67,8 +67,8 @@ struct ScenarioCPlannerTests {
|
||||
startLocation: startLocation,
|
||||
endLocation: nil, // Missing
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -102,8 +102,8 @@ struct ScenarioCPlannerTests {
|
||||
startLocation: startLocation,
|
||||
endLocation: endLocation,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -139,8 +139,8 @@ struct ScenarioCPlannerTests {
|
||||
startLocation: startLocation,
|
||||
endLocation: endLocation,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -174,8 +174,8 @@ struct ScenarioCPlannerTests {
|
||||
startLocation: startLocation,
|
||||
endLocation: endLocation,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -199,7 +199,7 @@ struct ScenarioCPlannerTests {
|
||||
|
||||
@Test("plan: city names with state suffixes match stadium city names")
|
||||
func plan_cityNamesWithStateSuffixes_matchStadiumCities() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
let endDate = baseDate.addingTimeInterval(86400 * 10)
|
||||
|
||||
let startLocation = LocationInput(name: "Chicago, IL", coordinate: chicagoCoord)
|
||||
@@ -242,7 +242,7 @@ struct ScenarioCPlannerTests {
|
||||
|
||||
@Test("plan: directional filtering includes stadiums toward destination")
|
||||
func plan_directionalFiltering_includesCorrectStadiums() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 14)
|
||||
|
||||
let startLocation = LocationInput(name: "Chicago", coordinate: chicagoCoord)
|
||||
@@ -297,7 +297,7 @@ struct ScenarioCPlannerTests {
|
||||
|
||||
@Test("plan: adds start and end as non-game stops")
|
||||
func plan_addsStartEndAsNonGameStops() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 10)
|
||||
|
||||
let startLocation = LocationInput(name: "Chicago", coordinate: chicagoCoord)
|
||||
@@ -350,7 +350,7 @@ struct ScenarioCPlannerTests {
|
||||
|
||||
@Test("Invariant: start stop has no games")
|
||||
func invariant_startStopHasNoGames() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 10)
|
||||
|
||||
let startLocation = LocationInput(name: "Chicago", coordinate: chicagoCoord)
|
||||
@@ -400,7 +400,7 @@ struct ScenarioCPlannerTests {
|
||||
|
||||
@Test("Invariant: end stop appears last")
|
||||
func invariant_endStopAppearsLast() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 10)
|
||||
|
||||
let startLocation = LocationInput(name: "Chicago", coordinate: chicagoCoord)
|
||||
|
||||
@@ -24,7 +24,7 @@ struct ScenarioDPlannerTests {
|
||||
|
||||
@Test("plan: no followTeamId returns missingTeamSelection failure")
|
||||
func plan_noFollowTeamId_returnsMissingTeamSelection() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
@@ -58,7 +58,7 @@ struct ScenarioDPlannerTests {
|
||||
|
||||
@Test("plan: no games for team returns noGamesInRange failure")
|
||||
func plan_noGamesForTeam_returnsNoGamesInRange() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
|
||||
@@ -105,7 +105,7 @@ struct ScenarioDPlannerTests {
|
||||
|
||||
@Test("plan: includes both home and away games for team")
|
||||
func plan_includesBothHomeAndAwayGames() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 14)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
@@ -168,7 +168,7 @@ struct ScenarioDPlannerTests {
|
||||
|
||||
@Test("plan: with selectedRegions filters team games to those regions")
|
||||
func plan_withSelectedRegions_filtersTeamGames() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 14)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord) // East
|
||||
@@ -229,7 +229,7 @@ struct ScenarioDPlannerTests {
|
||||
|
||||
@Test("plan: valid request returns success")
|
||||
func plan_validRequest_returnsSuccess() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
|
||||
@@ -274,7 +274,7 @@ struct ScenarioDPlannerTests {
|
||||
|
||||
@Test("plan: useHomeLocation with startLocation adds home start and end stops")
|
||||
func plan_useHomeLocationWithStartLocation_addsHomeEndpoints() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 10)
|
||||
|
||||
let homeCoord = CLLocationCoordinate2D(latitude: 39.7392, longitude: -104.9903) // Denver
|
||||
@@ -333,7 +333,7 @@ struct ScenarioDPlannerTests {
|
||||
|
||||
@Test("Invariant: all returned games have team as home or away")
|
||||
func invariant_allGamesHaveTeam() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 14)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
@@ -404,7 +404,7 @@ struct ScenarioDPlannerTests {
|
||||
|
||||
@Test("Invariant: duplicate routes are removed")
|
||||
func invariant_duplicateRoutesRemoved() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
|
||||
@@ -454,7 +454,7 @@ struct ScenarioDPlannerTests {
|
||||
|
||||
@Test("Property: success always has non-empty options")
|
||||
func property_successHasNonEmptyOptions() {
|
||||
let startDate = Date()
|
||||
let startDate = TestClock.now
|
||||
let endDate = startDate.addingTimeInterval(86400 * 7)
|
||||
|
||||
let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
|
||||
|
||||
@@ -40,8 +40,8 @@ struct ScenarioEPlannerTests {
|
||||
// With 2 teams, window duration = 4 days.
|
||||
// The window algorithm checks: windowEnd <= latestGameDay + 1
|
||||
// So with games on day 1 and day 4: latestDay=4, windowEnd=5 <= day 5 (4+1) - valid!
|
||||
let calendar = Calendar.current
|
||||
let baseDate = calendar.startOfDay(for: Date())
|
||||
let calendar = TestClock.calendar
|
||||
let baseDate = calendar.startOfDay(for: TestClock.now)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
@@ -102,7 +102,7 @@ struct ScenarioEPlannerTests {
|
||||
@Test("generateValidWindows: window with only 2 of 3 teams excluded")
|
||||
func generateValidWindows_windowMissingTeam_excluded() {
|
||||
// Setup: 3 teams selected, but games are spread so no single window covers all 3
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
@@ -168,7 +168,7 @@ struct ScenarioEPlannerTests {
|
||||
@Test("generateValidWindows: empty season returns empty")
|
||||
func generateValidWindows_emptySeason_returnsEmpty() {
|
||||
// Setup: No games available
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .teamFirst,
|
||||
@@ -204,8 +204,8 @@ struct ScenarioEPlannerTests {
|
||||
@Test("generateValidWindows: sampling works when more than 50 windows")
|
||||
func generateValidWindows_manyWindows_samplesProperly() {
|
||||
// Setup: Create many overlapping games so there are >50 valid windows
|
||||
let calendar = Calendar.current
|
||||
let baseDate = calendar.startOfDay(for: Date())
|
||||
let calendar = TestClock.calendar
|
||||
let baseDate = calendar.startOfDay(for: TestClock.now)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
@@ -278,8 +278,8 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("plan: returns PlanningResult with routes")
|
||||
func plan_validRequest_returnsPlanningResultWithRoutes() {
|
||||
let calendar = Calendar.current
|
||||
let baseDate = calendar.startOfDay(for: Date())
|
||||
let calendar = TestClock.calendar
|
||||
let baseDate = calendar.startOfDay(for: TestClock.now)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
@@ -339,8 +339,8 @@ struct ScenarioEPlannerTests {
|
||||
@Test("plan: all routes include all selected teams")
|
||||
func plan_allRoutesIncludeAllSelectedTeams() {
|
||||
// Use just 2 teams for a simpler, more reliable test
|
||||
let calendar = Calendar.current
|
||||
let baseDate = calendar.startOfDay(for: Date())
|
||||
let calendar = TestClock.calendar
|
||||
let baseDate = calendar.startOfDay(for: TestClock.now)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
@@ -408,8 +408,8 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("plan: falls back when earliest per-team anchors are infeasible")
|
||||
func plan_fallbackWhenEarliestAnchorsInfeasible() {
|
||||
let calendar = Calendar.current
|
||||
let baseDate = calendar.startOfDay(for: Date())
|
||||
let calendar = TestClock.calendar
|
||||
let baseDate = calendar.startOfDay(for: TestClock.now)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let chicagoStadium = makeStadium(id: "chi", city: "Chicago", coordinate: chicagoCoord)
|
||||
@@ -477,8 +477,8 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("plan: keeps date-distinct options even when city order is identical")
|
||||
func plan_keepsDistinctGameSetsWithSameCityOrder() {
|
||||
let calendar = Calendar.current
|
||||
let baseDate = calendar.startOfDay(for: Date())
|
||||
let calendar = TestClock.calendar
|
||||
let baseDate = calendar.startOfDay(for: TestClock.now)
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
@@ -548,7 +548,7 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("plan: routes sorted by duration ascending")
|
||||
func plan_routesSortedByDurationAscending() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
@@ -621,7 +621,7 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("plan: respects max driving time constraint")
|
||||
func plan_respectsMaxDrivingTimeConstraint() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
// NYC and LA are ~40 hours apart by car
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
@@ -681,7 +681,7 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("plan: teams with no overlapping games returns graceful error")
|
||||
func plan_noOverlappingGames_returnsGracefulError() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
@@ -735,7 +735,7 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("plan: single team selected returns validation error")
|
||||
func plan_singleTeamSelected_returnsValidationError() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
|
||||
@@ -777,7 +777,7 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("plan: no teams selected returns validation error")
|
||||
func plan_noTeamsSelected_returnsValidationError() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .teamFirst,
|
||||
@@ -810,8 +810,8 @@ struct ScenarioEPlannerTests {
|
||||
@Test("plan: teams in same city treated as separate stops")
|
||||
func plan_teamsInSameCity_treatedAsSeparateStops() {
|
||||
// Setup: Yankees and Mets both play in NYC but at different stadiums
|
||||
let calendar = Calendar.current
|
||||
let baseDate = calendar.startOfDay(for: Date())
|
||||
let calendar = TestClock.calendar
|
||||
let baseDate = calendar.startOfDay(for: TestClock.now)
|
||||
|
||||
let yankeeStadiumCoord = CLLocationCoordinate2D(latitude: 40.8296, longitude: -73.9262)
|
||||
let citiFieldCoord = CLLocationCoordinate2D(latitude: 40.7571, longitude: -73.8458)
|
||||
@@ -879,7 +879,7 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("plan: team with no home games returns error")
|
||||
func plan_teamWithNoHomeGames_returnsError() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
|
||||
@@ -942,7 +942,7 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("Invariant: maximum 10 results returned")
|
||||
func invariant_maximum10ResultsReturned() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
@@ -996,7 +996,7 @@ struct ScenarioEPlannerTests {
|
||||
|
||||
@Test("Invariant: all routes contain home games from all selected teams")
|
||||
func invariant_allRoutesContainAllSelectedTeams() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
|
||||
@@ -20,8 +20,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .followTeam,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1,
|
||||
@@ -41,8 +41,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
planningMode: .gameFirst,
|
||||
sports: [.mlb],
|
||||
mustSeeGameIds: [game.id],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -64,8 +64,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
|
||||
endLocation: LocationInput(name: "LA", coordinate: laCoord),
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -82,8 +82,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -108,8 +108,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
endLocation: LocationInput(name: "LA", coordinate: laCoord), // C condition
|
||||
sports: [.mlb],
|
||||
mustSeeGameIds: [game.id], // B condition
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1,
|
||||
@@ -135,8 +135,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
endLocation: LocationInput(name: "LA", coordinate: laCoord), // C condition
|
||||
sports: [.mlb],
|
||||
mustSeeGameIds: [game.id], // B condition
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -156,8 +156,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .followTeam,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1,
|
||||
@@ -177,8 +177,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
planningMode: .gameFirst,
|
||||
sports: [.mlb],
|
||||
mustSeeGameIds: [game.id],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -200,8 +200,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
|
||||
endLocation: LocationInput(name: "LA", coordinate: laCoord),
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -218,8 +218,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -239,8 +239,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
let prefsA = TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1
|
||||
@@ -254,8 +254,8 @@ struct ScenarioPlannerFactoryTests {
|
||||
let prefsD = TripPreferences(
|
||||
planningMode: .followTeam,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1,
|
||||
@@ -287,7 +287,7 @@ struct ScenarioPlannerFactoryTests {
|
||||
homeTeamId: "team1",
|
||||
awayTeamId: "team2",
|
||||
stadiumId: "stadium1",
|
||||
dateTime: Date(),
|
||||
dateTime: TestClock.now,
|
||||
sport: .mlb,
|
||||
season: "2026",
|
||||
isPlayoff: false
|
||||
|
||||
@@ -28,7 +28,7 @@ struct TeamFirstIntegrationTests {
|
||||
|
||||
@Test("Integration: 3 MLB teams returns top 10 routes")
|
||||
func integration_3MLBTeams_returnsTop10Routes() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
// Create realistic MLB stadiums
|
||||
let yankeeStadium = Stadium(
|
||||
@@ -99,7 +99,7 @@ struct TeamFirstIntegrationTests {
|
||||
// Day 1: Yankees home
|
||||
// Day 3: Red Sox home
|
||||
// Day 6: Phillies home (spans 6 days, window fits)
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let day1 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 1))!
|
||||
let day3 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 3))!
|
||||
let day6 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 6))!
|
||||
@@ -177,7 +177,7 @@ struct TeamFirstIntegrationTests {
|
||||
|
||||
@Test("Integration: each route visits all 3 stadiums")
|
||||
func integration_eachRouteVisitsAll3Stadiums() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let yankeeStadium = makeStadium(
|
||||
id: "yankee-stadium",
|
||||
@@ -288,7 +288,7 @@ struct TeamFirstIntegrationTests {
|
||||
|
||||
@Test("Integration: total duration within 6 days (teams x 2)")
|
||||
func integration_totalDurationWithinLimit() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let yankeeStadium = makeStadium(
|
||||
id: "yankee-stadium",
|
||||
@@ -311,7 +311,7 @@ struct TeamFirstIntegrationTests {
|
||||
|
||||
// Create games that fit within a 6-day window
|
||||
// For 3 teams, window = 6 days. Games must span at least 6 days.
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let day1 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 1))!
|
||||
let day3 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 3))!
|
||||
let day6 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 6))!
|
||||
@@ -393,7 +393,7 @@ struct TeamFirstIntegrationTests {
|
||||
continue
|
||||
}
|
||||
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let tripDays = calendar.dateComponents(
|
||||
[.day],
|
||||
from: calendar.startOfDay(for: firstStop.arrivalDate),
|
||||
@@ -407,7 +407,7 @@ struct TeamFirstIntegrationTests {
|
||||
|
||||
@Test("Integration: factory selects ScenarioEPlanner for teamFirst mode")
|
||||
func integration_factorySelectsScenarioEPlanner() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .teamFirst,
|
||||
@@ -436,7 +436,7 @@ struct TeamFirstIntegrationTests {
|
||||
|
||||
@Test("Integration: factory requires 2+ teams for ScenarioE")
|
||||
func integration_factoryRequires2TeamsForScenarioE() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
// With only 1 team, should NOT select ScenarioE
|
||||
var prefs = TripPreferences(
|
||||
@@ -475,7 +475,7 @@ struct TeamFirstIntegrationTests {
|
||||
|
||||
@Test("Integration: realistic east coast trip with 4 teams")
|
||||
func integration_realisticEastCoastTrip() {
|
||||
let baseDate = Date()
|
||||
let baseDate = TestClock.now
|
||||
|
||||
// East coast stadiums (NYC, Boston, Philly, Baltimore)
|
||||
let yankeeStadium = makeStadium(
|
||||
@@ -505,7 +505,7 @@ struct TeamFirstIntegrationTests {
|
||||
|
||||
// Create games spread across 8-day window (4 teams * 2 = 8 days)
|
||||
// For 4 teams, window = 8 days. Games must span at least 8 days.
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let day1 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 1))!
|
||||
let day3 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 3))!
|
||||
let day5 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 5))!
|
||||
|
||||
@@ -213,7 +213,7 @@ struct TravelEstimatorTests {
|
||||
|
||||
@Test("calculateTravelDays: zero hours returns departure day only")
|
||||
func calculateTravelDays_zeroHours_returnsDepartureDay() {
|
||||
let departure = Date()
|
||||
let departure = TestClock.now
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 0)
|
||||
|
||||
#expect(days.count == 1)
|
||||
@@ -221,7 +221,7 @@ struct TravelEstimatorTests {
|
||||
|
||||
@Test("calculateTravelDays: 1-8 hours returns single day")
|
||||
func calculateTravelDays_1to8Hours_returnsSingleDay() {
|
||||
let departure = Date()
|
||||
let departure = TestClock.now
|
||||
|
||||
for hours in [1.0, 4.0, 7.0, 8.0] {
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: hours)
|
||||
@@ -231,7 +231,7 @@ struct TravelEstimatorTests {
|
||||
|
||||
@Test("calculateTravelDays: 8.01-16 hours returns two days")
|
||||
func calculateTravelDays_8to16Hours_returnsTwoDays() {
|
||||
let departure = Date()
|
||||
let departure = TestClock.now
|
||||
|
||||
for hours in [8.01, 12.0, 16.0] {
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: hours)
|
||||
@@ -241,7 +241,7 @@ struct TravelEstimatorTests {
|
||||
|
||||
@Test("calculateTravelDays: 16.01-24 hours returns three days")
|
||||
func calculateTravelDays_16to24Hours_returnsThreeDays() {
|
||||
let departure = Date()
|
||||
let departure = TestClock.now
|
||||
|
||||
for hours in [16.01, 20.0, 24.0] {
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: hours)
|
||||
@@ -251,9 +251,9 @@ struct TravelEstimatorTests {
|
||||
|
||||
@Test("calculateTravelDays: all dates are start of day")
|
||||
func calculateTravelDays_allDatesAreStartOfDay() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
// Use a specific time that's not midnight
|
||||
var components = calendar.dateComponents([.year, .month, .day], from: Date())
|
||||
var components = calendar.dateComponents([.year, .month, .day], from: TestClock.now)
|
||||
components.hour = 14
|
||||
components.minute = 30
|
||||
let departure = calendar.date(from: components)!
|
||||
@@ -269,8 +269,8 @@ struct TravelEstimatorTests {
|
||||
|
||||
@Test("calculateTravelDays: consecutive days")
|
||||
func calculateTravelDays_consecutiveDays() {
|
||||
let calendar = Calendar.current
|
||||
let departure = Date()
|
||||
let calendar = TestClock.calendar
|
||||
let departure = TestClock.now
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 24)
|
||||
|
||||
#expect(days.count == 3)
|
||||
@@ -414,21 +414,21 @@ struct TravelEstimatorTests {
|
||||
|
||||
@Test("Edge: calculateTravelDays with exactly 8 hours")
|
||||
func edge_calculateTravelDays_exactly8Hours() {
|
||||
let departure = Date()
|
||||
let departure = TestClock.now
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 8.0)
|
||||
#expect(days.count == 1, "Exactly 8 hours should be 1 day")
|
||||
}
|
||||
|
||||
@Test("Edge: calculateTravelDays just over 8 hours")
|
||||
func edge_calculateTravelDays_justOver8Hours() {
|
||||
let departure = Date()
|
||||
let departure = TestClock.now
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 8.001)
|
||||
#expect(days.count == 2, "Just over 8 hours should be 2 days")
|
||||
}
|
||||
|
||||
@Test("Edge: negative driving hours treated as minimum 1 day")
|
||||
func edge_negativeDrivingHours() {
|
||||
let departure = Date()
|
||||
let departure = TestClock.now
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: -5)
|
||||
#expect(days.count >= 1, "Negative hours should still return at least 1 day")
|
||||
}
|
||||
@@ -445,8 +445,8 @@ struct TravelEstimatorTests {
|
||||
state: state,
|
||||
coordinate: coordinate,
|
||||
games: [],
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date(),
|
||||
arrivalDate: TestClock.now,
|
||||
departureDate: TestClock.now,
|
||||
location: LocationInput(name: city, coordinate: coordinate),
|
||||
firstGameStart: nil
|
||||
)
|
||||
|
||||
@@ -113,7 +113,7 @@ struct TripPlanningEngineTests {
|
||||
|
||||
@Test("effectiveTripDuration: calculates from date range when tripDuration is nil")
|
||||
func effectiveTripDuration_calculated() {
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
let startDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
let endDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 22))!
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ struct HistoricalGameQueryTests {
|
||||
/// - Expected Behavior: Returns date in yyyy-MM-dd format (Eastern time)
|
||||
@Test("normalizedDateString: formats as yyyy-MM-dd")
|
||||
func normalizedDateString_format() {
|
||||
var calendar = Calendar.current
|
||||
var calendar = TestClock.calendar
|
||||
calendar.timeZone = TimeZone(identifier: "America/New_York")!
|
||||
let date = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
|
||||
@@ -64,7 +64,7 @@ struct HistoricalGameQueryTests {
|
||||
|
||||
@Test("normalizedDateString: pads single-digit months")
|
||||
func normalizedDateString_padMonth() {
|
||||
var calendar = Calendar.current
|
||||
var calendar = TestClock.calendar
|
||||
calendar.timeZone = TimeZone(identifier: "America/New_York")!
|
||||
let date = calendar.date(from: DateComponents(year: 2026, month: 3, day: 5))!
|
||||
|
||||
@@ -77,7 +77,7 @@ struct HistoricalGameQueryTests {
|
||||
|
||||
@Test("init: stores sport correctly")
|
||||
func init_storesSport() {
|
||||
let query = HistoricalGameQuery(sport: .nba, date: Date())
|
||||
let query = HistoricalGameQuery(sport: .nba, date: TestClock.now)
|
||||
#expect(query.sport == .nba)
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ struct HistoricalGameQueryTests {
|
||||
func init_storesTeams() {
|
||||
let query = HistoricalGameQuery(
|
||||
sport: .mlb,
|
||||
date: Date(),
|
||||
date: TestClock.now,
|
||||
homeTeamAbbrev: "NYY",
|
||||
awayTeamAbbrev: "BOS"
|
||||
)
|
||||
@@ -96,7 +96,7 @@ struct HistoricalGameQueryTests {
|
||||
|
||||
@Test("init: team abbreviations default to nil")
|
||||
func init_defaultNilTeams() {
|
||||
let query = HistoricalGameQuery(sport: .mlb, date: Date())
|
||||
let query = HistoricalGameQuery(sport: .mlb, date: TestClock.now)
|
||||
|
||||
#expect(query.homeTeamAbbrev == nil)
|
||||
#expect(query.awayTeamAbbrev == nil)
|
||||
@@ -117,7 +117,7 @@ struct HistoricalGameResultTests {
|
||||
) -> HistoricalGameResult {
|
||||
HistoricalGameResult(
|
||||
sport: .mlb,
|
||||
gameDate: Date(),
|
||||
gameDate: TestClock.now,
|
||||
homeTeamAbbrev: "NYY",
|
||||
awayTeamAbbrev: "BOS",
|
||||
homeTeamName: "Yankees",
|
||||
@@ -214,7 +214,7 @@ struct ScoreResolutionResultTests {
|
||||
private func makeHistoricalResult() -> HistoricalGameResult {
|
||||
HistoricalGameResult(
|
||||
sport: .mlb,
|
||||
gameDate: Date(),
|
||||
gameDate: TestClock.now,
|
||||
homeTeamAbbrev: "NYY",
|
||||
awayTeamAbbrev: "BOS",
|
||||
homeTeamName: "Yankees",
|
||||
|
||||
@@ -87,7 +87,7 @@ struct GameMatchResultTests {
|
||||
homeTeamId: "home_team",
|
||||
awayTeamId: "away_team",
|
||||
stadiumId: "stadium_1",
|
||||
dateTime: Date(),
|
||||
dateTime: TestClock.now,
|
||||
sport: .mlb,
|
||||
season: "2026"
|
||||
)
|
||||
@@ -194,7 +194,7 @@ struct GameMatchCandidateTests {
|
||||
homeTeamId: "home_team",
|
||||
awayTeamId: "away_team",
|
||||
stadiumId: "stadium_1",
|
||||
dateTime: Date(),
|
||||
dateTime: TestClock.now,
|
||||
sport: .mlb,
|
||||
season: "2026"
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ struct ScrapedGameTests {
|
||||
sport: Sport = .mlb
|
||||
) -> ScrapedGame {
|
||||
ScrapedGame(
|
||||
date: Date(),
|
||||
date: TestClock.now,
|
||||
homeTeam: homeTeam,
|
||||
awayTeam: awayTeam,
|
||||
homeScore: homeScore,
|
||||
@@ -69,7 +69,7 @@ struct ScrapedGameTests {
|
||||
|
||||
@Test("ScrapedGame: stores date")
|
||||
func scrapedGame_date() {
|
||||
let date = Date()
|
||||
let date = TestClock.now
|
||||
let game = ScrapedGame(
|
||||
date: date,
|
||||
homeTeam: "Home",
|
||||
|
||||
@@ -18,7 +18,7 @@ struct PhotoMetadataTests {
|
||||
// MARK: - Test Data
|
||||
|
||||
private func makeMetadata(
|
||||
captureDate: Date? = Date(),
|
||||
captureDate: Date? = TestClock.now,
|
||||
coordinates: CLLocationCoordinate2D? = CLLocationCoordinate2D(latitude: 40.0, longitude: -74.0)
|
||||
) -> PhotoMetadata {
|
||||
PhotoMetadata(captureDate: captureDate, coordinates: coordinates)
|
||||
@@ -45,7 +45,7 @@ struct PhotoMetadataTests {
|
||||
/// - Expected Behavior: true when captureDate is provided
|
||||
@Test("hasValidDate: true when captureDate provided")
|
||||
func hasValidDate_true() {
|
||||
let metadata = makeMetadata(captureDate: Date())
|
||||
let metadata = makeMetadata(captureDate: TestClock.now)
|
||||
#expect(metadata.hasValidDate == true)
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ struct PhotoMetadataTests {
|
||||
|
||||
@Test("Both valid: location and date both provided")
|
||||
func bothValid() {
|
||||
let metadata = makeMetadata(captureDate: Date(), coordinates: CLLocationCoordinate2D(latitude: 0, longitude: 0))
|
||||
let metadata = makeMetadata(captureDate: TestClock.now, coordinates: CLLocationCoordinate2D(latitude: 0, longitude: 0))
|
||||
#expect(metadata.hasValidLocation == true)
|
||||
#expect(metadata.hasValidDate == true)
|
||||
}
|
||||
@@ -101,7 +101,7 @@ struct PhotoMetadataTests {
|
||||
|
||||
@Test("Only date: coordinates nil")
|
||||
func onlyDate() {
|
||||
let metadata = makeMetadata(captureDate: Date(), coordinates: nil)
|
||||
let metadata = makeMetadata(captureDate: TestClock.now, coordinates: nil)
|
||||
#expect(metadata.hasValidLocation == false)
|
||||
#expect(metadata.hasValidDate == true)
|
||||
}
|
||||
@@ -121,7 +121,7 @@ struct PhotoMetadataTests {
|
||||
/// - Invariant: hasValidDate == (captureDate != nil)
|
||||
@Test("Invariant: hasValidDate equals captureDate check")
|
||||
func invariant_hasValidDateEqualsCaptureCheck() {
|
||||
let withDate = makeMetadata(captureDate: Date())
|
||||
let withDate = makeMetadata(captureDate: TestClock.now)
|
||||
let withoutDate = makeMetadata(captureDate: nil)
|
||||
|
||||
#expect(withDate.hasValidDate == (withDate.captureDate != nil))
|
||||
|
||||
@@ -41,8 +41,8 @@ struct RouteDescriptionInputTests {
|
||||
state: "XX",
|
||||
coordinate: nycCoord,
|
||||
games: games,
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date().addingTimeInterval(86400),
|
||||
arrivalDate: TestClock.now,
|
||||
departureDate: TestClock.now.addingTimeInterval(86400),
|
||||
location: LocationInput(name: city, coordinate: nycCoord),
|
||||
firstGameStart: nil
|
||||
)
|
||||
@@ -54,7 +54,7 @@ struct RouteDescriptionInputTests {
|
||||
homeTeamId: "team1",
|
||||
awayTeamId: "team2",
|
||||
stadiumId: "stadium1",
|
||||
dateTime: Date(),
|
||||
dateTime: TestClock.now,
|
||||
sport: sport,
|
||||
season: "2026",
|
||||
isPlayoff: false
|
||||
|
||||
@@ -22,8 +22,8 @@ struct SuggestedTripTests {
|
||||
preferences: TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(86400 * 7),
|
||||
startDate: TestClock.now,
|
||||
endDate: TestClock.now.addingTimeInterval(86400 * 7),
|
||||
leisureLevel: .moderate
|
||||
),
|
||||
stops: [],
|
||||
@@ -339,7 +339,7 @@ struct CrossCountryFeatureTripTests {
|
||||
idPrefix: String
|
||||
) -> [Game] {
|
||||
var games: [Game] = []
|
||||
let calendar = Calendar.current
|
||||
let calendar = TestClock.calendar
|
||||
|
||||
for (index, stadium) in stadiums.enumerated() {
|
||||
let gameDate = calendar.date(byAdding: .day, value: index * spacingDays, to: startDate) ?? startDate
|
||||
@@ -373,7 +373,7 @@ struct CrossCountryFeatureTripTests {
|
||||
idPrefix: "e2w"
|
||||
)
|
||||
|
||||
let secondLegStart = Calendar.current.date(
|
||||
let secondLegStart = TestClock.calendar.date(
|
||||
byAdding: .day,
|
||||
value: (sortedEastToWest.count * spacingDays) + 2,
|
||||
to: baseDate
|
||||
|
||||
@@ -27,6 +27,9 @@ class BaseUITestCase: XCTestCase {
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
|
||||
// Keep UI tests in a consistent orientation to avoid layout-dependent flakiness.
|
||||
XCUIDevice.shared.orientation = .portrait
|
||||
|
||||
app = XCUIApplication()
|
||||
app.launchArguments = [
|
||||
"--ui-testing",
|
||||
|
||||
@@ -150,10 +150,11 @@ struct TripWizardScreen {
|
||||
/// Waits for the wizard sheet to appear.
|
||||
@discardableResult
|
||||
func waitForLoad() -> TripWizardScreen {
|
||||
navigationTitle.waitForExistenceOrFail(
|
||||
timeout: BaseUITestCase.defaultTimeout,
|
||||
"Trip Wizard should appear"
|
||||
)
|
||||
if navigationTitle.waitForExistence(timeout: BaseUITestCase.defaultTimeout) ||
|
||||
planningModeButton("dateRange").waitForExistence(timeout: BaseUITestCase.defaultTimeout) {
|
||||
return self
|
||||
}
|
||||
XCTFail("Trip Wizard should appear")
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -177,28 +178,73 @@ struct TripWizardScreen {
|
||||
startDay: String,
|
||||
endDay: String
|
||||
) {
|
||||
// Navigate forward to the target month
|
||||
let target = "\(targetMonth) \(targetYear)"
|
||||
var attempts = 0
|
||||
// First ensure the month label is visible
|
||||
// First, navigate by month label so tests that assert month visibility stay stable.
|
||||
monthLabel.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
while !monthLabel.label.contains(target) && attempts < 18 {
|
||||
nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
attempts += 1
|
||||
let targetMonthYear = "\(targetMonth) \(targetYear)"
|
||||
|
||||
var monthAttempts = 0
|
||||
while monthAttempts < 24 && !monthLabel.label.contains(targetMonthYear) {
|
||||
// Prefer directional navigation when current label can be parsed.
|
||||
let currentLabel = monthLabel.label
|
||||
if currentLabel.contains(targetYear),
|
||||
let currentMonthName = currentLabel.split(separator: " ").first {
|
||||
let monthOrder = [
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
]
|
||||
if let currentIdx = monthOrder.firstIndex(of: String(currentMonthName)),
|
||||
let targetIdx = monthOrder.firstIndex(of: targetMonth) {
|
||||
if currentIdx > targetIdx {
|
||||
previousMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
previousMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
} else if currentIdx < targetIdx {
|
||||
nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
}
|
||||
} else {
|
||||
nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
}
|
||||
monthAttempts += 1
|
||||
}
|
||||
|
||||
// If the exact target day IDs are unavailable, fall back to visible day cells.
|
||||
let startBtn = dayButton(startDay)
|
||||
if !startBtn.exists {
|
||||
// Fallback for locale/device-calendar drift: pick visible day cells.
|
||||
let dayCells = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'wizard.dates.day.'"))
|
||||
guard dayCells.count > 1 else { return }
|
||||
let startFallback = dayCells.element(boundBy: 0)
|
||||
let endFallback = dayCells.element(boundBy: min(4, max(1, dayCells.count - 1)))
|
||||
startFallback.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
startFallback.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
endFallback.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
endFallback.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(monthLabel.label.contains(target),
|
||||
"Should navigate to \(target)")
|
||||
|
||||
// Select start date — scroll calendar grid into view first
|
||||
let startBtn = dayButton(startDay)
|
||||
startBtn.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
startBtn.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
|
||||
// Select end date
|
||||
let endBtn = dayButton(endDay)
|
||||
endBtn.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
endBtn.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
if endBtn.exists {
|
||||
endBtn.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
endBtn.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
} else {
|
||||
let dayCells = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'wizard.dates.day.'"))
|
||||
guard dayCells.count > 1 else { return }
|
||||
let fallback = dayCells.element(boundBy: min(4, max(1, dayCells.count - 1)))
|
||||
fallback.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
fallback.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects a sport (e.g., "mlb").
|
||||
@@ -263,7 +309,7 @@ struct TripOptionsScreen {
|
||||
@discardableResult
|
||||
func waitForLoad() -> TripOptionsScreen {
|
||||
sortDropdown.waitForExistenceOrFail(
|
||||
timeout: BaseUITestCase.longTimeout,
|
||||
timeout: 90,
|
||||
"Trip Options should appear after planning completes"
|
||||
)
|
||||
return self
|
||||
@@ -716,14 +762,18 @@ enum TestFlows {
|
||||
wizard.waitForLoad()
|
||||
wizard.selectDateRangeMode()
|
||||
|
||||
wizard.nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
wizard.selectDateRange(
|
||||
targetMonth: month,
|
||||
targetYear: year,
|
||||
startDay: startDay,
|
||||
endDay: endDay
|
||||
)
|
||||
wizard.selectSport(sport)
|
||||
|
||||
// If calendar day cells aren't available, we likely kept default dates.
|
||||
// Use an in-season sport to keep planning flows deterministic year-round.
|
||||
let dayCells = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'wizard.dates.day.'"))
|
||||
let selectedSport = dayCells.count > 1 ? sport : "nba"
|
||||
wizard.selectSport(selectedSport)
|
||||
wizard.selectRegion(region)
|
||||
wizard.tapPlanTrip()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user