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>
117 lines
3.8 KiB
Swift
117 lines
3.8 KiB
Swift
import XCTest
|
|
import SwiftData
|
|
@testable import SportsTime
|
|
|
|
@MainActor
|
|
final class VisitListTests: 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_VisitsForStadium_ReturnsAllVisitsSortedByDate() async throws {
|
|
// Given: Multiple visits to the same stadium
|
|
let stadiumId = "yankee-stadium"
|
|
|
|
let visit1 = StadiumVisit(
|
|
stadiumId: stadiumId,
|
|
stadiumNameAtVisit: "Yankee Stadium",
|
|
visitDate: Date().addingTimeInterval(-86400 * 30), // 30 days ago
|
|
sport: .mlb,
|
|
visitType: .game,
|
|
dataSource: .fullyManual
|
|
)
|
|
|
|
let visit2 = StadiumVisit(
|
|
stadiumId: stadiumId,
|
|
stadiumNameAtVisit: "Yankee Stadium",
|
|
visitDate: Date().addingTimeInterval(-86400 * 7), // 7 days ago
|
|
sport: .mlb,
|
|
visitType: .game,
|
|
dataSource: .fullyManual
|
|
)
|
|
|
|
let visit3 = StadiumVisit(
|
|
stadiumId: stadiumId,
|
|
stadiumNameAtVisit: "Yankee Stadium",
|
|
visitDate: Date(), // today
|
|
sport: .mlb,
|
|
visitType: .tour,
|
|
dataSource: .fullyManual
|
|
)
|
|
|
|
modelContext.insert(visit1)
|
|
modelContext.insert(visit2)
|
|
modelContext.insert(visit3)
|
|
try modelContext.save()
|
|
|
|
// When: Fetching visits for that stadium
|
|
let descriptor = FetchDescriptor<StadiumVisit>(
|
|
predicate: #Predicate { $0.stadiumId == stadiumId },
|
|
sortBy: [SortDescriptor(\.visitDate, order: .reverse)]
|
|
)
|
|
let visits = try modelContext.fetch(descriptor)
|
|
|
|
// Then: All visits returned, most recent first
|
|
XCTAssertEqual(visits.count, 3, "Should return all 3 visits")
|
|
XCTAssertEqual(visits[0].visitType, .tour, "Most recent visit should be first")
|
|
XCTAssertEqual(visits[2].visitType, .game, "Oldest visit should be last")
|
|
}
|
|
|
|
func test_VisitCountForStadium_ReturnsCorrectCount() async throws {
|
|
// Given: 3 visits to one stadium, 1 to another
|
|
let stadium1 = "yankee-stadium"
|
|
let stadium2 = "fenway-park"
|
|
|
|
for i in 0..<3 {
|
|
let visit = StadiumVisit(
|
|
stadiumId: stadium1,
|
|
stadiumNameAtVisit: "Yankee Stadium",
|
|
visitDate: Date().addingTimeInterval(Double(-i * 86400)),
|
|
sport: .mlb,
|
|
visitType: .game,
|
|
dataSource: .fullyManual
|
|
)
|
|
modelContext.insert(visit)
|
|
}
|
|
|
|
let fenwayVisit = StadiumVisit(
|
|
stadiumId: stadium2,
|
|
stadiumNameAtVisit: "Fenway Park",
|
|
visitDate: Date(),
|
|
sport: .mlb,
|
|
visitType: .game,
|
|
dataSource: .fullyManual
|
|
)
|
|
modelContext.insert(fenwayVisit)
|
|
try modelContext.save()
|
|
|
|
// When: Counting visits per stadium
|
|
let yankeeDescriptor = FetchDescriptor<StadiumVisit>(
|
|
predicate: #Predicate { $0.stadiumId == stadium1 }
|
|
)
|
|
let fenwayDescriptor = FetchDescriptor<StadiumVisit>(
|
|
predicate: #Predicate { $0.stadiumId == stadium2 }
|
|
)
|
|
|
|
let yankeeCount = try modelContext.fetchCount(yankeeDescriptor)
|
|
let fenwayCount = try modelContext.fetchCount(fenwayDescriptor)
|
|
|
|
// Then: Correct counts
|
|
XCTAssertEqual(yankeeCount, 3)
|
|
XCTAssertEqual(fenwayCount, 1)
|
|
}
|
|
}
|