Files
Sportstime/SportsTimeTests/Services/StadiumProximityMatcherTests.swift
Trey t 8162b4a029 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>
2026-01-16 14:07:41 -06:00

341 lines
11 KiB
Swift

//
// StadiumProximityMatcherTests.swift
// SportsTimeTests
//
// TDD specification tests for StadiumProximityMatcher and related types.
//
import Testing
import Foundation
import CoreLocation
@testable import SportsTime
// MARK: - MatchConfidence Tests
@Suite("MatchConfidence")
struct MatchConfidenceTests {
// MARK: - Specification Tests: description
@Test("description: high has description")
func description_high() {
#expect(!MatchConfidence.high.description.isEmpty)
}
@Test("description: medium has description")
func description_medium() {
#expect(!MatchConfidence.medium.description.isEmpty)
}
@Test("description: low has description")
func description_low() {
#expect(!MatchConfidence.low.description.isEmpty)
}
@Test("description: none has description")
func description_none() {
#expect(!MatchConfidence.none.description.isEmpty)
}
// MARK: - Specification Tests: shouldAutoSelect
@Test("shouldAutoSelect: true for high confidence")
func shouldAutoSelect_high() {
#expect(MatchConfidence.high.shouldAutoSelect == true)
}
@Test("shouldAutoSelect: false for medium confidence")
func shouldAutoSelect_medium() {
#expect(MatchConfidence.medium.shouldAutoSelect == false)
}
@Test("shouldAutoSelect: false for low confidence")
func shouldAutoSelect_low() {
#expect(MatchConfidence.low.shouldAutoSelect == false)
}
@Test("shouldAutoSelect: false for none confidence")
func shouldAutoSelect_none() {
#expect(MatchConfidence.none.shouldAutoSelect == false)
}
// MARK: - Specification Tests: Comparable
@Test("Comparable: high > medium > low > none")
func comparable_ordering() {
#expect(MatchConfidence.high > MatchConfidence.medium)
#expect(MatchConfidence.medium > MatchConfidence.low)
#expect(MatchConfidence.low > MatchConfidence.none)
}
// MARK: - Invariant Tests
@Test("Invariant: all cases have non-empty description")
func invariant_allHaveDescription() {
let cases: [MatchConfidence] = [.high, .medium, .low, .none]
for confidence in cases {
#expect(!confidence.description.isEmpty)
}
}
}
// MARK: - TemporalConfidence Tests
@Suite("TemporalConfidence")
struct TemporalConfidenceTests {
// MARK: - Specification Tests: description
@Test("description: exactDay has description")
func description_exactDay() {
#expect(!TemporalConfidence.exactDay.description.isEmpty)
}
@Test("description: adjacentDay has description")
func description_adjacentDay() {
#expect(!TemporalConfidence.adjacentDay.description.isEmpty)
}
@Test("description: outOfRange has description")
func description_outOfRange() {
#expect(!TemporalConfidence.outOfRange.description.isEmpty)
}
// MARK: - Specification Tests: Comparable
@Test("Comparable: exactDay > adjacentDay > outOfRange")
func comparable_ordering() {
#expect(TemporalConfidence.exactDay > TemporalConfidence.adjacentDay)
#expect(TemporalConfidence.adjacentDay > TemporalConfidence.outOfRange)
}
// MARK: - Invariant Tests
@Test("Invariant: all cases have non-empty description")
func invariant_allHaveDescription() {
let cases: [TemporalConfidence] = [.exactDay, .adjacentDay, .outOfRange]
for temporal in cases {
#expect(!temporal.description.isEmpty)
}
}
}
// MARK: - CombinedConfidence Tests
@Suite("CombinedConfidence")
struct CombinedConfidenceTests {
// MARK: - Specification Tests: combine
@Test("combine: high + exactDay = autoSelect")
func combine_highExactDay() {
let result = CombinedConfidence.combine(spatial: .high, temporal: .exactDay)
#expect(result == .autoSelect)
}
@Test("combine: high + adjacentDay = userConfirm")
func combine_highAdjacentDay() {
let result = CombinedConfidence.combine(spatial: .high, temporal: .adjacentDay)
#expect(result == .userConfirm)
}
@Test("combine: medium + exactDay = userConfirm")
func combine_mediumExactDay() {
let result = CombinedConfidence.combine(spatial: .medium, temporal: .exactDay)
#expect(result == .userConfirm)
}
@Test("combine: medium + adjacentDay = userConfirm")
func combine_mediumAdjacentDay() {
let result = CombinedConfidence.combine(spatial: .medium, temporal: .adjacentDay)
#expect(result == .userConfirm)
}
@Test("combine: low spatial = manualOnly regardless of temporal")
func combine_lowSpatial() {
#expect(CombinedConfidence.combine(spatial: .low, temporal: .exactDay) == .manualOnly)
#expect(CombinedConfidence.combine(spatial: .low, temporal: .adjacentDay) == .manualOnly)
#expect(CombinedConfidence.combine(spatial: .low, temporal: .outOfRange) == .manualOnly)
}
@Test("combine: none spatial = manualOnly regardless of temporal")
func combine_noneSpatial() {
#expect(CombinedConfidence.combine(spatial: .none, temporal: .exactDay) == .manualOnly)
#expect(CombinedConfidence.combine(spatial: .none, temporal: .adjacentDay) == .manualOnly)
#expect(CombinedConfidence.combine(spatial: .none, temporal: .outOfRange) == .manualOnly)
}
@Test("combine: outOfRange temporal with high/medium spatial = manualOnly")
func combine_outOfRangeTemporal() {
#expect(CombinedConfidence.combine(spatial: .high, temporal: .outOfRange) == .manualOnly)
#expect(CombinedConfidence.combine(spatial: .medium, temporal: .outOfRange) == .manualOnly)
}
// MARK: - Specification Tests: description
@Test("description: autoSelect has description")
func description_autoSelect() {
#expect(!CombinedConfidence.autoSelect.description.isEmpty)
}
@Test("description: userConfirm has description")
func description_userConfirm() {
#expect(!CombinedConfidence.userConfirm.description.isEmpty)
}
@Test("description: manualOnly has description")
func description_manualOnly() {
#expect(!CombinedConfidence.manualOnly.description.isEmpty)
}
// MARK: - Specification Tests: Comparable
@Test("Comparable: autoSelect > userConfirm > manualOnly")
func comparable_ordering() {
#expect(CombinedConfidence.autoSelect > CombinedConfidence.userConfirm)
#expect(CombinedConfidence.userConfirm > CombinedConfidence.manualOnly)
}
}
// MARK: - StadiumMatch Tests
@Suite("StadiumMatch")
struct StadiumMatchTests {
private func makeStadium() -> Stadium {
Stadium(
id: "stadium_1",
name: "Test Stadium",
city: "Test City",
state: "TS",
latitude: 40.7580,
longitude: -73.9855,
capacity: 40000,
sport: .mlb
)
}
// MARK: - Specification Tests: confidence
@Test("confidence: high for distance < 500m")
func confidence_high() {
let match = StadiumMatch(stadium: makeStadium(), distance: 300)
#expect(match.confidence == .high)
}
@Test("confidence: medium for distance 500m - 2km")
func confidence_medium() {
let match = StadiumMatch(stadium: makeStadium(), distance: 1000)
#expect(match.confidence == .medium)
}
@Test("confidence: low for distance 2km - 5km")
func confidence_low() {
let match = StadiumMatch(stadium: makeStadium(), distance: 3000)
#expect(match.confidence == .low)
}
@Test("confidence: none for distance > 5km")
func confidence_none() {
let match = StadiumMatch(stadium: makeStadium(), distance: 6000)
#expect(match.confidence == .none)
}
// MARK: - Specification Tests: formattedDistance
@Test("formattedDistance: meters for < 1km")
func formattedDistance_meters() {
let match = StadiumMatch(stadium: makeStadium(), distance: 500)
#expect(match.formattedDistance.contains("m"))
#expect(!match.formattedDistance.contains("km"))
}
@Test("formattedDistance: kilometers for >= 1km")
func formattedDistance_kilometers() {
let match = StadiumMatch(stadium: makeStadium(), distance: 2500)
#expect(match.formattedDistance.contains("km"))
}
// MARK: - Specification Tests: Identifiable
@Test("id: matches stadium id")
func id_matchesStadiumId() {
let stadium = makeStadium()
let match = StadiumMatch(stadium: stadium, distance: 100)
#expect(match.id == stadium.id)
}
// MARK: - Invariant Tests
@Test("Invariant: confidence boundaries")
func invariant_confidenceBoundaries() {
let stadium = makeStadium()
// Boundary at 500m
#expect(StadiumMatch(stadium: stadium, distance: 499).confidence == .high)
#expect(StadiumMatch(stadium: stadium, distance: 500).confidence == .medium)
// Boundary at 2000m
#expect(StadiumMatch(stadium: stadium, distance: 1999).confidence == .medium)
#expect(StadiumMatch(stadium: stadium, distance: 2000).confidence == .low)
// Boundary at 5000m
#expect(StadiumMatch(stadium: stadium, distance: 4999).confidence == .low)
#expect(StadiumMatch(stadium: stadium, distance: 5000).confidence == .none)
}
}
// MARK: - PhotoMatchConfidence Composition Tests
@Suite("PhotoMatchConfidence Composition")
struct PhotoMatchConfidenceCompositionTests {
@Test("combined: derived from spatial and temporal")
func combined_derived() {
let confidence = PhotoMatchConfidence(spatial: .high, temporal: .exactDay)
#expect(confidence.combined == .autoSelect)
#expect(confidence.spatial == .high)
#expect(confidence.temporal == .exactDay)
}
@Test("combined: matches CombinedConfidence.combine result")
func combined_matchesCombine() {
let spatials: [MatchConfidence] = [.high, .medium, .low, .none]
let temporals: [TemporalConfidence] = [.exactDay, .adjacentDay, .outOfRange]
for spatial in spatials {
for temporal in temporals {
let confidence = PhotoMatchConfidence(spatial: spatial, temporal: temporal)
let expected = CombinedConfidence.combine(spatial: spatial, temporal: temporal)
#expect(confidence.combined == expected)
}
}
}
}
// MARK: - ProximityConstants Tests
@Suite("ProximityConstants")
struct ProximityConstantsTests {
@Test("highConfidenceRadius: 500m")
func highConfidenceRadius() {
#expect(ProximityConstants.highConfidenceRadius == 500)
}
@Test("mediumConfidenceRadius: 2km")
func mediumConfidenceRadius() {
#expect(ProximityConstants.mediumConfidenceRadius == 2000)
}
@Test("searchRadius: 5km")
func searchRadius() {
#expect(ProximityConstants.searchRadius == 5000)
}
@Test("dateToleranceDays: 1")
func dateToleranceDays() {
#expect(ProximityConstants.dateToleranceDays == 1)
}
}