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>
316 lines
10 KiB
Swift
316 lines
10 KiB
Swift
//
|
|
// GameMatcherTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// TDD specification tests for GameMatcher types.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
// MARK: - NoMatchReason Tests
|
|
|
|
@Suite("NoMatchReason")
|
|
struct NoMatchReasonTests {
|
|
|
|
// MARK: - Specification Tests: description
|
|
|
|
/// - Expected Behavior: Each reason has a user-friendly description
|
|
@Test("description: noStadiumNearby has description")
|
|
func description_noStadiumNearby() {
|
|
let reason = NoMatchReason.noStadiumNearby
|
|
#expect(!reason.description.isEmpty)
|
|
#expect(reason.description.lowercased().contains("stadium") || reason.description.lowercased().contains("nearby"))
|
|
}
|
|
|
|
@Test("description: noGamesOnDate has description")
|
|
func description_noGamesOnDate() {
|
|
let reason = NoMatchReason.noGamesOnDate
|
|
#expect(!reason.description.isEmpty)
|
|
#expect(reason.description.lowercased().contains("game") || reason.description.lowercased().contains("date"))
|
|
}
|
|
|
|
@Test("description: metadataMissing noLocation has description")
|
|
func description_metadataMissing_noLocation() {
|
|
let reason = NoMatchReason.metadataMissing(.noLocation)
|
|
#expect(!reason.description.isEmpty)
|
|
#expect(reason.description.lowercased().contains("location"))
|
|
}
|
|
|
|
@Test("description: metadataMissing noDate has description")
|
|
func description_metadataMissing_noDate() {
|
|
let reason = NoMatchReason.metadataMissing(.noDate)
|
|
#expect(!reason.description.isEmpty)
|
|
#expect(reason.description.lowercased().contains("date"))
|
|
}
|
|
|
|
@Test("description: metadataMissing noBoth has description")
|
|
func description_metadataMissing_noBoth() {
|
|
let reason = NoMatchReason.metadataMissing(.noBoth)
|
|
#expect(!reason.description.isEmpty)
|
|
#expect(reason.description.lowercased().contains("location") || reason.description.lowercased().contains("date"))
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
/// - Invariant: All reasons have non-empty descriptions
|
|
@Test("Invariant: all reasons have non-empty descriptions")
|
|
func invariant_allHaveDescriptions() {
|
|
let allReasons: [NoMatchReason] = [
|
|
.noStadiumNearby,
|
|
.noGamesOnDate,
|
|
.metadataMissing(.noLocation),
|
|
.metadataMissing(.noDate),
|
|
.metadataMissing(.noBoth)
|
|
]
|
|
|
|
for reason in allReasons {
|
|
#expect(!reason.description.isEmpty, "Reason should have description")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - GameMatchResult Tests
|
|
|
|
@Suite("GameMatchResult")
|
|
struct GameMatchResultTests {
|
|
|
|
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
|
|
|
// MARK: - Test Data
|
|
|
|
private func makeGame(id: String = "game_1") -> Game {
|
|
Game(
|
|
id: id,
|
|
homeTeamId: "home_team",
|
|
awayTeamId: "away_team",
|
|
stadiumId: "stadium_1",
|
|
dateTime: Date(),
|
|
sport: .mlb,
|
|
season: "2026"
|
|
)
|
|
}
|
|
|
|
private func makeStadium() -> Stadium {
|
|
Stadium(
|
|
id: "stadium_1",
|
|
name: "Test Stadium",
|
|
city: "Test City",
|
|
state: "TS",
|
|
latitude: nycCoord.latitude,
|
|
longitude: nycCoord.longitude,
|
|
capacity: 40000,
|
|
sport: .mlb
|
|
)
|
|
}
|
|
|
|
private func makeTeam(id: String = "team_1") -> Team {
|
|
Team(
|
|
id: id,
|
|
name: "Test Team",
|
|
abbreviation: "TST",
|
|
sport: .mlb,
|
|
city: "Test City",
|
|
stadiumId: "stadium_1"
|
|
)
|
|
}
|
|
|
|
private func makeCandidate(gameId: String = "game_1") -> GameMatchCandidate {
|
|
GameMatchCandidate(
|
|
game: makeGame(id: gameId),
|
|
stadium: makeStadium(),
|
|
homeTeam: makeTeam(id: "home"),
|
|
awayTeam: makeTeam(id: "away"),
|
|
confidence: PhotoMatchConfidence(spatial: .high, temporal: .exactDay)
|
|
)
|
|
}
|
|
|
|
// MARK: - Specification Tests: hasMatch
|
|
|
|
/// - Expected Behavior: singleMatch returns true for hasMatch
|
|
@Test("hasMatch: true for singleMatch case")
|
|
func hasMatch_singleMatch() {
|
|
let result = GameMatchResult.singleMatch(makeCandidate())
|
|
#expect(result.hasMatch == true)
|
|
}
|
|
|
|
/// - Expected Behavior: multipleMatches returns true for hasMatch
|
|
@Test("hasMatch: true for multipleMatches case")
|
|
func hasMatch_multipleMatches() {
|
|
let candidates = [makeCandidate(gameId: "game_1"), makeCandidate(gameId: "game_2")]
|
|
let result = GameMatchResult.multipleMatches(candidates)
|
|
#expect(result.hasMatch == true)
|
|
}
|
|
|
|
/// - Expected Behavior: noMatches returns false for hasMatch
|
|
@Test("hasMatch: false for noMatches case")
|
|
func hasMatch_noMatches() {
|
|
let result = GameMatchResult.noMatches(.noGamesOnDate)
|
|
#expect(result.hasMatch == false)
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
/// - Invariant: singleMatch and multipleMatches always hasMatch
|
|
@Test("Invariant: match cases always hasMatch")
|
|
func invariant_matchCasesHaveMatch() {
|
|
let single = GameMatchResult.singleMatch(makeCandidate())
|
|
let multiple = GameMatchResult.multipleMatches([makeCandidate()])
|
|
|
|
#expect(single.hasMatch == true)
|
|
#expect(multiple.hasMatch == true)
|
|
}
|
|
|
|
/// - Invariant: noMatches never hasMatch
|
|
@Test("Invariant: noMatches never hasMatch")
|
|
func invariant_noMatchesNeverHasMatch() {
|
|
let reasons: [NoMatchReason] = [
|
|
.noStadiumNearby,
|
|
.noGamesOnDate,
|
|
.metadataMissing(.noLocation)
|
|
]
|
|
|
|
for reason in reasons {
|
|
let result = GameMatchResult.noMatches(reason)
|
|
#expect(result.hasMatch == false)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - GameMatchCandidate Tests
|
|
|
|
@Suite("GameMatchCandidate")
|
|
struct GameMatchCandidateTests {
|
|
|
|
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
|
|
|
// MARK: - Test Data
|
|
|
|
private func makeGame() -> Game {
|
|
Game(
|
|
id: "game_test",
|
|
homeTeamId: "home_team",
|
|
awayTeamId: "away_team",
|
|
stadiumId: "stadium_1",
|
|
dateTime: Date(),
|
|
sport: .mlb,
|
|
season: "2026"
|
|
)
|
|
}
|
|
|
|
private func makeStadium() -> Stadium {
|
|
Stadium(
|
|
id: "stadium_1",
|
|
name: "Test Stadium",
|
|
city: "Test City",
|
|
state: "TS",
|
|
latitude: nycCoord.latitude,
|
|
longitude: nycCoord.longitude,
|
|
capacity: 40000,
|
|
sport: .mlb
|
|
)
|
|
}
|
|
|
|
private func makeTeam(id: String, name: String, abbreviation: String) -> Team {
|
|
Team(
|
|
id: id,
|
|
name: name,
|
|
abbreviation: abbreviation,
|
|
sport: .mlb,
|
|
city: "Test",
|
|
stadiumId: "stadium_1"
|
|
)
|
|
}
|
|
|
|
// MARK: - Specification Tests
|
|
|
|
@Test("id: matches game id")
|
|
func id_matchesGameId() {
|
|
let game = makeGame()
|
|
let candidate = GameMatchCandidate(
|
|
game: game,
|
|
stadium: makeStadium(),
|
|
homeTeam: makeTeam(id: "home", name: "Home Team", abbreviation: "HOM"),
|
|
awayTeam: makeTeam(id: "away", name: "Away Team", abbreviation: "AWY"),
|
|
confidence: PhotoMatchConfidence(spatial: .high, temporal: .exactDay)
|
|
)
|
|
|
|
#expect(candidate.id == game.id)
|
|
}
|
|
|
|
@Test("matchupDescription: returns abbreviations format")
|
|
func matchupDescription_format() {
|
|
let candidate = GameMatchCandidate(
|
|
game: makeGame(),
|
|
stadium: makeStadium(),
|
|
homeTeam: makeTeam(id: "home", name: "Home Team", abbreviation: "HOM"),
|
|
awayTeam: makeTeam(id: "away", name: "Away Team", abbreviation: "AWY"),
|
|
confidence: PhotoMatchConfidence(spatial: .high, temporal: .exactDay)
|
|
)
|
|
|
|
#expect(candidate.matchupDescription == "AWY @ HOM")
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
/// - Invariant: id is always equal to game.id
|
|
@Test("Invariant: id equals game.id")
|
|
func invariant_idEqualsGameId() {
|
|
let game = makeGame()
|
|
let candidate = GameMatchCandidate(
|
|
game: game,
|
|
stadium: makeStadium(),
|
|
homeTeam: makeTeam(id: "home", name: "Home", abbreviation: "HOM"),
|
|
awayTeam: makeTeam(id: "away", name: "Away", abbreviation: "AWY"),
|
|
confidence: PhotoMatchConfidence(spatial: .medium, temporal: .adjacentDay)
|
|
)
|
|
|
|
#expect(candidate.id == candidate.game.id)
|
|
}
|
|
}
|
|
|
|
// MARK: - PhotoMatchConfidence Tests
|
|
|
|
@Suite("PhotoMatchConfidence")
|
|
struct PhotoMatchConfidenceTests {
|
|
|
|
// MARK: - Specification Tests: combined
|
|
|
|
@Test("combined: high spatial + exactDay = autoSelect")
|
|
func combined_highAndExact() {
|
|
let confidence = PhotoMatchConfidence(spatial: .high, temporal: .exactDay)
|
|
#expect(confidence.combined == .autoSelect)
|
|
}
|
|
|
|
@Test("combined: medium spatial + adjacentDay = userConfirm")
|
|
func combined_mediumAndAdjacentDay() {
|
|
let confidence = PhotoMatchConfidence(spatial: .medium, temporal: .adjacentDay)
|
|
#expect(confidence.combined == .userConfirm)
|
|
}
|
|
|
|
@Test("combined: low spatial = manualOnly")
|
|
func combined_lowSpatial() {
|
|
let confidence = PhotoMatchConfidence(spatial: .low, temporal: .exactDay)
|
|
#expect(confidence.combined == .manualOnly)
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
/// - Invariant: combined is always derived from spatial and temporal
|
|
@Test("Invariant: combined is deterministic from spatial and temporal")
|
|
func invariant_combinedDeterministic() {
|
|
let spatials: [MatchConfidence] = [.high, .medium, .low]
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|