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>
204 lines
7.6 KiB
Swift
204 lines
7.6 KiB
Swift
//
|
|
// 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)
|
|
}
|
|
}
|
|
}
|