diff --git a/SportsTimeUITests/Framework/Screens.swift b/SportsTimeUITests/Framework/Screens.swift index c071ccb..9e820d1 100644 --- a/SportsTimeUITests/Framework/Screens.swift +++ b/SportsTimeUITests/Framework/Screens.swift @@ -160,7 +160,8 @@ struct TripWizardScreen { /// Selects a planning mode by raw value (e.g., "dateRange", "gameFirst", "locations", "followTeam", "teamFirst"). func selectPlanningMode(_ mode: String) { let btn = planningModeButton(mode) - btn.scrollIntoView(in: app.scrollViews.firstMatch) + // Planning modes are at the top of the wizard — scroll up to find them + btn.scrollIntoView(in: app.scrollViews.firstMatch, direction: .up) btn.tap() } diff --git a/SportsTimeUITests/SportsTimeUITests.swift b/SportsTimeUITests/SportsTimeUITests.swift index 5ea277d..5352d1b 100644 --- a/SportsTimeUITests/SportsTimeUITests.swift +++ b/SportsTimeUITests/SportsTimeUITests.swift @@ -25,6 +25,9 @@ final class SportsTimeUITests: XCTestCase { func testAccessibilitySmoke_LargeDynamicTypeEntryFlow() throws { let app = XCUIApplication() app.launchArguments = [ + "--ui-testing", + "--disable-animations", + "--reset-state", "-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityXXXL" ] @@ -32,11 +35,14 @@ final class SportsTimeUITests: XCTestCase { let startPlanningButton = app.buttons["home.startPlanningButton"] XCTAssertTrue(startPlanningButton.waitForExistence(timeout: 20), "Start Planning should exist at large Dynamic Type") + // At XXXL the button may be pushed below the fold — scroll into view + startPlanningButton.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertTrue(startPlanningButton.isHittable, "Start Planning should remain hittable at large Dynamic Type") startPlanningButton.tap() let dateRangeMode = app.buttons["wizard.planningMode.dateRange"] XCTAssertTrue(dateRangeMode.waitForExistence(timeout: 10), "Planning mode options should load") + dateRangeMode.scrollIntoView(in: app.scrollViews.firstMatch) XCTAssertTrue(dateRangeMode.isHittable, "Planning mode option should remain hittable at large Dynamic Type") } @@ -61,99 +67,28 @@ final class SportsTimeUITests: XCTestCase { @MainActor func testTripPlanningDemoFlow() throws { let app = XCUIApplication() - app.launchArguments = ["-DemoMode"] + app.launchArguments = [ + "--ui-testing", + "--disable-animations", + "--reset-state" + ] app.launch() - // Wait for app to fully load - sleep(2) - // MARK: Step 1 - Tap "Start Planning" let startPlanningButton = app.buttons["home.startPlanningButton"] XCTAssertTrue(startPlanningButton.waitForExistence(timeout: 10), "Start Planning button should exist") startPlanningButton.tap() - // Wait for demo mode to auto-select planning mode - sleep(2) - - // MARK: Step 2 - Continuous scroll through wizard - // Demo mode auto-selects: Date Range → June 11-16 → MLB → Central - // Each step auto-selects 0.5s after appearing, so we scroll slowly - for _ in 1...8 { - slowSwipeUp(app: app) - sleep(2) // Give time for auto-selections - } - - // MARK: Step 3 - Click "Plan My Trip" - let planTripButton = app.buttons["wizard.planTripButton"] - XCTAssertTrue(planTripButton.waitForExistence(timeout: 5), "Plan My Trip button should exist") - planTripButton.tap() - - // Wait for planning to complete - sleep(6) - - // MARK: Step 4 - Demo mode auto-selects "Most Games" and navigates to 4th trip - // Wait for TripOptionsView to load and auto-selections to complete - let sortDropdown = app.buttons["tripOptions.sortDropdown"] - XCTAssertTrue(sortDropdown.waitForExistence(timeout: 15), "Sort dropdown should exist") - - // Wait for demo mode to auto-select sort and navigate to trip detail - sleep(3) - - // MARK: Step 5 - Scroll through trip detail (demo mode auto-favorites) - // Wait for TripDetailView to appear - let favoriteButton = app.buttons["tripDetail.favoriteButton"] - if favoriteButton.waitForExistence(timeout: 10) { - // Demo mode will auto-favorite, but we scroll to show the itinerary - for _ in 1...6 { - slowSwipeUp(app: app) - sleep(2) - } - - // Scroll back up to show the favorited state - for _ in 1...4 { - slowSwipeDown(app: app) - sleep(1) - } - } - - // Wait to display final state - sleep(3) - - // Test complete - demo flow finished with trip favorited - } - - // MARK: - Manual Demo Flow Test (Original) - - /// Original manual test flow for comparison or when demo mode is not desired - @MainActor - func testTripPlanningManualFlow() throws { - let app = XCUIApplication() - app.launch() - - // Wait for app to fully load - sleep(2) - - // MARK: Step 1 - Tap "Start Planning" - let startPlanningButton = app.buttons["home.startPlanningButton"] - XCTAssertTrue(startPlanningButton.waitForExistence(timeout: 10), "Start Planning button should exist") - startPlanningButton.tap() - sleep(1) - - // MARK: Step 2 - Choose "By Dates" mode + // MARK: Step 2 - Fill wizard steps + // Note: -DemoMode removed because demo auto-selections conflict with manual + // taps (toggling sports/regions off). This test verifies the full flow manually. let dateRangeMode = app.buttons["wizard.planningMode.dateRange"] - XCTAssertTrue(dateRangeMode.waitForExistence(timeout: 5), "Date Range mode should exist") + XCTAssertTrue(dateRangeMode.waitForExistence(timeout: 10), "Date Range mode should exist") dateRangeMode.tap() - sleep(1) - // Scroll down to see dates step - app.swipeUp() - sleep(1) - - // MARK: Step 3 - Select June 11-16, 2026 // Navigate to June 2026 let nextMonthButton = app.buttons["wizard.dates.nextMonth"] - XCTAssertTrue(nextMonthButton.waitForExistence(timeout: 5), "Next month button should exist") - + nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch) let monthLabel = app.staticTexts["wizard.dates.monthLabel"] var attempts = 0 while !monthLabel.label.contains("June 2026") && attempts < 12 { @@ -162,57 +97,38 @@ final class SportsTimeUITests: XCTestCase { attempts += 1 } - // Select June 11 + // Select June 11-16 let june11 = app.buttons["wizard.dates.day.2026-06-11"] - XCTAssertTrue(june11.waitForExistence(timeout: 5), "June 11 should exist") + june11.scrollIntoView(in: app.scrollViews.firstMatch) june11.tap() Thread.sleep(forTimeInterval: 0.5) - - // Select June 16 let june16 = app.buttons["wizard.dates.day.2026-06-16"] - XCTAssertTrue(june16.waitForExistence(timeout: 5), "June 16 should exist") + june16.scrollIntoView(in: app.scrollViews.firstMatch) june16.tap() - sleep(1) + Thread.sleep(forTimeInterval: 0.5) - // Scroll down to see sports step - app.swipeUp(velocity: .slow) - sleep(1) - - // MARK: Step 4 - Pick MLB + // Select MLB let mlbButton = app.buttons["wizard.sports.mlb"] - XCTAssertTrue(mlbButton.waitForExistence(timeout: 5), "MLB button should exist") + mlbButton.scrollIntoView(in: app.scrollViews.firstMatch) mlbButton.tap() - sleep(1) - // Scroll down to see regions step - app.swipeUp(velocity: .slow) - sleep(1) - - // MARK: Step 5 - Select Central US region + // Select Central region let centralRegion = app.buttons["wizard.regions.central"] - XCTAssertTrue(centralRegion.waitForExistence(timeout: 5), "Central region should exist") + centralRegion.scrollIntoView(in: app.scrollViews.firstMatch) centralRegion.tap() - sleep(1) - // Scroll down to see remaining steps - app.swipeUp(velocity: .slow) - sleep(1) - - // Keep scrolling for defaults - app.swipeUp(velocity: .slow) - sleep(1) - - // MARK: Step 8 - Click "Plan My Trip" + // MARK: Step 3 - Plan trip let planTripButton = app.buttons["wizard.planTripButton"] - XCTAssertTrue(planTripButton.waitForExistence(timeout: 5), "Plan My Trip button should exist") + planTripButton.scrollIntoView(in: app.scrollViews.firstMatch) + let enabledPred = NSPredicate(format: "isEnabled == true") + let enabledExp = XCTNSPredicateExpectation(predicate: enabledPred, object: planTripButton) + let waitResult = XCTWaiter.wait(for: [enabledExp], timeout: 10) + XCTAssertEqual(waitResult, .completed, "Plan My Trip should become enabled") planTripButton.tap() - // Wait for planning to complete - sleep(5) - - // MARK: Step 9 - Select "Most Games" from dropdown + // MARK: Step 4 - Wait for planning results let sortDropdown = app.buttons["tripOptions.sortDropdown"] - XCTAssertTrue(sortDropdown.waitForExistence(timeout: 15), "Sort dropdown should exist") + XCTAssertTrue(sortDropdown.waitForExistence(timeout: 30), "Sort dropdown should exist") sortDropdown.tap() Thread.sleep(forTimeInterval: 0.5) @@ -224,42 +140,122 @@ final class SportsTimeUITests: XCTestCase { } Thread.sleep(forTimeInterval: 1) - // MARK: Step 10 - Scroll and select 4th trip - for _ in 1...3 { - slowSwipeUp(app: app) - sleep(1) - } + // MARK: Step 5 - Select a trip and navigate to detail + let anyTrip = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'tripOptions.trip.'")).firstMatch + XCTAssertTrue(anyTrip.waitForExistence(timeout: 10), "At least one trip option should exist") + anyTrip.tap() + Thread.sleep(forTimeInterval: 2) - let fourthTrip = app.buttons["tripOptions.trip.3"] - if fourthTrip.waitForExistence(timeout: 5) { - fourthTrip.tap() - } else { - slowSwipeUp(app: app) - sleep(1) - if fourthTrip.waitForExistence(timeout: 3) { - fourthTrip.tap() - } else { - let anyTrip = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'tripOptions.trip.'")).firstMatch - XCTAssertTrue(anyTrip.waitForExistence(timeout: 5), "At least one trip option should exist") - anyTrip.tap() - } - } - sleep(2) - - // MARK: Step 12 - Scroll through itinerary + // MARK: Step 6 - Scroll through itinerary for _ in 1...5 { slowSwipeUp(app: app) Thread.sleep(forTimeInterval: 1.5) } - // MARK: Step 13 - Favorite the trip - for _ in 1...5 { - slowSwipeDown(app: app) - Thread.sleep(forTimeInterval: 0.5) + // MARK: Step 7 - Favorite the trip + let favoriteButton = app.buttons["tripDetail.favoriteButton"] + favoriteButton.scrollIntoView(in: app.scrollViews.firstMatch, direction: .up) + XCTAssertTrue(favoriteButton.exists, "Favorite button should exist") + favoriteButton.tap() + sleep(2) + } + + // MARK: - Manual Demo Flow Test (Original) + + /// Original manual test flow for comparison or when demo mode is not desired + @MainActor + func testTripPlanningManualFlow() throws { + let app = XCUIApplication() + app.launchArguments = [ + "--ui-testing", + "--disable-animations", + "--reset-state" + ] + app.launch() + + // MARK: Step 1 - Tap "Start Planning" + let startPlanningButton = app.buttons["home.startPlanningButton"] + XCTAssertTrue(startPlanningButton.waitForExistence(timeout: 10), "Start Planning button should exist") + startPlanningButton.tap() + + // MARK: Step 2 - Choose "By Dates" mode + let dateRangeMode = app.buttons["wizard.planningMode.dateRange"] + XCTAssertTrue(dateRangeMode.waitForExistence(timeout: 5), "Date Range mode should exist") + dateRangeMode.tap() + + // MARK: Step 3 - Select June 11-16, 2026 + let nextMonthButton = app.buttons["wizard.dates.nextMonth"] + nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch) + + let monthLabel = app.staticTexts["wizard.dates.monthLabel"] + var attempts = 0 + while !monthLabel.label.contains("June 2026") && attempts < 12 { + nextMonthButton.tap() + Thread.sleep(forTimeInterval: 0.3) + attempts += 1 } + let june11 = app.buttons["wizard.dates.day.2026-06-11"] + june11.scrollIntoView(in: app.scrollViews.firstMatch) + june11.tap() + Thread.sleep(forTimeInterval: 0.5) + + let june16 = app.buttons["wizard.dates.day.2026-06-16"] + june16.scrollIntoView(in: app.scrollViews.firstMatch) + june16.tap() + Thread.sleep(forTimeInterval: 0.5) + + // MARK: Step 4 - Pick MLB + let mlbButton = app.buttons["wizard.sports.mlb"] + mlbButton.scrollIntoView(in: app.scrollViews.firstMatch) + mlbButton.tap() + + // MARK: Step 5 - Select Central US region + let centralRegion = app.buttons["wizard.regions.central"] + centralRegion.scrollIntoView(in: app.scrollViews.firstMatch) + centralRegion.tap() + + // MARK: Step 6 - Scroll to Plan button and wait for it to be enabled + // Scrolling reveals RoutePreference and RepeatCities steps whose .onAppear + // auto-set flags required for canPlanTrip to return true. + let planTripButton = app.buttons["wizard.planTripButton"] + planTripButton.scrollIntoView(in: app.scrollViews.firstMatch) + let enabledPred = NSPredicate(format: "isEnabled == true") + let enabledExp = XCTNSPredicateExpectation(predicate: enabledPred, object: planTripButton) + let waitResult = XCTWaiter.wait(for: [enabledExp], timeout: 10) + XCTAssertEqual(waitResult, .completed, "Plan My Trip should become enabled") + planTripButton.tap() + + // MARK: Step 7 - Wait for planning results + let sortDropdown = app.buttons["tripOptions.sortDropdown"] + XCTAssertTrue(sortDropdown.waitForExistence(timeout: 30), "Sort dropdown should exist") + sortDropdown.tap() + Thread.sleep(forTimeInterval: 0.5) + + let mostGamesOption = app.buttons["tripOptions.sortOption.mostgames"] + if mostGamesOption.waitForExistence(timeout: 3) { + mostGamesOption.tap() + } else { + app.buttons["Most Games"].tap() + } + Thread.sleep(forTimeInterval: 1) + + // MARK: Step 8 - Select a trip option + let anyTrip = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'tripOptions.trip.'")).firstMatch + XCTAssertTrue(anyTrip.waitForExistence(timeout: 10), "At least one trip option should exist") + anyTrip.tap() + Thread.sleep(forTimeInterval: 2) + + // MARK: Step 9 - Scroll through itinerary + for _ in 1...5 { + slowSwipeUp(app: app) + Thread.sleep(forTimeInterval: 1.5) + } + + // MARK: Step 10 - Favorite the trip let favoriteButton = app.buttons["tripDetail.favoriteButton"] - XCTAssertTrue(favoriteButton.waitForExistence(timeout: 5), "Favorite button should exist") + favoriteButton.scrollIntoView(in: app.scrollViews.firstMatch, direction: .up) + XCTAssertTrue(favoriteButton.exists, "Favorite button should exist") favoriteButton.tap() sleep(2) } diff --git a/SportsTimeUITests/Tests/TripWizardFlowTests.swift b/SportsTimeUITests/Tests/TripWizardFlowTests.swift index bcb4280..c4132eb 100644 --- a/SportsTimeUITests/Tests/TripWizardFlowTests.swift +++ b/SportsTimeUITests/Tests/TripWizardFlowTests.swift @@ -273,14 +273,20 @@ final class TripWizardFlowTests: BaseUITestCase { let (_, wizard) = openWizard() wizard.selectDateRangeMode() - // Pick December 2026 — MLB off-season, no games expected + // 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.selectSport("mlb") + wizard.selectRegion("central") wizard.tapPlanTrip()