feat: add planning tips and grouped trip options sorting
- Add PlanningTips data model with 105 tips across 7 categories - Wire random tips into HomeView (3 tips per session) - Add TripOptionsGrouper for grouping by city/game count and mileage - Update TripOptionsView with sectioned display when sorting - Recommended and Best Efficiency remain flat lists Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
38
SportsTimeTests/PlanningTipsTests.swift
Normal file
38
SportsTimeTests/PlanningTipsTests.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// PlanningTipsTests.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
|
||||
import Testing
|
||||
@testable import SportsTime
|
||||
|
||||
struct PlanningTipsTests {
|
||||
|
||||
@Test func allTipsHasAtLeast100Tips() {
|
||||
#expect(PlanningTips.all.count >= 100)
|
||||
}
|
||||
|
||||
@Test func randomReturnsRequestedCount() {
|
||||
let tips = PlanningTips.random(3)
|
||||
#expect(tips.count == 3)
|
||||
}
|
||||
|
||||
@Test func randomReturnsUniqueIds() {
|
||||
let tips = PlanningTips.random(5)
|
||||
let uniqueIds = Set(tips.map { $0.id })
|
||||
#expect(uniqueIds.count == 5)
|
||||
}
|
||||
|
||||
@Test func eachTipHasNonEmptyFields() {
|
||||
for tip in PlanningTips.all {
|
||||
#expect(!tip.icon.isEmpty, "Tip should have icon")
|
||||
#expect(!tip.title.isEmpty, "Tip should have title")
|
||||
#expect(!tip.subtitle.isEmpty, "Tip should have subtitle")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func randomWithCountGreaterThanAvailableReturnsAll() {
|
||||
let tips = PlanningTips.random(1000)
|
||||
#expect(tips.count == PlanningTips.all.count)
|
||||
}
|
||||
}
|
||||
95
SportsTimeTests/TripOptionsGroupingTests.swift
Normal file
95
SportsTimeTests/TripOptionsGroupingTests.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// TripOptionsGroupingTests.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
|
||||
import Testing
|
||||
@testable import SportsTime
|
||||
|
||||
struct TripOptionsGroupingTests {
|
||||
|
||||
// Helper to create mock ItineraryOption
|
||||
private func makeOption(stops: [(city: String, games: [String])], totalMiles: Double = 500) -> ItineraryOption {
|
||||
let tripStops = stops.map { stopData in
|
||||
TripStop(
|
||||
city: stopData.city,
|
||||
state: "XX",
|
||||
coordinate: .init(latitude: 0, longitude: 0),
|
||||
games: stopData.games,
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date(),
|
||||
travelFromPrevious: nil
|
||||
)
|
||||
}
|
||||
return ItineraryOption(
|
||||
id: UUID().uuidString,
|
||||
stops: tripStops,
|
||||
totalDistanceMiles: totalMiles,
|
||||
totalDrivingHours: totalMiles / 60,
|
||||
score: 1.0
|
||||
)
|
||||
}
|
||||
|
||||
@Test func groupByCityCountDescending() {
|
||||
let options = [
|
||||
makeOption(stops: [("NYC", []), ("Boston", [])]), // 2 cities
|
||||
makeOption(stops: [("LA", []), ("SF", []), ("Seattle", [])]), // 3 cities
|
||||
makeOption(stops: [("Chicago", [])]), // 1 city
|
||||
]
|
||||
|
||||
let grouped = TripOptionsGrouper.groupByCityCount(options, ascending: false)
|
||||
|
||||
#expect(grouped.count == 3)
|
||||
#expect(grouped[0].header == "3 cities")
|
||||
#expect(grouped[1].header == "2 cities")
|
||||
#expect(grouped[2].header == "1 city")
|
||||
}
|
||||
|
||||
@Test func groupByGameCountAscending() {
|
||||
let options = [
|
||||
makeOption(stops: [("NYC", ["g1", "g2", "g3"])]), // 3 games
|
||||
makeOption(stops: [("LA", ["g1"])]), // 1 game
|
||||
makeOption(stops: [("Chicago", ["g1", "g2"])]), // 2 games
|
||||
]
|
||||
|
||||
let grouped = TripOptionsGrouper.groupByGameCount(options, ascending: true)
|
||||
|
||||
#expect(grouped.count == 3)
|
||||
#expect(grouped[0].header == "1 game")
|
||||
#expect(grouped[1].header == "2 games")
|
||||
#expect(grouped[2].header == "3 games")
|
||||
}
|
||||
|
||||
@Test func groupByMileageRangeDescending() {
|
||||
let options = [
|
||||
makeOption(stops: [("NYC", [])], totalMiles: 300), // 0-500
|
||||
makeOption(stops: [("LA", [])], totalMiles: 1200), // 1000-1500
|
||||
makeOption(stops: [("Chicago", [])], totalMiles: 2500), // 2000+
|
||||
]
|
||||
|
||||
let grouped = TripOptionsGrouper.groupByMileageRange(options, ascending: false)
|
||||
|
||||
#expect(grouped[0].header == "2000+ mi")
|
||||
#expect(grouped[1].header == "1000-1500 mi")
|
||||
#expect(grouped[2].header == "0-500 mi")
|
||||
}
|
||||
|
||||
@Test func groupByMileageRangeAscending() {
|
||||
let options = [
|
||||
makeOption(stops: [("NYC", [])], totalMiles: 300),
|
||||
makeOption(stops: [("LA", [])], totalMiles: 1200),
|
||||
makeOption(stops: [("Chicago", [])], totalMiles: 2500),
|
||||
]
|
||||
|
||||
let grouped = TripOptionsGrouper.groupByMileageRange(options, ascending: true)
|
||||
|
||||
#expect(grouped[0].header == "0-500 mi")
|
||||
#expect(grouped[1].header == "1000-1500 mi")
|
||||
#expect(grouped[2].header == "2000+ mi")
|
||||
}
|
||||
|
||||
@Test func emptyOptionsReturnsEmptyGroups() {
|
||||
let grouped = TripOptionsGrouper.groupByCityCount([], ascending: false)
|
||||
#expect(grouped.isEmpty)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user