Files
Sportstime/SportsTimeTests/Features/Trip/ItinerarySectionBuilderTests.swift
2026-02-18 13:00:15 -06:00

218 lines
7.2 KiB
Swift

//
// ItinerarySectionBuilderTests.swift
// SportsTimeTests
//
// Tests for ItinerarySectionBuilder pure static function.
//
import Testing
import CoreLocation
@testable import SportsTime
@Suite("ItinerarySectionBuilder")
struct ItinerarySectionBuilderTests {
// MARK: - Helpers
private func makeTripDays(count: Int, startDate: Date = TestClock.now) -> [Date] {
(0..<count).map {
TestClock.calendar.date(byAdding: .day, value: $0, to: TestClock.calendar.startOfDay(for: startDate))!
}
}
private func makeTrip(
cities: [String] = ["New York", "Boston"],
startDate: Date = TestClock.now,
daysPerStop: Int = 1,
gameIds: [[String]] = []
) -> (Trip, [Date]) {
let stops = TestFixtures.tripStops(cities: cities, startDate: startDate, daysPerStop: daysPerStop)
var stopsWithGames = stops
for (i, ids) in gameIds.enumerated() where i < stopsWithGames.count {
stopsWithGames[i] = TripStop(
stopNumber: stopsWithGames[i].stopNumber,
city: stopsWithGames[i].city,
state: stopsWithGames[i].state,
coordinate: stopsWithGames[i].coordinate,
arrivalDate: stopsWithGames[i].arrivalDate,
departureDate: stopsWithGames[i].departureDate,
games: ids,
isRestDay: stopsWithGames[i].isRestDay
)
}
let trip = TestFixtures.trip(stops: stopsWithGames)
let totalDays = cities.count * daysPerStop
let days = makeTripDays(count: totalDays, startDate: startDate)
return (trip, days)
}
// MARK: - Tests
@Test("builds one section per day")
func buildsSectionsForEachDay() {
let startDate = TestClock.calendar.startOfDay(for: TestClock.now)
let (trip, days) = makeTrip(cities: ["New York", "Boston", "Philadelphia"], startDate: startDate)
let sections = ItinerarySectionBuilder.build(
trip: trip,
tripDays: days,
games: [:],
travelOverrides: [:],
itineraryItems: [],
allowCustomItems: false
)
// Should have travel + day sections
let daySections = sections.filter {
if case .day = $0 { return true }
return false
}
#expect(daySections.count == days.count)
}
@Test("games filtered correctly by date")
func gamesOnFiltersCorrectly() {
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")
let (trip, days) = makeTrip(
cities: ["New York", "Boston"],
startDate: startDate,
gameIds: [[game.id]]
)
let sections = ItinerarySectionBuilder.build(
trip: trip,
tripDays: days,
games: [game.id: richGame],
travelOverrides: [:],
itineraryItems: [],
allowCustomItems: false
)
// Find the first day section and verify it has a game
let firstDaySection = sections.first {
if case .day(1, _, _) = $0 { return true }
return false
}
if case .day(_, _, let gamesOnDay) = firstDaySection {
#expect(gamesOnDay.count == 1)
#expect(gamesOnDay.first?.game.id == game.id)
} else {
Issue.record("Expected day section with games")
}
}
@Test("travel segments appear in sections")
func travelSegmentsAppear() {
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"],
startDate: startDate,
daysPerStop: 1
)
// Create trip with travel segment
var trip = baseTrip
trip.travelSegments = [travel]
// Build without overrides - travel appears at default position
let sectionsDefault = ItinerarySectionBuilder.build(
trip: trip,
tripDays: days,
games: [:],
travelOverrides: [:],
itineraryItems: [],
allowCustomItems: false
)
let travelSections = sectionsDefault.filter {
if case .travel = $0 { return true }
return false
}
// TravelPlacement should place the travel segment
#expect(travelSections.count == 1)
// Verify travel appears before day 2 (arrival day)
if let travelIdx = sectionsDefault.firstIndex(where: {
if case .travel = $0 { return true }
return false
}) {
let nextIdx = sectionsDefault.index(after: travelIdx)
if nextIdx < sectionsDefault.count,
case .day(let dayNum, _, _) = sectionsDefault[nextIdx] {
#expect(dayNum == 2)
}
}
}
@Test("custom items included when allowCustomItems is true")
func customItemsIncluded() {
let startDate = TestClock.calendar.startOfDay(for: TestClock.now)
let (trip, days) = makeTrip(cities: ["New York"], startDate: startDate)
let customItem = ItineraryItem(
tripId: trip.id,
day: 1,
sortOrder: 1.0,
kind: .custom(CustomInfo(title: "Lunch at Joe's", icon: "fork.knife"))
)
let sectionsWithItems = ItinerarySectionBuilder.build(
trip: trip,
tripDays: days,
games: [:],
travelOverrides: [:],
itineraryItems: [customItem],
allowCustomItems: true
)
let sectionsWithoutItems = ItinerarySectionBuilder.build(
trip: trip,
tripDays: days,
games: [:],
travelOverrides: [:],
itineraryItems: [customItem],
allowCustomItems: false
)
let customSectionsWithItems = sectionsWithItems.filter { $0.isCustomItem }
let addButtonSections = sectionsWithItems.filter {
if case .addButton = $0 { return true }
return false
}
let customSectionsWithoutItems = sectionsWithoutItems.filter { $0.isCustomItem }
#expect(customSectionsWithItems.count == 1)
#expect(addButtonSections.count >= 1)
#expect(customSectionsWithoutItems.count == 0)
}
@Test("stableTravelAnchorId format is consistent")
func stableTravelAnchorIdFormat() {
let segment = TestFixtures.travelSegment(from: "New York", to: "Boston")
let id = ItinerarySectionBuilder.stableTravelAnchorId(segment, at: 0)
#expect(id.hasPrefix("travel:0:"))
#expect(id.contains("->"))
}
@Test("empty trip produces no sections")
func emptyTripProducesNoSections() {
let trip = TestFixtures.trip(stops: [])
let sections = ItinerarySectionBuilder.build(
trip: trip,
tripDays: [],
games: [:],
travelOverrides: [:],
itineraryItems: [],
allowCustomItems: false
)
#expect(sections.isEmpty)
}
}