Stabilize flaky UI wizard and settings test flows

This commit is contained in:
treyt
2026-02-18 14:51:04 -06:00
parent 7e54ff2ef2
commit 7eaa21abd4
2 changed files with 114 additions and 19 deletions

View File

@@ -118,25 +118,40 @@ extension XCUIElement {
file: StaticString = #filePath, file: StaticString = #filePath,
line: UInt = #line line: UInt = #line
) -> XCUIElement { ) -> XCUIElement {
var scrollsRemaining = maxScrolls if exists && isHittable { return self }
while !exists || !isHittable {
guard scrollsRemaining > 0 else { func attemptScroll(_ scrollDirection: ScrollDirection, attempts: Int) -> Bool {
XCTFail("Could not scroll \(self) into view after \(maxScrolls) scrolls", var remaining = attempts
file: file, line: line) while (!exists || !isHittable) && remaining > 0 {
return self switch scrollDirection {
case .down:
scrollView.swipeUp(velocity: .slow)
case .up:
scrollView.swipeDown(velocity: .slow)
}
remaining -= 1
} }
switch direction { return exists && isHittable
case .down:
scrollView.swipeUp(velocity: .slow)
case .up:
scrollView.swipeDown(velocity: .slow)
}
scrollsRemaining -= 1
} }
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 return self
} }
} }
enum ScrollDirection { enum ScrollDirection: Equatable {
case up, down case up, down
} }

View File

@@ -74,7 +74,35 @@ struct HomeScreen {
/// Taps "Start Planning" to open the Trip Wizard sheet. /// Taps "Start Planning" to open the Trip Wizard sheet.
func tapStartPlanning() { 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. /// Switches to a tab by tapping its tab bar button.
@@ -150,10 +178,21 @@ struct TripWizardScreen {
/// Waits for the wizard sheet to appear. /// Waits for the wizard sheet to appear.
@discardableResult @discardableResult
func waitForLoad() -> TripWizardScreen { func waitForLoad() -> TripWizardScreen {
if navigationTitle.waitForExistence(timeout: BaseUITestCase.defaultTimeout) || if navigationTitle.waitForExistence(timeout: BaseUITestCase.longTimeout) ||
planningModeButton("dateRange").waitForExistence(timeout: BaseUITestCase.defaultTimeout) { planningModeButton("dateRange").waitForExistence(timeout: BaseUITestCase.longTimeout) {
return self 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") XCTFail("Trip Wizard should appear")
return self return self
} }
@@ -169,6 +208,19 @@ struct TripWizardScreen {
/// Selects the "By Dates" planning mode and waits for steps to expand. /// Selects the "By Dates" planning mode and waits for steps to expand.
func selectDateRangeMode() { func selectDateRangeMode() {
selectPlanningMode("dateRange") 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. /// Navigates the calendar to a target month/year and selects start/end dates.
@@ -178,6 +230,11 @@ struct TripWizardScreen {
startDay: String, startDay: String,
endDay: 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. // First, navigate by month label so tests that assert month visibility stay stable.
monthLabel.scrollIntoView(in: app.scrollViews.firstMatch) monthLabel.scrollIntoView(in: app.scrollViews.firstMatch)
let targetMonthYear = "\(targetMonth) \(targetYear)" let targetMonthYear = "\(targetMonth) \(targetYear)"
@@ -560,8 +617,31 @@ struct SettingsScreen {
// MARK: Assertions // MARK: Assertions
func assertLoaded() { func assertLoaded() {
XCTAssertTrue(subscriptionSection.waitForExistence(timeout: BaseUITestCase.defaultTimeout), if subscriptionSection.waitForExistence(timeout: BaseUITestCase.defaultTimeout) {
"Settings should show Subscription section") 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() { func assertVersionDisplayed() {