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>
187 lines
5.6 KiB
Swift
187 lines
5.6 KiB
Swift
//
|
|
// TripWizardViewModelTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
|
|
import XCTest
|
|
@testable import SportsTime
|
|
|
|
final class TripWizardViewModelTests: XCTestCase {
|
|
|
|
// MARK: - Visibility Tests
|
|
|
|
func test_initialState_stepsNotVisible() {
|
|
let viewModel = TripWizardViewModel()
|
|
|
|
XCTAssertFalse(viewModel.areStepsVisible)
|
|
}
|
|
|
|
func test_selectingPlanningMode_revealsAllSteps() {
|
|
let viewModel = TripWizardViewModel()
|
|
|
|
viewModel.planningMode = .dateRange
|
|
|
|
XCTAssertTrue(viewModel.areStepsVisible)
|
|
}
|
|
|
|
// MARK: - canPlanTrip Validation Tests
|
|
|
|
func test_canPlanTrip_initiallyFalse() {
|
|
let viewModel = TripWizardViewModel()
|
|
|
|
XCTAssertFalse(viewModel.canPlanTrip)
|
|
}
|
|
|
|
func test_canPlanTrip_requiresAllFields() {
|
|
let viewModel = TripWizardViewModel()
|
|
|
|
// None set
|
|
XCTAssertFalse(viewModel.canPlanTrip)
|
|
|
|
// Only planning mode
|
|
viewModel.planningMode = .dateRange
|
|
XCTAssertFalse(viewModel.canPlanTrip)
|
|
|
|
// Add dates
|
|
viewModel.hasSetDates = true
|
|
XCTAssertFalse(viewModel.canPlanTrip)
|
|
|
|
// Add sports
|
|
viewModel.selectedSports = [.mlb]
|
|
XCTAssertFalse(viewModel.canPlanTrip)
|
|
|
|
// Add regions
|
|
viewModel.selectedRegions = [.east]
|
|
XCTAssertFalse(viewModel.canPlanTrip)
|
|
|
|
// Add route preference
|
|
viewModel.hasSetRoutePreference = true
|
|
XCTAssertFalse(viewModel.canPlanTrip)
|
|
|
|
// Add repeat cities - now all required fields are set
|
|
viewModel.hasSetRepeatCities = true
|
|
XCTAssertTrue(viewModel.canPlanTrip)
|
|
}
|
|
|
|
func test_canPlanTrip_trueWhenAllFieldsSet() {
|
|
let viewModel = TripWizardViewModel()
|
|
viewModel.planningMode = .dateRange
|
|
viewModel.hasSetDates = true
|
|
viewModel.selectedSports = [.mlb, .nba]
|
|
viewModel.selectedRegions = [.east, .central]
|
|
viewModel.hasSetRoutePreference = true
|
|
viewModel.hasSetRepeatCities = true
|
|
|
|
XCTAssertTrue(viewModel.canPlanTrip)
|
|
}
|
|
|
|
func test_canPlanTrip_mustStopsOptional() {
|
|
let viewModel = TripWizardViewModel()
|
|
viewModel.planningMode = .dateRange
|
|
viewModel.hasSetDates = true
|
|
viewModel.selectedSports = [.nba]
|
|
viewModel.selectedRegions = [.west]
|
|
viewModel.hasSetRoutePreference = true
|
|
viewModel.hasSetRepeatCities = true
|
|
|
|
// canPlanTrip should be true even without must stops
|
|
XCTAssertTrue(viewModel.canPlanTrip)
|
|
XCTAssertTrue(viewModel.mustStopLocations.isEmpty)
|
|
}
|
|
|
|
// MARK: - Reset Behavior Tests
|
|
|
|
func test_changingPlanningMode_resetsAllSelections() {
|
|
let viewModel = TripWizardViewModel()
|
|
|
|
// Set up full wizard state
|
|
viewModel.planningMode = .dateRange
|
|
viewModel.selectedSports = [.mlb, .nba]
|
|
viewModel.hasSetDates = true
|
|
viewModel.selectedRegions = [.east, .central]
|
|
viewModel.hasSetRoutePreference = true
|
|
viewModel.hasSetRepeatCities = true
|
|
viewModel.mustStopLocations = [LocationInput(name: "Test", coordinate: nil)]
|
|
|
|
// Change planning mode
|
|
viewModel.planningMode = .locations
|
|
|
|
// Verify all state is reset
|
|
XCTAssertTrue(viewModel.selectedSports.isEmpty)
|
|
XCTAssertFalse(viewModel.hasSetDates)
|
|
XCTAssertTrue(viewModel.selectedRegions.isEmpty)
|
|
XCTAssertFalse(viewModel.hasSetRoutePreference)
|
|
XCTAssertFalse(viewModel.hasSetRepeatCities)
|
|
XCTAssertTrue(viewModel.mustStopLocations.isEmpty)
|
|
}
|
|
|
|
func test_settingSamePlanningMode_doesNotResetState() {
|
|
let viewModel = TripWizardViewModel()
|
|
viewModel.planningMode = .dateRange
|
|
viewModel.selectedSports = [.mlb]
|
|
viewModel.hasSetDates = true
|
|
|
|
// Re-set the same mode
|
|
viewModel.planningMode = .dateRange
|
|
|
|
// State should NOT be reset
|
|
XCTAssertEqual(viewModel.selectedSports, [.mlb])
|
|
XCTAssertTrue(viewModel.hasSetDates)
|
|
}
|
|
|
|
// MARK: - Sport Availability Tests
|
|
|
|
func test_canSelectSport_defaultsToTrue() {
|
|
let viewModel = TripWizardViewModel()
|
|
|
|
XCTAssertTrue(viewModel.canSelectSport(.mlb))
|
|
XCTAssertTrue(viewModel.canSelectSport(.nba))
|
|
XCTAssertTrue(viewModel.canSelectSport(.nhl))
|
|
}
|
|
|
|
func test_canSelectSport_respectsAvailability() {
|
|
let viewModel = TripWizardViewModel()
|
|
viewModel.sportAvailability = [.mlb: true, .nba: false, .nhl: true]
|
|
|
|
XCTAssertTrue(viewModel.canSelectSport(.mlb))
|
|
XCTAssertFalse(viewModel.canSelectSport(.nba))
|
|
XCTAssertTrue(viewModel.canSelectSport(.nhl))
|
|
}
|
|
|
|
// MARK: - Multi-Selection Tests
|
|
|
|
func test_multipleSports_canBeSelected() {
|
|
let viewModel = TripWizardViewModel()
|
|
|
|
viewModel.selectedSports = [.mlb, .nba, .nhl]
|
|
|
|
XCTAssertEqual(viewModel.selectedSports.count, 3)
|
|
}
|
|
|
|
func test_multipleRegions_canBeSelected() {
|
|
let viewModel = TripWizardViewModel()
|
|
|
|
viewModel.selectedRegions = [.east, .central, .west]
|
|
|
|
XCTAssertEqual(viewModel.selectedRegions.count, 3)
|
|
}
|
|
|
|
// MARK: - Planning State Tests
|
|
|
|
func test_isPlanning_defaultsToFalse() {
|
|
let viewModel = TripWizardViewModel()
|
|
|
|
XCTAssertFalse(viewModel.isPlanning)
|
|
}
|
|
|
|
func test_mustStopLocations_canBeAdded() {
|
|
let viewModel = TripWizardViewModel()
|
|
let location = LocationInput(name: "Chicago, IL", coordinate: nil)
|
|
|
|
viewModel.mustStopLocations.append(location)
|
|
|
|
XCTAssertEqual(viewModel.mustStopLocations.count, 1)
|
|
XCTAssertEqual(viewModel.mustStopLocations.first?.name, "Chicago, IL")
|
|
}
|
|
}
|