fix: 10 audit fixes — memory safety, performance, accessibility, architecture
- Add a11y label to ProgressMapView reset button and progress bar values - Fix CADisplayLink retain cycle in ItineraryTableViewController via deinit - Add [weak self] to PhotoGalleryViewModel Task closure - Add @MainActor to TripWizardViewModel, remove manual MainActor.run hop - Fix O(n²) rank lookup in PollDetailView/DebugPollPreviewView with enumerated() - Cache itinerarySections via ItinerarySectionBuilder static extraction + @State - Convert CanonicalSyncService/BootstrapService from actor to @MainActor final class - Add .accessibilityHidden(true) to RegionMapSelector Map to prevent duplicate VoiceOver Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
217
SportsTimeTests/Features/Trip/ItinerarySectionBuilderTests.swift
Normal file
217
SportsTimeTests/Features/Trip/ItinerarySectionBuilderTests.swift
Normal file
@@ -0,0 +1,217 @@
|
||||
//
|
||||
// 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 = Date()) -> [Date] {
|
||||
(0..<count).map {
|
||||
Calendar.current.date(byAdding: .day, value: $0, to: Calendar.current.startOfDay(for: startDate))!
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTrip(
|
||||
cities: [String] = ["New York", "Boston"],
|
||||
startDate: Date = Date(),
|
||||
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 = Calendar.current.startOfDay(for: Date())
|
||||
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 = Calendar.current.startOfDay(for: Date())
|
||||
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 = Calendar.current.startOfDay(for: Date())
|
||||
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 = Calendar.current.startOfDay(for: Date())
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user