Files
Sportstime/SportsTimeUITests/Tests/TripWizardFlowTests.swift
2026-02-20 00:37:34 -06:00

495 lines
17 KiB
Swift

//
// 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: - Review Step (F-040)
/// F-040: Review step shows summary of selections.
@MainActor
func testF040_ReviewStepShowsSummary() {
let (_, wizard) = openWizard()
wizard.selectDateRangeMode()
// Fill required fields
wizard.selectDateRange(
targetMonth: "June",
targetYear: "2026",
startDay: "2026-06-11",
endDay: "2026-06-16"
)
wizard.selectSport("mlb")
wizard.selectRegion("central")
// Scroll to find the review section
let reviewSubtitle = app.staticTexts["Review your selections"]
reviewSubtitle.scrollIntoView(in: app.scrollViews.firstMatch)
XCTAssertTrue(reviewSubtitle.exists,
"Review step subtitle should be visible")
// Mode label should be present in the review
let modeLabel = app.staticTexts["Mode"]
XCTAssertTrue(modeLabel.exists, "Mode label should appear in review step")
captureScreenshot(named: "F040-ReviewStep")
}
// 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")
}
}