Implement freemium model with StoreKit 2: - StoreManager singleton for purchase/restore/entitlements - ProFeature enum defining gated features - PaywallView and OnboardingPaywallView for upsell UI - ProGate view modifier and ProBadge component Feature gating: - Trip saving: 1 free trip, then requires Pro - PDF export: Pro only with badge indicator - Progress tab: Shows ProLockedView for free users - Settings: Subscription management section Also fixes pre-existing test issues with StadiumVisit and ItineraryOption model signature changes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
116 lines
3.9 KiB
Swift
116 lines
3.9 KiB
Swift
import XCTest
|
|
import SwiftData
|
|
@testable import SportsTime
|
|
|
|
@MainActor
|
|
final class GamesHistoryViewModelTests: XCTestCase {
|
|
var modelContainer: ModelContainer!
|
|
var modelContext: ModelContext!
|
|
|
|
override func setUp() async throws {
|
|
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
|
modelContainer = try ModelContainer(
|
|
for: StadiumVisit.self, Achievement.self, UserPreferences.self,
|
|
configurations: config
|
|
)
|
|
modelContext = modelContainer.mainContext
|
|
}
|
|
|
|
override func tearDown() async throws {
|
|
modelContainer = nil
|
|
modelContext = nil
|
|
}
|
|
|
|
func test_GamesHistoryViewModel_GroupsVisitsByYear() async throws {
|
|
// Given: Visits in different years
|
|
let visit2026 = StadiumVisit(
|
|
stadiumId: "stadium-1",
|
|
stadiumNameAtVisit: "Stadium 2026",
|
|
visitDate: Calendar.current.date(from: DateComponents(year: 2026, month: 6, day: 15))!,
|
|
sport: .mlb,
|
|
visitType: .game,
|
|
dataSource: .fullyManual
|
|
)
|
|
|
|
let visit2025 = StadiumVisit(
|
|
stadiumId: "stadium-2",
|
|
stadiumNameAtVisit: "Stadium 2025",
|
|
visitDate: Calendar.current.date(from: DateComponents(year: 2025, month: 6, day: 15))!,
|
|
sport: .mlb,
|
|
visitType: .game,
|
|
dataSource: .fullyManual
|
|
)
|
|
|
|
modelContext.insert(visit2026)
|
|
modelContext.insert(visit2025)
|
|
try modelContext.save()
|
|
|
|
// When: Loading games history
|
|
let viewModel = GamesHistoryViewModel(modelContext: modelContext)
|
|
await viewModel.loadGames()
|
|
|
|
// Then: Visits are grouped by year
|
|
XCTAssertEqual(viewModel.visitsByYear.keys.count, 2, "Should have 2 years")
|
|
XCTAssertTrue(viewModel.visitsByYear.keys.contains(2026))
|
|
XCTAssertTrue(viewModel.visitsByYear.keys.contains(2025))
|
|
}
|
|
|
|
func test_GamesHistoryViewModel_FiltersBySport() async throws {
|
|
// Given: Visits to different sport stadiums
|
|
// Note: This requires stadiums in AppDataProvider to map stadiumId → sport
|
|
let mlbVisit = StadiumVisit(
|
|
stadiumId: "yankee-stadium",
|
|
stadiumNameAtVisit: "Yankee Stadium",
|
|
visitDate: Date(),
|
|
sport: .mlb,
|
|
visitType: .game,
|
|
dataSource: .fullyManual
|
|
)
|
|
|
|
modelContext.insert(mlbVisit)
|
|
try modelContext.save()
|
|
|
|
// When: Filtering by MLB
|
|
let viewModel = GamesHistoryViewModel(modelContext: modelContext)
|
|
await viewModel.loadGames()
|
|
viewModel.selectedSports = [.mlb]
|
|
|
|
// Then: Only MLB visits shown
|
|
let filteredCount = viewModel.filteredVisits.count
|
|
XCTAssertGreaterThanOrEqual(filteredCount, 0, "Filter should work without crashing")
|
|
}
|
|
|
|
func test_GamesHistoryViewModel_SortsMostRecentFirst() async throws {
|
|
// Given: Visits on different dates
|
|
let oldVisit = StadiumVisit(
|
|
stadiumId: "stadium-1",
|
|
stadiumNameAtVisit: "Old Stadium",
|
|
visitDate: Date().addingTimeInterval(-86400 * 30), // 30 days ago
|
|
sport: .mlb,
|
|
visitType: .game,
|
|
dataSource: .fullyManual
|
|
)
|
|
|
|
let newVisit = StadiumVisit(
|
|
stadiumId: "stadium-2",
|
|
stadiumNameAtVisit: "New Stadium",
|
|
visitDate: Date(),
|
|
sport: .mlb,
|
|
visitType: .game,
|
|
dataSource: .fullyManual
|
|
)
|
|
|
|
modelContext.insert(oldVisit)
|
|
modelContext.insert(newVisit)
|
|
try modelContext.save()
|
|
|
|
// When: Loading games
|
|
let viewModel = GamesHistoryViewModel(modelContext: modelContext)
|
|
await viewModel.loadGames()
|
|
|
|
// Then: Most recent first within year
|
|
let visits = viewModel.allVisits
|
|
XCTAssertEqual(visits.first?.stadiumNameAtVisit, "New Stadium", "Most recent should be first")
|
|
}
|
|
}
|