Stabilize flaky UI wizard and settings test flows
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user