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:
Trey t
2026-01-16 14:07:41 -06:00
parent 035dd6f5de
commit 8162b4a029
102 changed files with 13409 additions and 9883 deletions

View File

@@ -0,0 +1,67 @@
//
// MapSnapshotServiceTests.swift
// SportsTimeTests
//
// TDD specification tests for MapSnapshotService types.
//
import Testing
import Foundation
@testable import SportsTime
// MARK: - MapSnapshotError Tests
@Suite("MapSnapshotError")
struct MapSnapshotErrorTests {
// MARK: - Specification Tests: errorDescription
/// - Expected Behavior: noStops explains no stops provided
@Test("errorDescription: noStops mentions stops")
func errorDescription_noStops() {
let error = MapSnapshotService.MapSnapshotError.noStops
#expect(error.errorDescription != nil)
#expect(error.errorDescription!.lowercased().contains("stop") || error.errorDescription!.lowercased().contains("no"))
}
/// - Expected Behavior: snapshotFailed includes the reason
@Test("errorDescription: snapshotFailed includes reason")
func errorDescription_snapshotFailed() {
let error = MapSnapshotService.MapSnapshotError.snapshotFailed("Test reason")
#expect(error.errorDescription != nil)
#expect(error.errorDescription!.contains("Test reason") || error.errorDescription!.lowercased().contains("snapshot"))
}
/// - Expected Behavior: invalidCoordinates explains the issue
@Test("errorDescription: invalidCoordinates mentions coordinates")
func errorDescription_invalidCoordinates() {
let error = MapSnapshotService.MapSnapshotError.invalidCoordinates
#expect(error.errorDescription != nil)
#expect(error.errorDescription!.lowercased().contains("coordinate") || error.errorDescription!.lowercased().contains("invalid"))
}
// MARK: - Invariant Tests
/// - Invariant: All errors have non-empty descriptions
@Test("Invariant: all errors have descriptions")
func invariant_allHaveDescriptions() {
let errors: [MapSnapshotService.MapSnapshotError] = [
.noStops,
.snapshotFailed("test"),
.invalidCoordinates
]
for error in errors {
#expect(error.errorDescription != nil)
#expect(!error.errorDescription!.isEmpty)
}
}
/// - Invariant: snapshotFailed preserves the reason message
@Test("Invariant: snapshotFailed preserves reason")
func invariant_snapshotFailedPreservesReason() {
let testReason = "Network timeout 12345"
let error = MapSnapshotService.MapSnapshotError.snapshotFailed(testReason)
#expect(error.errorDescription!.contains(testReason))
}
}

View File

@@ -0,0 +1,185 @@
//
// PDFGeneratorTests.swift
// SportsTimeTests
//
// TDD specification tests for PDFGenerator types.
//
import Testing
import Foundation
import UIKit
@testable import SportsTime
// MARK: - UIColor Hex Extension Tests
@Suite("UIColor Hex Extension")
struct UIColorHexExtensionTests {
// MARK: - Specification Tests: Parsing
/// - Expected Behavior: Parses 6-digit hex without #
@Test("init(hex:): parses 6-digit hex without #")
func initHex_sixDigitWithoutHash() {
let color = UIColor(hex: "FF0000")
#expect(color != nil)
}
/// - Expected Behavior: Parses 6-digit hex with #
@Test("init(hex:): parses 6-digit hex with #")
func initHex_sixDigitWithHash() {
let color = UIColor(hex: "#FF0000")
#expect(color != nil)
}
/// - Expected Behavior: Returns nil for invalid length
@Test("init(hex:): returns nil for invalid length")
func initHex_invalidLength() {
let tooShort = UIColor(hex: "FF00")
let tooLong = UIColor(hex: "FF00FF00")
#expect(tooShort == nil)
#expect(tooLong == nil)
}
/// - Expected Behavior: Handles whitespace
@Test("init(hex:): handles whitespace")
func initHex_whitespace() {
let color = UIColor(hex: " FF0000 ")
#expect(color != nil)
}
// MARK: - Specification Tests: Color Values
/// - Expected Behavior: Red hex produces red color
@Test("init(hex:): FF0000 produces red")
func initHex_redColor() {
let color = UIColor(hex: "FF0000")!
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
#expect(abs(r - 1.0) < 0.01)
#expect(abs(g - 0.0) < 0.01)
#expect(abs(b - 0.0) < 0.01)
#expect(abs(a - 1.0) < 0.01)
}
/// - Expected Behavior: Green hex produces green color
@Test("init(hex:): 00FF00 produces green")
func initHex_greenColor() {
let color = UIColor(hex: "00FF00")!
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
#expect(abs(r - 0.0) < 0.01)
#expect(abs(g - 1.0) < 0.01)
#expect(abs(b - 0.0) < 0.01)
}
/// - Expected Behavior: Blue hex produces blue color
@Test("init(hex:): 0000FF produces blue")
func initHex_blueColor() {
let color = UIColor(hex: "0000FF")!
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
#expect(abs(r - 0.0) < 0.01)
#expect(abs(g - 0.0) < 0.01)
#expect(abs(b - 1.0) < 0.01)
}
/// - Expected Behavior: Black hex produces black color
@Test("init(hex:): 000000 produces black")
func initHex_blackColor() {
let color = UIColor(hex: "000000")!
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
#expect(abs(r - 0.0) < 0.01)
#expect(abs(g - 0.0) < 0.01)
#expect(abs(b - 0.0) < 0.01)
}
/// - Expected Behavior: White hex produces white color
@Test("init(hex:): FFFFFF produces white")
func initHex_whiteColor() {
let color = UIColor(hex: "FFFFFF")!
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
#expect(abs(r - 1.0) < 0.01)
#expect(abs(g - 1.0) < 0.01)
#expect(abs(b - 1.0) < 0.01)
}
/// - Expected Behavior: Mixed hex produces correct color
@Test("init(hex:): mixed color produces correct values")
func initHex_mixedColor() {
// 80 = 128 = 0.502 (50%)
let color = UIColor(hex: "804020")!
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
// 0x80 = 128/255 = ~0.502
// 0x40 = 64/255 = ~0.251
// 0x20 = 32/255 = ~0.125
#expect(abs(r - 0.502) < 0.01)
#expect(abs(g - 0.251) < 0.01)
#expect(abs(b - 0.125) < 0.01)
}
// MARK: - Edge Cases
/// - Expected Behavior: Lowercase hex works
@Test("init(hex:): lowercase hex works")
func initHex_lowercase() {
let color = UIColor(hex: "ff0000")
#expect(color != nil)
}
/// - Expected Behavior: Mixed case hex works
@Test("init(hex:): mixed case hex works")
func initHex_mixedCase() {
let color = UIColor(hex: "Ff00fF")
#expect(color != nil)
}
/// - Expected Behavior: Empty string returns nil
@Test("init(hex:): empty string returns nil")
func initHex_emptyString() {
let color = UIColor(hex: "")
#expect(color == nil)
}
/// - Expected Behavior: Just # returns nil
@Test("init(hex:): just # returns nil")
func initHex_justHash() {
let color = UIColor(hex: "#")
#expect(color == nil)
}
// MARK: - Invariant Tests
/// - Invariant: Alpha is always 1.0
@Test("Invariant: alpha is always 1.0")
func invariant_alphaIsOne() {
let testHexes = ["FF0000", "00FF00", "0000FF", "123456", "ABCDEF"]
for hex in testHexes {
let color = UIColor(hex: hex)!
var a: CGFloat = 0
color.getRed(nil, green: nil, blue: nil, alpha: &a)
#expect(abs(a - 1.0) < 0.001)
}
}
/// - Invariant: Valid 6-digit hex always succeeds
@Test("Invariant: valid 6-digit hex always succeeds")
func invariant_validHexSucceeds() {
let validHexes = ["000000", "FFFFFF", "123456", "ABCDEF", "abcdef", "#000000", "#FFFFFF"]
for hex in validHexes {
let color = UIColor(hex: hex)
#expect(color != nil, "Failed for hex: \(hex)")
}
}
}

View File

@@ -0,0 +1,203 @@
//
// 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)
}
}
}

View File

@@ -0,0 +1,274 @@
//
// ShareableContentTests.swift
// SportsTimeTests
//
// TDD specification tests for ShareableContent types.
//
import Testing
import Foundation
import SwiftUI
@testable import SportsTime
// MARK: - ShareCardType Tests
@Suite("ShareCardType")
struct ShareCardTypeTests {
// MARK: - Specification Tests: CaseIterable
/// - Expected Behavior: Includes all expected card types
@Test("allCases: includes all card types")
func allCases_includesAll() {
let allTypes = ShareCardType.allCases
#expect(allTypes.contains(.tripSummary))
#expect(allTypes.contains(.achievementSpotlight))
#expect(allTypes.contains(.achievementCollection))
#expect(allTypes.contains(.achievementMilestone))
#expect(allTypes.contains(.achievementContext))
#expect(allTypes.contains(.stadiumProgress))
}
// MARK: - Invariant Tests
/// - Invariant: Each type has a unique rawValue
@Test("Invariant: unique rawValues")
func invariant_uniqueRawValues() {
let allTypes = ShareCardType.allCases
let rawValues = allTypes.map { $0.rawValue }
let uniqueRawValues = Set(rawValues)
#expect(rawValues.count == uniqueRawValues.count)
}
/// - Invariant: Count matches expected number
@Test("Invariant: correct count")
func invariant_correctCount() {
#expect(ShareCardType.allCases.count == 6)
}
}
// MARK: - ShareTheme Tests
@Suite("ShareTheme")
struct ShareThemeTests {
// MARK: - Specification Tests: Static Themes
/// - Expected Behavior: All preset themes are accessible
@Test("Static themes: all presets exist")
func staticThemes_allExist() {
// Access each theme to ensure they exist
let _ = ShareTheme.dark
let _ = ShareTheme.light
let _ = ShareTheme.midnight
let _ = ShareTheme.forest
let _ = ShareTheme.sunset
let _ = ShareTheme.berry
let _ = ShareTheme.ocean
let _ = ShareTheme.slate
#expect(true) // If we got here, all themes exist
}
/// - Expected Behavior: all array contains all themes
@Test("all: contains all preset themes")
func all_containsAllThemes() {
let all = ShareTheme.all
#expect(all.count == 8)
#expect(all.contains(where: { $0.id == "dark" }))
#expect(all.contains(where: { $0.id == "light" }))
#expect(all.contains(where: { $0.id == "midnight" }))
#expect(all.contains(where: { $0.id == "forest" }))
#expect(all.contains(where: { $0.id == "sunset" }))
#expect(all.contains(where: { $0.id == "berry" }))
#expect(all.contains(where: { $0.id == "ocean" }))
#expect(all.contains(where: { $0.id == "slate" }))
}
// MARK: - Specification Tests: theme(byId:)
/// - Expected Behavior: Returns matching theme by id
@Test("theme(byId:): returns matching theme")
func themeById_returnsMatching() {
let dark = ShareTheme.theme(byId: "dark")
let light = ShareTheme.theme(byId: "light")
#expect(dark.id == "dark")
#expect(light.id == "light")
}
/// - Expected Behavior: Returns dark theme for unknown id
@Test("theme(byId:): returns dark for unknown id")
func themeById_unknownReturnsDark() {
let unknown = ShareTheme.theme(byId: "nonexistent")
#expect(unknown.id == "dark")
}
/// - Expected Behavior: Finds all valid themes by id
@Test("theme(byId:): finds all valid themes")
func themeById_findsAllValid() {
let ids = ["dark", "light", "midnight", "forest", "sunset", "berry", "ocean", "slate"]
for id in ids {
let theme = ShareTheme.theme(byId: id)
#expect(theme.id == id)
}
}
// MARK: - Specification Tests: Properties
/// - Expected Behavior: Each theme has required properties
@Test("Properties: themes have all required fields")
func properties_allRequired() {
for theme in ShareTheme.all {
#expect(!theme.id.isEmpty)
#expect(!theme.name.isEmpty)
#expect(theme.gradientColors.count >= 2)
// Colors exist (can't easily test Color values)
}
}
// MARK: - Invariant Tests
/// - Invariant: All themes have unique ids
@Test("Invariant: unique theme ids")
func invariant_uniqueIds() {
let ids = ShareTheme.all.map { $0.id }
let uniqueIds = Set(ids)
#expect(ids.count == uniqueIds.count)
}
/// - Invariant: All themes are Hashable and Identifiable
@Test("Invariant: themes are Hashable")
func invariant_hashable() {
var themeSet: Set<ShareTheme> = []
for theme in ShareTheme.all {
themeSet.insert(theme)
}
#expect(themeSet.count == ShareTheme.all.count)
}
}
// MARK: - ShareError Tests
@Suite("ShareError")
struct ShareErrorTests {
// MARK: - Specification Tests: errorDescription
/// - Expected Behavior: renderingFailed explains render failure
@Test("errorDescription: renderingFailed mentions render")
func errorDescription_renderingFailed() {
let error = ShareError.renderingFailed
#expect(error.errorDescription != nil)
#expect(error.errorDescription!.lowercased().contains("render") || error.errorDescription!.lowercased().contains("failed"))
}
/// - Expected Behavior: mapSnapshotFailed explains snapshot failure
@Test("errorDescription: mapSnapshotFailed mentions map")
func errorDescription_mapSnapshotFailed() {
let error = ShareError.mapSnapshotFailed
#expect(error.errorDescription != nil)
#expect(error.errorDescription!.lowercased().contains("map") || error.errorDescription!.lowercased().contains("snapshot"))
}
/// - Expected Behavior: instagramNotInstalled explains Instagram requirement
@Test("errorDescription: instagramNotInstalled mentions Instagram")
func errorDescription_instagramNotInstalled() {
let error = ShareError.instagramNotInstalled
#expect(error.errorDescription != nil)
#expect(error.errorDescription!.lowercased().contains("instagram"))
}
// MARK: - Invariant Tests
/// - Invariant: All errors have non-empty descriptions
@Test("Invariant: all errors have descriptions")
func invariant_allHaveDescriptions() {
let errors: [ShareError] = [
.renderingFailed,
.mapSnapshotFailed,
.instagramNotInstalled
]
for error in errors {
#expect(error.errorDescription != nil)
#expect(!error.errorDescription!.isEmpty)
}
}
}
// MARK: - ShareCardDimensions Tests
@Suite("ShareCardDimensions")
struct ShareCardDimensionsTests {
// MARK: - Specification Tests: Static Constants
/// - Expected Behavior: Card size is standard Instagram story size
@Test("cardSize: is 1080x1920")
func cardSize_instagramStory() {
#expect(ShareCardDimensions.cardSize.width == 1080)
#expect(ShareCardDimensions.cardSize.height == 1920)
}
/// - Expected Behavior: Map snapshot size fits within card with padding
@Test("mapSnapshotSize: has reasonable dimensions")
func mapSnapshotSize_reasonable() {
#expect(ShareCardDimensions.mapSnapshotSize.width == 960)
#expect(ShareCardDimensions.mapSnapshotSize.height == 480)
}
/// - Expected Behavior: Route map size fits within card with padding
@Test("routeMapSize: has reasonable dimensions")
func routeMapSize_reasonable() {
#expect(ShareCardDimensions.routeMapSize.width == 960)
#expect(ShareCardDimensions.routeMapSize.height == 576)
}
/// - Expected Behavior: Padding value is positive
@Test("padding: is positive")
func padding_positive() {
#expect(ShareCardDimensions.padding == 60)
#expect(ShareCardDimensions.padding > 0)
}
/// - Expected Behavior: Header height is positive
@Test("headerHeight: is positive")
func headerHeight_positive() {
#expect(ShareCardDimensions.headerHeight == 120)
#expect(ShareCardDimensions.headerHeight > 0)
}
/// - Expected Behavior: Footer height is positive
@Test("footerHeight: is positive")
func footerHeight_positive() {
#expect(ShareCardDimensions.footerHeight == 100)
#expect(ShareCardDimensions.footerHeight > 0)
}
// MARK: - Invariant Tests
/// - Invariant: Card aspect ratio is 9:16 (portrait)
@Test("Invariant: card is portrait aspect ratio")
func invariant_portraitAspectRatio() {
let aspectRatio = ShareCardDimensions.cardSize.width / ShareCardDimensions.cardSize.height
// 9:16 = 0.5625
#expect(abs(aspectRatio - 0.5625) < 0.001)
}
/// - Invariant: Map sizes fit within card with padding
@Test("Invariant: maps fit within card")
func invariant_mapsFitWithinCard() {
let availableWidth = ShareCardDimensions.cardSize.width - (ShareCardDimensions.padding * 2)
#expect(ShareCardDimensions.mapSnapshotSize.width <= availableWidth)
#expect(ShareCardDimensions.routeMapSize.width <= availableWidth)
}
}