fix: comprehensive codebase hardening — crashes, silent failures, performance, and security
Fixes ~95 issues from deep audit across 12 categories in 82 files: - Crash prevention: double-resume in PhotoMetadataExtractor, force unwraps in DateRangePicker, array bounds checks in polls/achievements, ProGate hit-test bypass, Dictionary(uniqueKeysWithValues:) → uniquingKeysWith in 4 files - Silent failure elimination: all 34 try? sites replaced with do/try/catch + logging (SavedTrip, TripDetailView, CanonicalSyncService, BootstrapService, CanonicalModels, CKModels, SportsTimeApp, and more) - Performance: cached DateFormatters (7 files), O(1) team lookups via AppDataProvider, achievement definition dictionary, AnimatedBackground consolidated from 19 Tasks to 1, task cancellation in SharePreviewView - Concurrency: UIKit drawing → MainActor.run, background fetch timeout guard, @MainActor on ThemeManager/AppearanceManager, SyncLogger read/write race fix - Planning engine: game end time in travel feasibility, state-aware city normalization, exact city matching, DrivingConstraints parameter propagation - IAP: unknown subscription states → expired, unverified transaction logging, entitlements updated before paywall dismiss, restore visible to all users - Security: API key to Info.plist lookup, filename sanitization in PDF export, honest User-Agent, removed stale "Feels" analytics super properties - Navigation: consolidated competing navigationDestination, boolean → value-based - Testing: 8 sleep() → waitForExistence, duplicates extracted, Swift 6 compat - Service bugs: infinite retry cap, duplicate achievement prevention, TOCTOU vote fix, PollVote.odg → voterId rename, deterministic placeholder IDs, parallel MKDirections, Sendable-safe POI struct Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,111 @@ final class SportsTimeUITests: XCTestCase {
|
||||
XCTAssertTrue(dateRangeMode.isHittable, "Planning mode option should remain hittable at large Dynamic Type")
|
||||
}
|
||||
|
||||
// MARK: - Shared Wizard Helpers
|
||||
|
||||
/// Launches the app with standard UI testing arguments and taps Start Planning.
|
||||
private func launchAndStartPlanning() -> XCUIApplication {
|
||||
let app = XCUIApplication()
|
||||
app.launchArguments = [
|
||||
"--ui-testing",
|
||||
"--disable-animations",
|
||||
"--reset-state"
|
||||
]
|
||||
app.launch()
|
||||
|
||||
let startPlanningButton = app.buttons["home.startPlanningButton"]
|
||||
XCTAssertTrue(startPlanningButton.waitForExistence(timeout: 10), "Start Planning button should exist")
|
||||
startPlanningButton.tap()
|
||||
return app
|
||||
}
|
||||
|
||||
/// Fills the wizard: selects By Dates mode, navigates to June 2026,
|
||||
/// picks June 11-16, MLB, and Central region.
|
||||
private func fillWizardSteps(app: XCUIApplication) {
|
||||
// Choose "By Dates" mode
|
||||
let dateRangeMode = app.buttons["wizard.planningMode.dateRange"]
|
||||
XCTAssertTrue(dateRangeMode.waitForExistence(timeout: 10), "Date Range mode should exist")
|
||||
dateRangeMode.tap()
|
||||
|
||||
// Navigate to June 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()
|
||||
// Wait for the month label to update after tap
|
||||
let updatedLabel = app.staticTexts["wizard.dates.monthLabel"]
|
||||
_ = updatedLabel.waitForExistence(timeout: 2)
|
||||
attempts += 1
|
||||
}
|
||||
|
||||
// Select June 11
|
||||
let june11 = app.buttons["wizard.dates.day.2026-06-11"]
|
||||
june11.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
june11.tap()
|
||||
|
||||
// Wait for June 16 to be available after selecting the start date
|
||||
let june16 = app.buttons["wizard.dates.day.2026-06-16"]
|
||||
XCTAssertTrue(june16.waitForExistence(timeout: 5), "June 16 button should exist after selecting start date")
|
||||
june16.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
june16.tap()
|
||||
|
||||
// Select MLB
|
||||
let mlbButton = app.buttons["wizard.sports.mlb"]
|
||||
mlbButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
mlbButton.tap()
|
||||
|
||||
// Select Central region
|
||||
let centralRegion = app.buttons["wizard.regions.central"]
|
||||
centralRegion.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
centralRegion.tap()
|
||||
}
|
||||
|
||||
/// Taps "Plan My Trip" after waiting for it to become enabled.
|
||||
private func tapPlanTrip(app: XCUIApplication) {
|
||||
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()
|
||||
}
|
||||
|
||||
/// Selects "Most Games" sort option from the sort dropdown.
|
||||
private func selectMostGamesSort(app: XCUIApplication) {
|
||||
let sortDropdown = app.buttons["tripOptions.sortDropdown"]
|
||||
XCTAssertTrue(sortDropdown.waitForExistence(timeout: 30), "Sort dropdown should exist")
|
||||
sortDropdown.tap()
|
||||
|
||||
// Wait for the sort option to appear
|
||||
let mostGamesOption = app.buttons["tripOptions.sortOption.mostgames"]
|
||||
do {
|
||||
let found = mostGamesOption.waitForExistence(timeout: 3)
|
||||
if found {
|
||||
mostGamesOption.tap()
|
||||
} else {
|
||||
XCTFail("Expected element with accessibility ID 'tripOptions.sortOption.mostgames' but it did not appear")
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for sort to be applied and trip list to update
|
||||
let anyTrip = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'tripOptions.trip.'")).firstMatch
|
||||
XCTAssertTrue(anyTrip.waitForExistence(timeout: 10), "Trip options should appear after sorting")
|
||||
}
|
||||
|
||||
/// Selects the first available trip option and waits for the detail view to load.
|
||||
private func selectFirstTrip(app: XCUIApplication) {
|
||||
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()
|
||||
|
||||
// Wait for the trip detail view to load
|
||||
let favoriteButton = app.buttons["tripDetail.favoriteButton"]
|
||||
XCTAssertTrue(favoriteButton.waitForExistence(timeout: 10), "Trip detail view should load with favorite button")
|
||||
}
|
||||
|
||||
// MARK: - Demo Flow Test (Continuous Scroll Mode)
|
||||
|
||||
/// Complete trip planning demo with continuous smooth scrolling.
|
||||
@@ -67,198 +172,71 @@ final class SportsTimeUITests: XCTestCase {
|
||||
/// 4. Wait for transitions to complete
|
||||
@MainActor
|
||||
func testTripPlanningDemoFlow() throws {
|
||||
let app = XCUIApplication()
|
||||
app.launchArguments = [
|
||||
"--ui-testing",
|
||||
"--disable-animations",
|
||||
"--reset-state"
|
||||
]
|
||||
app.launch()
|
||||
let app = launchAndStartPlanning()
|
||||
fillWizardSteps(app: app)
|
||||
tapPlanTrip(app: app)
|
||||
selectMostGamesSort(app: app)
|
||||
selectFirstTrip(app: app)
|
||||
|
||||
// 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 - 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: 10), "Date Range mode should exist")
|
||||
dateRangeMode.tap()
|
||||
|
||||
// Navigate to June 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
|
||||
}
|
||||
|
||||
// Select June 11-16
|
||||
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)
|
||||
|
||||
// Select MLB
|
||||
let mlbButton = app.buttons["wizard.sports.mlb"]
|
||||
mlbButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
mlbButton.tap()
|
||||
|
||||
// Select Central region
|
||||
let centralRegion = app.buttons["wizard.regions.central"]
|
||||
centralRegion.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
centralRegion.tap()
|
||||
|
||||
// MARK: Step 3 - Plan trip
|
||||
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 4 - 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 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)
|
||||
|
||||
// MARK: Step 6 - Scroll through itinerary
|
||||
// Scroll through itinerary
|
||||
for _ in 1...5 {
|
||||
slowSwipeUp(app: app)
|
||||
Thread.sleep(forTimeInterval: 1.5)
|
||||
// Wait for scroll content to settle
|
||||
let scrollView = app.scrollViews.firstMatch
|
||||
_ = scrollView.waitForExistence(timeout: 3)
|
||||
}
|
||||
|
||||
// MARK: Step 7 - Favorite the trip
|
||||
// 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)
|
||||
|
||||
// Wait for favorite state to update
|
||||
let favoritedPredicate = NSPredicate(format: "isSelected == true OR label CONTAINS 'Favorited' OR label CONTAINS 'Unfavorite'")
|
||||
let favoritedExpectation = XCTNSPredicateExpectation(predicate: favoritedPredicate, object: favoriteButton)
|
||||
let result = XCTWaiter.wait(for: [favoritedExpectation], timeout: 5)
|
||||
// If the button doesn't change state visually, at least verify it still exists
|
||||
if result != .completed {
|
||||
XCTAssertTrue(favoriteButton.exists, "Favorite button should still exist after tapping")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Manual Demo Flow Test (Original)
|
||||
|
||||
/// Original manual test flow for comparison or when demo mode is not desired
|
||||
/// Original manual test flow for comparison or when demo mode is not desired.
|
||||
/// This test verifies the same wizard flow as the demo test, plus additional
|
||||
/// scrolling through the itinerary detail view.
|
||||
@MainActor
|
||||
func testTripPlanningManualFlow() throws {
|
||||
let app = XCUIApplication()
|
||||
app.launchArguments = [
|
||||
"--ui-testing",
|
||||
"--disable-animations",
|
||||
"--reset-state"
|
||||
]
|
||||
app.launch()
|
||||
let app = launchAndStartPlanning()
|
||||
fillWizardSteps(app: app)
|
||||
tapPlanTrip(app: app)
|
||||
selectMostGamesSort(app: app)
|
||||
selectFirstTrip(app: app)
|
||||
|
||||
// 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
|
||||
// Scroll through itinerary
|
||||
for _ in 1...5 {
|
||||
slowSwipeUp(app: app)
|
||||
Thread.sleep(forTimeInterval: 1.5)
|
||||
// Wait for scroll content to settle
|
||||
let scrollView = app.scrollViews.firstMatch
|
||||
_ = scrollView.waitForExistence(timeout: 3)
|
||||
}
|
||||
|
||||
// MARK: Step 10 - Favorite the trip
|
||||
// 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)
|
||||
|
||||
// Wait for favorite state to update
|
||||
let favoritedPredicate = NSPredicate(format: "isSelected == true OR label CONTAINS 'Favorited' OR label CONTAINS 'Unfavorite'")
|
||||
let favoritedExpectation = XCTNSPredicateExpectation(predicate: favoritedPredicate, object: favoriteButton)
|
||||
let result = XCTWaiter.wait(for: [favoritedExpectation], timeout: 5)
|
||||
// If the button doesn't change state visually, at least verify it still exists
|
||||
if result != .completed {
|
||||
XCTAssertTrue(favoriteButton.exists, "Favorite button should still exist after tapping")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
Reference in New Issue
Block a user