Files
Sportstime/SportsTimeTests/Export/POISearchServiceTests.swift
Trey T a6f538dfed Audit and fix 52 test correctness issues across 22 files
Systematic audit of 1,191 tests found tests written to pass rather than
verify correctness. Key fixes:

Infrastructure:
- TestClock: fixed timezone from .current to America/New_York (deterministic)
- TestFixtures: added 1.3x road routing factor to match production
- ItineraryTestHelpers: real per-city coordinates instead of hardcoded (40,-80)

Planning tests:
- Added missing Scenario E factory dispatch tests
- Tightened 12 loose assertions (>= 1 → == 8.0, > 0 → range checks)
- Fixed 4 no-op tests that accepted both success and failure
- Fixed wrong repeat-city invariant (was checking same-day, not different-day)
- Fixed tautological assertion in missing-stadium edge case

Services/Domain/Export tests:
- Replaced 4 placeholder tests (#expect(true)) with real assertions
- Fixed tautological assertions in POISearchServiceTests
- Fixed Chicago coordinate in RegionMapSelectorTests (-89 → -87.6553)
- Added sort order verification to ItineraryRowFlatteningTests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:00:46 -05:00

213 lines
8.3 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,
url: 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 as "0 ft"
@Test("formattedDistance: handles zero distance")
func formattedDistance_zero() {
// 0 meters = 0 feet, and 0 miles < 0.1 so it uses feet format
// String(format: "%.0f ft", 0 * 3.28084) == "0 ft"
let poi = makePOI(distanceMeters: 0)
let formatted = poi.formattedDistance
#expect(formatted == "0 ft", "Zero distance should format as '0 ft', got '\(formatted)'")
}
/// - Expected Behavior: Large distance formats correctly as miles
@Test("formattedDistance: handles large distance")
func formattedDistance_large() {
// 5000 meters * 0.000621371 = 3.106855 miles "3.1 mi"
let poi = makePOI(distanceMeters: 5000)
let formatted = poi.formattedDistance
#expect(formatted == "3.1 mi", "5000m should format as '3.1 mi', got '\(formatted)'")
}
// 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.bar.displayName == "Bar")
#expect(POISearchService.POICategory.coffee.displayName == "Coffee")
#expect(POISearchService.POICategory.hotel.displayName == "Hotel")
#expect(POISearchService.POICategory.parking.displayName == "Parking")
#expect(POISearchService.POICategory.attraction.displayName == "Attraction")
#expect(POISearchService.POICategory.entertainment.displayName == "Entertainment")
}
// 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.bar.iconName == "wineglass.fill")
#expect(POISearchService.POICategory.coffee.iconName == "cup.and.saucer.fill")
#expect(POISearchService.POICategory.hotel.iconName == "bed.double.fill")
#expect(POISearchService.POICategory.parking.iconName == "car.fill")
#expect(POISearchService.POICategory.attraction.iconName == "star.fill")
#expect(POISearchService.POICategory.entertainment.iconName == "theatermasks.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.bar.searchQuery == "bars")
#expect(POISearchService.POICategory.coffee.searchQuery == "coffee shops")
#expect(POISearchService.POICategory.hotel.searchQuery == "hotels")
#expect(POISearchService.POICategory.parking.searchQuery == "parking")
#expect(POISearchService.POICategory.attraction.searchQuery == "tourist attractions")
#expect(POISearchService.POICategory.entertainment.searchQuery == "entertainment")
}
// 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 == 7)
#expect(POISearchService.POICategory.allCases.contains(.restaurant))
#expect(POISearchService.POICategory.allCases.contains(.bar))
#expect(POISearchService.POICategory.allCases.contains(.coffee))
#expect(POISearchService.POICategory.allCases.contains(.hotel))
#expect(POISearchService.POICategory.allCases.contains(.parking))
#expect(POISearchService.POICategory.allCases.contains(.attraction))
#expect(POISearchService.POICategory.allCases.contains(.entertainment))
}
}
// 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)
}
}
}