495 lines
17 KiB
Swift
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")
|
|
}
|
|
}
|