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>
341 lines
11 KiB
Swift
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)
|
|
}
|
|
}
|