// // TripWizardFlowTests.swift // SportsTimeUITests // // Tests the trip planning wizard: planning modes, calendar navigation, // sport/region selection, and planning engine results. // QA Sheet: F-018 through F-042, F-047; also F-030, F-031, F-032 // import XCTest final class TripWizardFlowTests: BaseUITestCase { // MARK: - Helpers /// Opens wizard and returns screen objects ready for interaction. @MainActor private func openWizard() -> (home: HomeScreen, wizard: TripWizardScreen) { let home = HomeScreen(app: app) home.waitForLoad() home.tapStartPlanning() let wizard = TripWizardScreen(app: app) wizard.waitForLoad() return (home, wizard) } // MARK: - Date Range Mode (F-018) /// F-018: Full flow — Start Planning → Date Range → Select dates → MLB → Central → Plan. @MainActor func testF018_DateRangeTripPlanningFlow() { let (_, options) = TestFlows.planDateRangeTrip(app: app) options.assertHasResults() captureScreenshot(named: "F018-PlanningResults") } // MARK: - Planning Mode Selection (F-019 through F-022) /// F-019: "By Games" mode button is available and selectable. @MainActor func testF019_ByGamesModeSelectable() { let (_, wizard) = openWizard() wizard.selectPlanningMode("gameFirst") wizard.assertPlanningModeAvailable("gameFirst") captureScreenshot(named: "F019-ByGamesMode") } /// F-020: "By Route" mode button is available and selectable. @MainActor func testF020_ByRouteModeSelectable() { let (_, wizard) = openWizard() wizard.selectPlanningMode("locations") wizard.assertPlanningModeAvailable("locations") captureScreenshot(named: "F020-ByRouteMode") } /// F-021: "Follow Team" mode button is available and selectable. @MainActor func testF021_FollowTeamModeSelectable() { let (_, wizard) = openWizard() wizard.selectPlanningMode("followTeam") wizard.assertPlanningModeAvailable("followTeam") captureScreenshot(named: "F021-FollowTeamMode") } /// F-022: "By Teams" mode button is available and selectable. @MainActor func testF022_ByTeamsModeSelectable() { let (_, wizard) = openWizard() wizard.selectPlanningMode("teamFirst") wizard.assertPlanningModeAvailable("teamFirst") captureScreenshot(named: "F022-ByTeamsMode") } // MARK: - Calendar Navigation (F-024, F-025) /// F-024: Calendar forward navigation — month label updates correctly. @MainActor func testF024_CalendarNavigationForward() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() // Capture the initial month label wizard.monthLabel.scrollIntoView(in: app.scrollViews.firstMatch) let initialMonth = wizard.monthLabel.label // Navigate forward 3 times for _ in 0..<3 { wizard.nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch) wizard.nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap() } // Month label should have changed XCTAssertNotEqual(wizard.monthLabel.label, initialMonth, "Month label should update after navigating forward") captureScreenshot(named: "F024-CalendarForward") } /// F-025: Calendar backward navigation — can go back after going forward. @MainActor func testF025_CalendarNavigationBackward() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() wizard.monthLabel.scrollIntoView(in: app.scrollViews.firstMatch) // Go forward 3 months for _ in 0..<3 { wizard.nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch) wizard.nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap() } let afterForward = wizard.monthLabel.label // Go back 1 month wizard.previousMonthButton.scrollIntoView(in: app.scrollViews.firstMatch) wizard.previousMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap() XCTAssertNotEqual(wizard.monthLabel.label, afterForward, "Month should change after navigating backward") captureScreenshot(named: "F025-CalendarBackward") } // MARK: - Date Range Selection (F-026) /// F-026: Select start and end dates — both buttons respond to tap. @MainActor func testF026_DateRangeSelection() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() // Navigate to June 2026 wizard.selectDateRange( targetMonth: "June", targetYear: "2026", startDay: "2026-06-11", endDay: "2026-06-16" ) // Verify month label shows June XCTAssertTrue(wizard.monthLabel.label.contains("June"), "Calendar should show June after navigation") captureScreenshot(named: "F026-DateRangeSelected") } // MARK: - Date Edge Cases (F-030, F-031, F-032) /// F-030: Tapping end date before start date — selection auto-corrects. @MainActor func testF030_DateRangeEndBeforeStart() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() // Navigate to a future month wizard.selectDateRange( targetMonth: "July", targetYear: "2026", startDay: "2026-07-15", endDay: "2026-07-10" ) // The picker auto-corrects: if second tap is before first, // it swaps them so start <= end. No crash or error. captureScreenshot(named: "F030-EndBeforeStart") } /// F-031: Tapping the same date twice selects a single-day trip. @MainActor func testF031_DateRangeSameDay() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() // Navigate to July 2026 and tap the same day twice wizard.monthLabel.scrollIntoView(in: app.scrollViews.firstMatch) let targetMonthYear = "July 2026" var attempts = 0 while attempts < 24 && !wizard.monthLabel.label.contains(targetMonthYear) { wizard.nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch) wizard.nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap() attempts += 1 } let dayBtn = wizard.dayButton("2026-07-20") dayBtn.scrollIntoView(in: app.scrollViews.firstMatch) if dayBtn.exists && dayBtn.isHittable { dayBtn.tap() dayBtn.tap() } // Single-day selection - no crash captureScreenshot(named: "F031-SameDay") } /// F-032: Past dates are disabled and not tappable. @MainActor func testF032_PastDatesDisabled() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() // Navigate to the current month wizard.monthLabel.scrollIntoView(in: app.scrollViews.firstMatch) // Find yesterday's date let calendar = Calendar.current let yesterday = calendar.date(byAdding: .day, value: -1, to: Date())! let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" let yesterdayId = "wizard.dates.day.\(formatter.string(from: yesterday))" let yesterdayBtn = app.buttons[yesterdayId] if yesterdayBtn.exists { XCTAssertFalse(yesterdayBtn.isEnabled, "Yesterday's date should be disabled") } captureScreenshot(named: "F032-PastDatesDisabled") } // MARK: - Sport Selection (F-033, F-034) /// F-030: Single sport selection — MLB highlights. @MainActor func testF030_SingleSportSelection() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() wizard.selectSport("mlb") let mlbButton = wizard.sportButton("mlb") XCTAssertTrue(mlbButton.exists, "MLB sport button should exist after selection") captureScreenshot(named: "F030-SingleSport") } /// F-031: Multiple sport selection — MLB + NBA both highlighted. @MainActor func testF031_MultipleSportSelection() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() wizard.selectSport("mlb") wizard.selectSport("nba") XCTAssertTrue(wizard.sportButton("mlb").exists, "MLB should remain after selecting NBA") XCTAssertTrue(wizard.sportButton("nba").exists, "NBA sport button should exist") captureScreenshot(named: "F031-MultipleSports") } // MARK: - Region Selection (F-033) /// F-033: Region toggle — west, central, east buttons respond to tap. @MainActor func testF033_RegionSelection() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() // Select each region to verify they're tappable let regions = ["west", "central", "east"] for region in regions { let btn = wizard.regionButton(region) btn.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertTrue(btn.isHittable, "\(region) region button should be hittable") } // Tap west to toggle it wizard.selectRegion("west") captureScreenshot(named: "F033-RegionSelection") } // MARK: - Switching Modes Resets (F-023) /// F-023: Switching planning modes resets fields — Plan button becomes disabled. @MainActor func testF023_SwitchingModesResetsFields() { let (_, wizard) = openWizard() // Select date range mode and fill fields wizard.selectDateRangeMode() wizard.selectDateRange( targetMonth: "June", targetYear: "2026", startDay: "2026-06-11", endDay: "2026-06-16" ) wizard.selectSport("mlb") wizard.selectRegion("central") // Switch to a different mode wizard.selectPlanningMode("gameFirst") // Switch back to dateRange wizard.selectPlanningMode("dateRange") // Plan button should be disabled (fields were reset) let planBtn = wizard.planTripButton planBtn.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertFalse(planBtn.isEnabled, "Plan My Trip should be disabled after mode switch resets fields") captureScreenshot(named: "F023-ModeSwitch-Reset") } // MARK: - Plan Button States (F-038) /// F-038: Plan My Trip disabled when required fields are incomplete. @MainActor func testF038_PlanButtonDisabledState() { let (_, wizard) = openWizard() // Select mode but don't fill required fields wizard.selectDateRangeMode() let planBtn = wizard.planTripButton planBtn.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertFalse(planBtn.isEnabled, "Plan My Trip should be disabled without filling required fields") // Missing fields warning should be visible let warning = app.descendants(matching: .any)["wizard.missingFieldsWarning"] warning.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertTrue(warning.exists, "Missing fields warning should appear") captureScreenshot(named: "F038-PlanButton-Disabled") } // MARK: - Planning Error (F-040) /// F-040: Planning with no games in date range shows error alert. @MainActor func testF040_NoGamesFoundError() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() // Select sport BEFORE dates so the async sport-availability check // (triggered by date selection) doesn't disable the button. wizard.selectSport("mlb") // Pick December 2026 — MLB off-season, no games expected. // Scroll up to the dates section first since sport step is below dates. wizard.monthLabel.scrollIntoView(in: app.scrollViews.firstMatch, direction: .up) wizard.selectDateRange( targetMonth: "December", targetYear: "2026", startDay: "2026-12-01", endDay: "2026-12-07" ) wizard.selectRegion("central") wizard.tapPlanTrip() // Wait for the planning error alert let alert = app.alerts["Planning Error"] XCTAssertTrue(alert.waitForExistence(timeout: BaseUITestCase.longTimeout), "Planning Error alert should appear for off-season dates") // Dismiss the alert alert.buttons["OK"].tap() captureScreenshot(named: "F040-NoGamesFound") } // MARK: - Wizard Dismiss (F-042) /// F-042: Cancel wizard returns to home screen. @MainActor func testF042_WizardCanBeDismissed() { let (home, wizard) = openWizard() wizard.tapCancel() XCTAssertTrue( home.startPlanningButton.waitForExistence(timeout: BaseUITestCase.defaultTimeout), "Should return to Home after cancelling wizard" ) } // MARK: - Plan Button Enabled (F-041) /// F-041: Plan My Trip button becomes enabled after filling all required fields. @MainActor func testF041_PlanButtonEnabledState() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() // Fill all required fields wizard.selectDateRange( targetMonth: "June", targetYear: "2026", startDay: "2026-06-11", endDay: "2026-06-16" ) wizard.selectSport("mlb") wizard.selectRegion("central") // Plan button should now be enabled let planBtn = wizard.planTripButton planBtn.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertTrue(planBtn.isEnabled, "Plan My Trip should be enabled after filling all required fields") captureScreenshot(named: "F041-PlanButton-Enabled") } // MARK: - Wizard Scroll (F-047) /// F-047: All wizard steps are reachable by scrolling. @MainActor func testF047_WizardScrollBehavior() { let (_, wizard) = openWizard() wizard.selectDateRangeMode() // Verify date controls are reachable let monthLabel = wizard.monthLabel monthLabel.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertTrue(monthLabel.exists, "Month label should be reachable by scrolling") // Verify sport buttons are reachable let mlbButton = wizard.sportButton("mlb") mlbButton.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertTrue(mlbButton.exists, "Sport buttons should be reachable by scrolling") // Verify region buttons are reachable let centralButton = wizard.regionButton("central") centralButton.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertTrue(centralButton.exists, "Region buttons should be reachable by scrolling") // Verify Plan My Trip button is reachable at the bottom let planBtn = wizard.planTripButton planBtn.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertTrue(planBtn.exists, "Plan My Trip button should be reachable by scrolling") captureScreenshot(named: "F047-WizardScroll") } // MARK: - All 5 Modes Available (F-018 to F-022 combined) /// Verifies all 5 planning mode buttons are present in the wizard. @MainActor func testF018_F022_AllPlanningModesAvailable() { let (_, wizard) = openWizard() let modes = ["dateRange", "gameFirst", "locations", "followTeam", "teamFirst"] for mode in modes { wizard.assertPlanningModeAvailable(mode) } captureScreenshot(named: "F018-F022-AllModes") } }