diff --git a/SportsTimeUITests/Framework/BaseUITestCase.swift b/SportsTimeUITests/Framework/BaseUITestCase.swift index c394ab4..689b00e 100644 --- a/SportsTimeUITests/Framework/BaseUITestCase.swift +++ b/SportsTimeUITests/Framework/BaseUITestCase.swift @@ -118,25 +118,40 @@ extension XCUIElement { file: StaticString = #filePath, line: UInt = #line ) -> XCUIElement { - var scrollsRemaining = maxScrolls - while !exists || !isHittable { - guard scrollsRemaining > 0 else { - XCTFail("Could not scroll \(self) into view after \(maxScrolls) scrolls", - file: file, line: line) - return self + if exists && isHittable { return self } + + func attemptScroll(_ scrollDirection: ScrollDirection, attempts: Int) -> Bool { + var remaining = attempts + while (!exists || !isHittable) && remaining > 0 { + switch scrollDirection { + case .down: + scrollView.swipeUp(velocity: .slow) + case .up: + scrollView.swipeDown(velocity: .slow) + } + remaining -= 1 } - switch direction { - case .down: - scrollView.swipeUp(velocity: .slow) - case .up: - scrollView.swipeDown(velocity: .slow) - } - scrollsRemaining -= 1 + return exists && isHittable } + + if attemptScroll(direction, attempts: maxScrolls) { + return self + } + + let reverseDirection: ScrollDirection = direction == .down ? .up : .down + if attemptScroll(reverseDirection, attempts: maxScrolls) { + return self + } + + XCTFail( + "Could not scroll \(self) into view after \(maxScrolls) scrolls in either direction", + file: file, + line: line + ) return self } } -enum ScrollDirection { +enum ScrollDirection: Equatable { case up, down } diff --git a/SportsTimeUITests/Framework/Screens.swift b/SportsTimeUITests/Framework/Screens.swift index 0a55409..3322e78 100644 --- a/SportsTimeUITests/Framework/Screens.swift +++ b/SportsTimeUITests/Framework/Screens.swift @@ -74,7 +74,35 @@ struct HomeScreen { /// Taps "Start Planning" to open the Trip Wizard sheet. func tapStartPlanning() { - startPlanningButton.waitUntilHittable().tap() + let navTitle = app.navigationBars["Plan a Trip"] + let dateRangeMode = app.buttons["wizard.planningMode.dateRange"] + + if navTitle.exists || dateRangeMode.exists { + return + } + + func tapIfVisible(_ element: XCUIElement, timeout: TimeInterval) -> Bool { + guard element.waitForExistence(timeout: timeout), element.isHittable else { return false } + element.tap() + return true + } + + _ = tapIfVisible(startPlanningButton, timeout: BaseUITestCase.defaultTimeout) || + tapIfVisible(createTripToolbarButton, timeout: BaseUITestCase.shortTimeout) + + if navTitle.waitForExistence(timeout: BaseUITestCase.shortTimeout) || + dateRangeMode.waitForExistence(timeout: BaseUITestCase.shortTimeout) { + return + } + + _ = tapIfVisible(createTripToolbarButton, timeout: BaseUITestCase.shortTimeout) || + tapIfVisible(startPlanningButton, timeout: BaseUITestCase.shortTimeout) + + XCTAssertTrue( + navTitle.waitForExistence(timeout: BaseUITestCase.defaultTimeout) || + dateRangeMode.waitForExistence(timeout: BaseUITestCase.defaultTimeout), + "Trip Wizard should appear after tapping start planning" + ) } /// Switches to a tab by tapping its tab bar button. @@ -150,10 +178,21 @@ struct TripWizardScreen { /// Waits for the wizard sheet to appear. @discardableResult func waitForLoad() -> TripWizardScreen { - if navigationTitle.waitForExistence(timeout: BaseUITestCase.defaultTimeout) || - planningModeButton("dateRange").waitForExistence(timeout: BaseUITestCase.defaultTimeout) { + if navigationTitle.waitForExistence(timeout: BaseUITestCase.longTimeout) || + planningModeButton("dateRange").waitForExistence(timeout: BaseUITestCase.longTimeout) { return self } + + // Fallback: if we're still on Home, trigger planning again. + let home = HomeScreen(app: app) + if home.startPlanningButton.exists || home.createTripToolbarButton.exists { + home.tapStartPlanning() + if navigationTitle.waitForExistence(timeout: BaseUITestCase.defaultTimeout) || + planningModeButton("dateRange").waitForExistence(timeout: BaseUITestCase.defaultTimeout) { + return self + } + } + XCTFail("Trip Wizard should appear") return self } @@ -169,6 +208,19 @@ struct TripWizardScreen { /// Selects the "By Dates" planning mode and waits for steps to expand. func selectDateRangeMode() { selectPlanningMode("dateRange") + + if monthLabel.waitForExistence(timeout: BaseUITestCase.shortTimeout) || + nextMonthButton.waitForExistence(timeout: BaseUITestCase.shortTimeout) { + return + } + + // Retry once for occasional dropped taps under simulator load. + selectPlanningMode("dateRange") + XCTAssertTrue( + monthLabel.waitForExistence(timeout: BaseUITestCase.defaultTimeout) || + nextMonthButton.waitForExistence(timeout: BaseUITestCase.defaultTimeout), + "Date range controls should appear after selecting planning mode" + ) } /// Navigates the calendar to a target month/year and selects start/end dates. @@ -178,6 +230,11 @@ struct TripWizardScreen { startDay: String, endDay: String ) { + // Ensure date controls are rendered before attempting calendar navigation. + if !monthLabel.waitForExistence(timeout: BaseUITestCase.shortTimeout) { + selectDateRangeMode() + } + // First, navigate by month label so tests that assert month visibility stay stable. monthLabel.scrollIntoView(in: app.scrollViews.firstMatch) let targetMonthYear = "\(targetMonth) \(targetYear)" @@ -560,8 +617,31 @@ struct SettingsScreen { // MARK: Assertions func assertLoaded() { - XCTAssertTrue(subscriptionSection.waitForExistence(timeout: BaseUITestCase.defaultTimeout), - "Settings should show Subscription section") + if subscriptionSection.waitForExistence(timeout: BaseUITestCase.defaultTimeout) { + return + } + + let proLabel = app.staticTexts["SportsTime Pro"] + let manageSubscriptionButton = app.buttons["Manage Subscription"] + if proLabel.exists || manageSubscriptionButton.exists || + upgradeProButton.exists || restorePurchasesButton.exists { + return + } + + // Retry tab switch once when the first tap doesn't switch tabs under load. + let settingsTab = app.tabBars.buttons["Settings"] + if settingsTab.waitForExistence(timeout: BaseUITestCase.shortTimeout), settingsTab.isHittable { + settingsTab.tap() + } + + XCTAssertTrue( + subscriptionSection.waitForExistence(timeout: BaseUITestCase.defaultTimeout) || + proLabel.waitForExistence(timeout: BaseUITestCase.shortTimeout) || + manageSubscriptionButton.waitForExistence(timeout: BaseUITestCase.shortTimeout) || + upgradeProButton.waitForExistence(timeout: BaseUITestCase.shortTimeout) || + restorePurchasesButton.waitForExistence(timeout: BaseUITestCase.shortTimeout), + "Settings should show Subscription section" + ) } func assertVersionDisplayed() {