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:
@@ -4,6 +4,19 @@
|
||||
//
|
||||
// Registry of all achievement types and their requirements.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - AchievementRegistry.all is sorted by sortOrder ascending
|
||||
// - achievement(byId:) returns nil for unknown IDs
|
||||
// - achievements(forCategory:) filters by exact category match
|
||||
// - achievements(forSport:) includes sport-specific AND nil-sport (cross-sport) achievements
|
||||
// - divisionAchievements(forSport:) filters to category=.division AND sport
|
||||
//
|
||||
// - Invariants:
|
||||
// - All achievement IDs are unique
|
||||
// - Division achievements have non-nil divisionId
|
||||
// - Conference achievements have non-nil conferenceId
|
||||
// - AchievementDefinition equality is based solely on id
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
//
|
||||
// Protocol unifying Sport enum and DynamicSport for interchangeable use.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - isInSeason(for:) default implementation handles wrap-around seasons
|
||||
// - For normal seasons (start <= end): month >= start AND month <= end
|
||||
// - For wrap-around seasons (start > end): month >= start OR month <= end
|
||||
//
|
||||
// - Invariants:
|
||||
// - isInSeason returns true for exactly the months in the season range
|
||||
// - seasonMonths start and end are always 1-12
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
@@ -4,6 +4,18 @@
|
||||
//
|
||||
// Domain model for league structure: divisions and conferences.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - LeagueStructure.divisions(for:) returns divisions for MLB, NBA, NHL; empty for others
|
||||
// - LeagueStructure.conferences(for:) returns conferences filtered by sport
|
||||
// - LeagueStructure.division(byId:) finds division across all sports
|
||||
// - LeagueStructure.conference(byId:) finds conference across all sports
|
||||
// - stadiumCount returns 30 for MLB, 30 for NBA, 32 for NHL, 0 for others
|
||||
//
|
||||
// - Invariants:
|
||||
// - Division.teamCount == teamCanonicalIds.count
|
||||
// - MLB has 6 divisions (3 per league), NBA has 6, NHL has 4
|
||||
// - Each conference contains valid division IDs
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
//
|
||||
// Domain model for CloudKit-defined sports.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - sportId returns id (from AnySport protocol)
|
||||
// - color converts colorHex to SwiftUI Color
|
||||
// - seasonMonths returns (seasonStartMonth, seasonEndMonth)
|
||||
// - isInSeason uses default AnySport implementation
|
||||
//
|
||||
// - Invariants:
|
||||
// - sportId == id (they are aliases)
|
||||
// - seasonStartMonth and seasonEndMonth are 1-12
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
// Game.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Domain model for a scheduled game.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - gameDate returns the start of day (midnight) for dateTime
|
||||
// - startTime is an alias for dateTime
|
||||
// - Two games are equal if and only if their ids match
|
||||
//
|
||||
// - Invariants:
|
||||
// - gameDate is always at midnight (00:00:00) local time
|
||||
// - startTime == dateTime (they are aliases)
|
||||
// - Equality is based solely on id, not other fields
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
@@ -4,6 +4,19 @@
|
||||
//
|
||||
// Domain models for tracking stadium visit progress and achievements.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - completionPercentage = (visited / total) * 100, returns 0 when total is 0
|
||||
// - progressFraction = visited / total, returns 0 when total is 0
|
||||
// - isComplete = total > 0 AND visited >= total
|
||||
// - StadiumVisitStatus.latestVisit returns visit with max date
|
||||
// - StadiumVisitStatus.firstVisit returns visit with min date
|
||||
//
|
||||
// - Invariants:
|
||||
// - completionPercentage is in range [0, 100]
|
||||
// - progressFraction is in range [0, 1]
|
||||
// - isComplete can only be true if totalStadiums > 0
|
||||
// - visitCount == 0 for notVisited status
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
//
|
||||
// Geographic regions for trip classification.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - classify(longitude:) returns .east for longitude > -85
|
||||
// - classify(longitude:) returns .central for longitude in -110...-85 (inclusive)
|
||||
// - classify(longitude:) returns .west for longitude < -110
|
||||
// - Boundary values: -85 → central, -110 → central
|
||||
//
|
||||
// - Invariants:
|
||||
// - Every longitude maps to exactly one region (east, central, or west)
|
||||
// - crossCountry is never returned by classify() (it's for trip categorization)
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
// Sport.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Enumeration of supported sports with season information.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - isInSeason(for:) returns true when month falls within seasonMonths range
|
||||
// - For wrap-around seasons (start > end), month is in season if >= start OR <= end
|
||||
// - For normal seasons (start <= end), month is in season if >= start AND <= end
|
||||
// - supported returns all 7 sports in a consistent order
|
||||
//
|
||||
// - Invariants:
|
||||
// - Each sport has exactly one season range (start, end)
|
||||
// - seasonMonths values are always 1-12 (valid months)
|
||||
// - All CaseIterable cases match the supported array
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
// Stadium.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Domain model for a sports venue.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - region is derived from longitude using Region.classify()
|
||||
// - distance(to:) returns meters between two stadiums
|
||||
// - coordinate returns CLLocationCoordinate2D from latitude/longitude
|
||||
// - Two stadiums are equal if and only if their ids match
|
||||
//
|
||||
// - Invariants:
|
||||
// - location and coordinate always match latitude/longitude
|
||||
// - region is always consistent with Region.classify(longitude:)
|
||||
// - Equality is based solely on id
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
// Team.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Domain model for a sports team.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - fullName returns "city name" when city is non-empty
|
||||
// - fullName returns just name when city is empty
|
||||
// - Two teams are equal if and only if their ids match
|
||||
//
|
||||
// - Invariants:
|
||||
// - fullName is never empty (name is required)
|
||||
// - Equality is based solely on id
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
// TravelSegment.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Domain model for travel between trip stops.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - distanceMiles = distanceMeters * 0.000621371
|
||||
// - durationHours = durationSeconds / 3600
|
||||
// - estimatedDrivingHours and estimatedDistanceMiles are aliases
|
||||
// - formattedDuration shows "Xh Ym" format, dropping zero components
|
||||
//
|
||||
// - Invariants:
|
||||
// - distanceMiles is always positive when distanceMeters is positive
|
||||
// - durationHours is always positive when durationSeconds is positive
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
// Trip.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Domain model for a planned sports trip.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - itineraryDays() returns one ItineraryDay per calendar day from first arrival to last activity
|
||||
// - Last activity day is departure - 1 (departure is when you leave)
|
||||
// - tripDuration is max(1, days between first arrival and last departure + 1)
|
||||
// - cities returns deduplicated city list preserving visit order
|
||||
// - displayName uses " → " separator between cities
|
||||
//
|
||||
// - Invariants:
|
||||
// - tripDuration >= 1 (minimum 1 day)
|
||||
// - cities has no duplicates
|
||||
// - itineraryDays() dayNumber starts at 1 and increments
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
// TripPoll.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Domain models for group trip polling and voting.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - generateShareCode() returns 6 uppercase alphanumeric characters (no O/I/L/0/1)
|
||||
// - computeTripHash() produces deterministic hash from stops, games, and dates
|
||||
// - PollVote.calculateScores uses Borda count: points = tripCount - rank
|
||||
// - PollResults.tripScores sums all votes and sorts descending by score
|
||||
// - scorePercentage returns score/maxScore, 0 when maxScore is 0
|
||||
//
|
||||
// - Invariants:
|
||||
// - shareCode is exactly 6 characters
|
||||
// - tripVersions.count == tripSnapshots.count
|
||||
// - Borda points range from 1 (last place) to tripCount (first place)
|
||||
// - tripScores contains all trip indices
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
// TripPreferences.swift
|
||||
// SportsTime
|
||||
//
|
||||
// User preferences for trip planning.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - totalDriverHoursPerDay = (maxDrivingHoursPerDriver ?? 8.0) * numberOfDrivers
|
||||
// - effectiveTripDuration uses tripDuration if set, otherwise calculates from date range
|
||||
// - effectiveTripDuration is at least 1 day
|
||||
// - LeisureLevel.restDaysPerWeek: packed=0.5, moderate=1.5, relaxed=2.5
|
||||
// - LeisureLevel.maxGamesPerWeek: packed=7, moderate=5, relaxed=3
|
||||
// - RoutePreference.scenicWeight: direct=0.0, scenic=1.0, balanced=0.5
|
||||
//
|
||||
// - Invariants:
|
||||
// - totalDriverHoursPerDay > 0 (numberOfDrivers >= 1)
|
||||
// - effectiveTripDuration >= 1
|
||||
// - LocationInput.isResolved == (coordinate != nil)
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
// TripStop.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Domain model for a stop on a trip itinerary.
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - stayDuration returns days between arrival and departure (minimum 1)
|
||||
// - Same-day arrival and departure returns 1
|
||||
// - formattedDateRange shows single date for 1-day stay, range for multi-day
|
||||
//
|
||||
// - Invariants:
|
||||
// - stayDuration >= 1 (never zero or negative)
|
||||
// - hasGames == !games.isEmpty
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
Reference in New Issue
Block a user