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:
67
SportsTimeTests/Export/MapSnapshotServiceTests.swift
Normal file
67
SportsTimeTests/Export/MapSnapshotServiceTests.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
185
SportsTimeTests/Export/PDFGeneratorTests.swift
Normal file
185
SportsTimeTests/Export/PDFGeneratorTests.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
203
SportsTimeTests/Export/POISearchServiceTests.swift
Normal file
203
SportsTimeTests/Export/POISearchServiceTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
274
SportsTimeTests/Export/ShareableContentTests.swift
Normal file
274
SportsTimeTests/Export/ShareableContentTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user