refactor(tests): TDD rewrite of all unit tests with spec documentation

Complete rewrite of unit test suite using TDD methodology:

Planning Engine Tests:
- GameDAGRouterTests: Beam search, anchor games, transitions
- ItineraryBuilderTests: Stop connection, validators, EV enrichment
- RouteFiltersTests: Region, time window, scoring filters
- ScenarioA/B/C/D PlannerTests: All planning scenarios
- TravelEstimatorTests: Distance, duration, travel days
- TripPlanningEngineTests: Orchestration, caching, preferences

Domain Model Tests:
- AchievementDefinitionsTests, AnySportTests, DivisionTests
- GameTests, ProgressTests, RegionTests, StadiumTests
- TeamTests, TravelSegmentTests, TripTests, TripPollTests
- TripPreferencesTests, TripStopTests, SportTests

Service Tests:
- FreeScoreAPITests, RouteDescriptionGeneratorTests
- SuggestedTripsGeneratorTests

Export Tests:
- ShareableContentTests (card types, themes, dimensions)

Bug fixes discovered through TDD:
- ShareCardDimensions: mapSnapshotSize exceeded available width (960x480)
- ScenarioBPlanner: Added anchor game validation filter

All tests include:
- Specification tests (expected behavior)
- Invariant tests (properties that must always hold)
- Edge case tests (boundary conditions)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-16 14:07:41 -06:00
parent 035dd6f5de
commit 8162b4a029
102 changed files with 13409 additions and 9883 deletions

View File

@@ -0,0 +1,203 @@
//
// POISearchServiceTests.swift
// SportsTimeTests
//
// TDD specification tests for POISearchService types.
//
import Testing
import Foundation
import CoreLocation
@testable import SportsTime
// MARK: - POI Tests
@Suite("POI")
struct POITests {
// MARK: - Test Data
private func makePOI(distanceMeters: Double) -> POISearchService.POI {
POISearchService.POI(
id: UUID(),
name: "Test POI",
category: .restaurant,
coordinate: CLLocationCoordinate2D(latitude: 40.0, longitude: -74.0),
distanceMeters: distanceMeters,
address: nil
)
}
// MARK: - Specification Tests: formattedDistance
/// - Expected Behavior: Distances < 0.1 miles format as feet
@Test("formattedDistance: short distances show feet")
func formattedDistance_feet() {
// 100 meters = ~328 feet = ~0.062 miles (less than 0.1)
let poi = makePOI(distanceMeters: 100)
let formatted = poi.formattedDistance
#expect(formatted.contains("ft"))
#expect(!formatted.contains("mi"))
}
/// - Expected Behavior: Distances >= 0.1 miles format as miles
@Test("formattedDistance: longer distances show miles")
func formattedDistance_miles() {
// 500 meters = ~0.31 miles (greater than 0.1)
let poi = makePOI(distanceMeters: 500)
let formatted = poi.formattedDistance
#expect(formatted.contains("mi"))
#expect(!formatted.contains("ft"))
}
/// - Expected Behavior: Boundary at 0.1 miles (~161 meters)
@Test("formattedDistance: boundary at 0.1 miles")
func formattedDistance_boundary() {
// 0.1 miles = ~161 meters
let justUnderPOI = makePOI(distanceMeters: 160) // Just under 0.1 miles
let justOverPOI = makePOI(distanceMeters: 162) // Just over 0.1 miles
#expect(justUnderPOI.formattedDistance.contains("ft"))
#expect(justOverPOI.formattedDistance.contains("mi"))
}
/// - Expected Behavior: Zero distance formats correctly
@Test("formattedDistance: handles zero distance")
func formattedDistance_zero() {
let poi = makePOI(distanceMeters: 0)
let formatted = poi.formattedDistance
#expect(formatted.contains("0") || formatted.contains("ft"))
}
/// - Expected Behavior: Large distance formats correctly
@Test("formattedDistance: handles large distance")
func formattedDistance_large() {
// 5000 meters = ~3.1 miles
let poi = makePOI(distanceMeters: 5000)
let formatted = poi.formattedDistance
#expect(formatted.contains("mi"))
#expect(formatted.contains("3.1") || formatted.contains("3.") || Double(formatted.replacingOccurrences(of: " mi", with: ""))! > 3.0)
}
// MARK: - Invariant Tests
/// - Invariant: formattedDistance always contains a unit
@Test("Invariant: formattedDistance always has unit")
func invariant_formattedDistanceHasUnit() {
let testDistances: [Double] = [0, 50, 100, 160, 162, 500, 1000, 5000]
for distance in testDistances {
let poi = makePOI(distanceMeters: distance)
let formatted = poi.formattedDistance
#expect(formatted.contains("ft") || formatted.contains("mi"))
}
}
}
// MARK: - POICategory Tests
@Suite("POICategory")
struct POICategoryTests {
// MARK: - Specification Tests: displayName
/// - Expected Behavior: Each category has a human-readable display name
@Test("displayName: returns readable name")
func displayName_readable() {
#expect(POISearchService.POICategory.restaurant.displayName == "Restaurant")
#expect(POISearchService.POICategory.attraction.displayName == "Attraction")
#expect(POISearchService.POICategory.entertainment.displayName == "Entertainment")
#expect(POISearchService.POICategory.nightlife.displayName == "Nightlife")
#expect(POISearchService.POICategory.museum.displayName == "Museum")
}
// MARK: - Specification Tests: iconName
/// - Expected Behavior: Each category has a valid SF Symbol name
@Test("iconName: returns SF Symbol name")
func iconName_sfSymbol() {
#expect(POISearchService.POICategory.restaurant.iconName == "fork.knife")
#expect(POISearchService.POICategory.attraction.iconName == "star.fill")
#expect(POISearchService.POICategory.entertainment.iconName == "theatermasks.fill")
#expect(POISearchService.POICategory.nightlife.iconName == "moon.stars.fill")
#expect(POISearchService.POICategory.museum.iconName == "building.columns.fill")
}
// MARK: - Specification Tests: searchQuery
/// - Expected Behavior: Each category has a search-friendly query string
@Test("searchQuery: returns search string")
func searchQuery_searchString() {
#expect(POISearchService.POICategory.restaurant.searchQuery == "restaurants")
#expect(POISearchService.POICategory.attraction.searchQuery == "tourist attractions")
#expect(POISearchService.POICategory.entertainment.searchQuery == "entertainment")
#expect(POISearchService.POICategory.nightlife.searchQuery == "bars nightlife")
#expect(POISearchService.POICategory.museum.searchQuery == "museums")
}
// MARK: - Invariant Tests
/// - Invariant: All categories have non-empty properties
@Test("Invariant: all categories have non-empty properties")
func invariant_nonEmptyProperties() {
for category in POISearchService.POICategory.allCases {
#expect(!category.displayName.isEmpty)
#expect(!category.iconName.isEmpty)
#expect(!category.searchQuery.isEmpty)
}
}
/// - Invariant: CaseIterable includes all cases
@Test("Invariant: CaseIterable includes all cases")
func invariant_allCasesIncluded() {
#expect(POISearchService.POICategory.allCases.count == 5)
#expect(POISearchService.POICategory.allCases.contains(.restaurant))
#expect(POISearchService.POICategory.allCases.contains(.attraction))
#expect(POISearchService.POICategory.allCases.contains(.entertainment))
#expect(POISearchService.POICategory.allCases.contains(.nightlife))
#expect(POISearchService.POICategory.allCases.contains(.museum))
}
}
// MARK: - POISearchError Tests
@Suite("POISearchError")
struct POISearchErrorTests {
// MARK: - Specification Tests: errorDescription
/// - Expected Behavior: searchFailed includes the reason
@Test("errorDescription: searchFailed includes reason")
func errorDescription_searchFailed() {
let error = POISearchService.POISearchError.searchFailed("Network error")
#expect(error.errorDescription != nil)
#expect(error.errorDescription!.contains("Network error") || error.errorDescription!.lowercased().contains("search"))
}
/// - Expected Behavior: noResults explains no POIs found
@Test("errorDescription: noResults mentions no results")
func errorDescription_noResults() {
let error = POISearchService.POISearchError.noResults
#expect(error.errorDescription != nil)
#expect(error.errorDescription!.lowercased().contains("no") || error.errorDescription!.lowercased().contains("found"))
}
// MARK: - Invariant Tests
/// - Invariant: All errors have non-empty descriptions
@Test("Invariant: all errors have descriptions")
func invariant_allHaveDescriptions() {
let errors: [POISearchService.POISearchError] = [
.searchFailed("test"),
.noResults
]
for error in errors {
#expect(error.errorDescription != nil)
#expect(!error.errorDescription!.isEmpty)
}
}
}