Harden test harness and UI suite

This commit is contained in:
Trey t
2026-04-03 15:30:54 -05:00
parent 87b9971714
commit 0fa3db5401
13 changed files with 319 additions and 55 deletions

View File

@@ -59,6 +59,33 @@ class BaseUITestCase: XCTestCase {
screenshot.lifetime = .keepAlways
add(screenshot)
}
/// Polls until the condition becomes true or the timeout expires.
@discardableResult
func waitUntil(
timeout: TimeInterval = BaseUITestCase.defaultTimeout,
pollInterval: TimeInterval = 0.2,
_ message: String? = nil,
file: StaticString = #filePath,
line: UInt = #line,
condition: @escaping () -> Bool
) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if condition() {
return true
}
let remaining = deadline.timeIntervalSinceNow
let interval = min(pollInterval, max(0.01, remaining))
RunLoop.current.run(until: Date().addingTimeInterval(interval))
}
let success = condition()
XCTAssertTrue(success, message ?? "Condition was not met within \(timeout)s", file: file, line: line)
return success
}
}
// MARK: - Wait Helpers

View File

@@ -45,10 +45,12 @@ final class AppLaunchTests: BaseUITestCase {
// Background the app
XCUIDevice.shared.press(.home)
sleep(2)
// Foreground
app.activate()
waitUntil(timeout: BaseUITestCase.longTimeout, "App should return to the foreground") {
self.app.state == .runningForeground
}
// Assert: Home still loaded, no re-bootstrap
XCTAssertTrue(

View File

@@ -85,12 +85,10 @@ final class HomeTests: BaseUITestCase {
// Tap refresh and verify no crash
refreshButton.tap()
// Wait briefly for reload
sleep(2)
// Featured section should still exist after refresh
XCTAssertTrue(section.exists,
"Featured trips section should remain after refresh")
waitUntil(timeout: BaseUITestCase.longTimeout, "Featured trips section should remain after refresh") {
section.exists && refreshButton.exists
}
captureScreenshot(named: "F015-FeaturedTripsRefresh")
}
@@ -307,14 +305,17 @@ final class HomeTests: BaseUITestCase {
home.waitForLoad()
home.switchToTab(home.myTripsTab)
// Wait briefly for My Trips content to load
sleep(1)
let myTrips = MyTripsScreen(app: app)
waitUntil(timeout: BaseUITestCase.longTimeout, "My Trips should load before refreshing") {
myTrips.emptyState.exists || myTrips.tripCard(0).exists || self.app.staticTexts["Group Polls"].exists
}
// Pull down to refresh
app.swipeDown(velocity: .slow)
// Wait for any refresh to complete
sleep(2)
waitUntil(timeout: BaseUITestCase.longTimeout, "My Trips should still be loaded after pull to refresh") {
myTrips.emptyState.exists || myTrips.tripCard(0).exists || self.app.staticTexts["Group Polls"].exists
}
// Verify the tab is still functional (no crash)
let groupPolls = app.staticTexts["Group Polls"]

View File

@@ -206,22 +206,14 @@ final class ProgressTests: BaseUITestCase {
visitSheet.tapSave()
visitSheet.navigationBar.waitForNonExistence(timeout: BaseUITestCase.defaultTimeout)
// Wait for data to reload
sleep(2)
// Progress should have updated verify the progress circle label changed
let updatedCircle = app.descendants(matching: .any).matching(NSPredicate(
format: "label CONTAINS 'stadiums visited'"
)).firstMatch
XCTAssertTrue(updatedCircle.waitForExistence(timeout: BaseUITestCase.longTimeout),
"Progress circle should exist after adding a visit")
// If we had an initial label, verify it changed
if !initialLabel.isEmpty {
// The new label should have a higher visited count
XCTAssertNotEqual(updatedCircle.label, initialLabel,
"Progress label should update after adding a visit")
waitUntil(timeout: BaseUITestCase.longTimeout, "Progress label should update after adding a visit") {
guard updatedCircle.exists else { return false }
return initialLabel.isEmpty || updatedCircle.label != initialLabel
}
captureScreenshot(named: "F099-ProgressPercentageUpdated")

View File

@@ -127,9 +127,10 @@ final class ScheduleTests: BaseUITestCase {
"Search field should exist")
searchField.tap()
searchField.typeText("Yankees")
// Wait for results to filter
sleep(1)
XCTAssertTrue(
((searchField.value as? String) ?? "").contains("Yankees"),
"Search field should contain the typed team name"
)
captureScreenshot(named: "F089-SearchByTeam")
}
@@ -150,9 +151,10 @@ final class ScheduleTests: BaseUITestCase {
"Search field should exist")
searchField.tap()
searchField.typeText("Wrigley")
// Wait for results to filter
sleep(1)
XCTAssertTrue(
((searchField.value as? String) ?? "").contains("Wrigley"),
"Search field should contain the typed venue name"
)
captureScreenshot(named: "F090-SearchByVenue")
}
@@ -174,19 +176,15 @@ final class ScheduleTests: BaseUITestCase {
searchField.tap()
searchField.typeText("ZZZZNONEXISTENTTEAMZZZZ")
// Wait for empty state
sleep(1)
// Empty state or "no results" text should appear
let emptyState = schedule.emptyState
let noResults = app.staticTexts.matching(NSPredicate(
format: "label CONTAINS[c] 'no' AND label CONTAINS[c] 'game'"
)).firstMatch
let hasEmptyIndicator = emptyState.waitForExistence(timeout: BaseUITestCase.shortTimeout)
|| noResults.waitForExistence(timeout: BaseUITestCase.shortTimeout)
XCTAssertTrue(hasEmptyIndicator,
"Empty state should appear when no games match search")
waitUntil(timeout: BaseUITestCase.shortTimeout, "Empty state should appear when no games match search") {
emptyState.exists || noResults.exists
}
captureScreenshot(named: "F092-ScheduleEmptyState")
}

View File

@@ -162,10 +162,10 @@ final class SettingsTests: BaseUITestCase {
let switchCoord = toggle.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5))
switchCoord.tap()
// Small wait for the toggle animation to complete
sleep(1)
// Value should have changed
waitUntil(timeout: BaseUITestCase.shortTimeout, "Toggle value should change after tapping the switch") {
(toggle.value as? String) != initialValue
}
let newValue = toggle.value as? String
XCTAssertNotEqual(initialValue, newValue,
"Toggle value should change after tap (was '\(initialValue ?? "nil")', now '\(newValue ?? "nil")')")

View File

@@ -157,8 +157,11 @@ final class TripOptionsTests: BaseUITestCase {
"'5' cities filter button should exist")
fiveCitiesButton.tap()
// Results should update; verify no crash
sleep(1)
let firstTrip = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'tripOptions.trip.'")).firstMatch
XCTAssertTrue(
firstTrip.waitForExistence(timeout: BaseUITestCase.shortTimeout),
"Trip results should remain visible after applying the cities filter"
)
captureScreenshot(named: "F057-CitiesFilter-5")
}