feat: add XCUITest suite with 10 critical flow tests and QA test plan

Add comprehensive UI test infrastructure with Page Object pattern,
accessibility identifiers, UI test mode (--ui-testing, --reset-state,
--disable-animations), and 10 passing tests covering app launch, tab
navigation, trip wizard, trip saving, settings, schedule, and
accessibility at XXXL Dynamic Type. Also adds a 229-case QA test plan
Excel workbook for manual QA handoff.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-16 16:23:59 -06:00
parent 787a0f795e
commit d53f222489
16 changed files with 1528 additions and 25 deletions

View File

@@ -0,0 +1,46 @@
//
// AccessibilityTests.swift
// SportsTimeUITests
//
// Smoke test for Dynamic Type accessibility at XXXL text size.
//
import XCTest
final class AccessibilityTests: BaseUITestCase {
/// Verifies the entry flow is usable at AX XXL text size.
@MainActor
func testLargeDynamicTypeEntryFlow() {
// Re-launch with large Dynamic Type
app.terminate()
app.launchArguments = [
"--ui-testing",
"--disable-animations",
"--reset-state",
"-UIPreferredContentSizeCategoryName",
"UICTContentSizeCategoryAccessibilityXXXL"
]
app.launch()
let home = HomeScreen(app: app)
home.waitForLoad()
// At XXXL text the button may be pushed below the fold; scroll into view
home.startPlanningButton.scrollIntoView(in: app.scrollViews.firstMatch)
XCTAssertTrue(home.startPlanningButton.isHittable,
"Start Planning should remain hittable at large Dynamic Type")
// Open wizard and verify planning mode options are reachable
home.tapStartPlanning()
let wizard = TripWizardScreen(app: app)
wizard.waitForLoad()
let dateRangeMode = wizard.planningModeButton("dateRange")
dateRangeMode.scrollIntoView(in: app.scrollViews.firstMatch)
XCTAssertTrue(dateRangeMode.isHittable,
"Planning mode should be hittable at large Dynamic Type")
captureScreenshot(named: "Accessibility-LargeType")
}
}

View File

@@ -0,0 +1,38 @@
//
// AppLaunchTests.swift
// SportsTimeUITests
//
// Verifies app boot, bootstrap, and initial screen rendering.
//
import XCTest
final class AppLaunchTests: BaseUITestCase {
/// Verifies the app boots, shows the home screen, and all 5 tabs are present.
@MainActor
func testAppLaunchShowsHomeWithAllTabs() {
let home = HomeScreen(app: app)
home.waitForLoad()
// Assert: Hero card text visible
XCTAssertTrue(home.adventureAwaitsText.exists,
"Hero card should display 'Adventure Awaits'")
// Assert: All tabs present
home.assertTabBarVisible()
captureScreenshot(named: "HomeScreen-Launch")
}
/// Verifies the bootstrap loading indicator disappears and content renders.
@MainActor
func testBootstrapCompletesWithContent() {
let home = HomeScreen(app: app)
home.waitForLoad()
// Assert: Start Planning button is interactable (proves bootstrap loaded data)
XCTAssertTrue(home.startPlanningButton.isHittable,
"Start Planning should be hittable after bootstrap")
}
}

View File

@@ -0,0 +1,24 @@
//
// ScheduleTests.swift
// SportsTimeUITests
//
// Verifies the Schedule tab loads and displays content.
//
import XCTest
final class ScheduleTests: BaseUITestCase {
/// Verifies the schedule tab loads and shows content.
@MainActor
func testScheduleTabLoads() {
let home = HomeScreen(app: app)
home.waitForLoad()
home.switchToTab(home.scheduleTab)
let schedule = ScheduleScreen(app: app)
schedule.assertLoaded()
captureScreenshot(named: "Schedule-Loaded")
}
}

View File

@@ -0,0 +1,48 @@
//
// SettingsTests.swift
// SportsTimeUITests
//
// Verifies the Settings screen loads, displays version, and shows all sections.
//
import XCTest
final class SettingsTests: BaseUITestCase {
/// Verifies the Settings screen loads and displays the app version.
@MainActor
func testSettingsShowsVersion() {
let home = HomeScreen(app: app)
home.waitForLoad()
home.switchToTab(home.settingsTab)
let settings = SettingsScreen(app: app)
settings.assertLoaded()
settings.assertVersionDisplayed()
captureScreenshot(named: "Settings-Version")
}
/// Verifies key sections are present in Settings.
@MainActor
func testSettingsSectionsPresent() {
let home = HomeScreen(app: app)
home.waitForLoad()
home.switchToTab(home.settingsTab)
let settings = SettingsScreen(app: app)
settings.assertLoaded()
// Assert: Key sections exist
XCTAssertTrue(settings.subscriptionSection.waitForExistence(
timeout: BaseUITestCase.shortTimeout),
"Subscription section should exist")
// SwiftUI List renders as UICollectionView on iOS 26
settings.privacySection.scrollIntoView(in: app.collectionViews.firstMatch)
XCTAssertTrue(settings.privacySection.exists, "Privacy section should exist")
settings.aboutSection.scrollIntoView(in: app.collectionViews.firstMatch)
XCTAssertTrue(settings.aboutSection.exists, "About section should exist")
}
}

View File

@@ -0,0 +1,47 @@
//
// TabNavigationTests.swift
// SportsTimeUITests
//
// Verifies navigation through all 5 tabs.
//
import XCTest
final class TabNavigationTests: BaseUITestCase {
/// Navigates through all 5 tabs and asserts each one loads.
@MainActor
func testTabNavigationCycle() {
let home = HomeScreen(app: app)
home.waitForLoad()
// Schedule tab
home.switchToTab(home.scheduleTab)
let schedule = ScheduleScreen(app: app)
schedule.assertLoaded()
// My Trips tab
home.switchToTab(home.myTripsTab)
let myTrips = MyTripsScreen(app: app)
myTrips.assertEmpty()
// Progress tab (Pro-gated, but UI test mode forces Pro)
home.switchToTab(home.progressTab)
// Just verify the tab switched without crash
let progressNav = app.navigationBars.firstMatch
XCTAssertTrue(progressNav.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
"Progress tab should load")
// Settings tab
home.switchToTab(home.settingsTab)
let settings = SettingsScreen(app: app)
settings.assertLoaded()
// Return home
home.switchToTab(home.homeTab)
XCTAssertTrue(home.startPlanningButton.waitForExistence(timeout: BaseUITestCase.shortTimeout),
"Should return to Home tab")
captureScreenshot(named: "TabNavigation-ReturnHome")
}
}

View File

@@ -0,0 +1,66 @@
//
// TripSavingTests.swift
// SportsTimeUITests
//
// Tests the end-to-end trip saving flow: plan select save verify in My Trips.
//
import XCTest
final class TripSavingTests: BaseUITestCase {
/// Plans a trip, selects an option, saves it, and verifies it appears in My Trips.
@MainActor
func testSaveTripAppearsInMyTrips() {
let home = HomeScreen(app: app)
home.waitForLoad()
home.tapStartPlanning()
// Plan a trip using date range mode
let wizard = TripWizardScreen(app: app)
wizard.waitForLoad()
wizard.selectDateRangeMode()
wizard.nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
wizard.selectDateRange(
targetMonth: "June",
targetYear: "2026",
startDay: "2026-06-11",
endDay: "2026-06-16"
)
wizard.selectSport("mlb")
wizard.selectRegion("central")
wizard.tapPlanTrip()
// Select first trip option
let options = TripOptionsScreen(app: app)
options.waitForLoad()
options.selectTrip(at: 0)
// Save the trip
let detail = TripDetailScreen(app: app)
detail.waitForLoad()
detail.assertSaveState(isSaved: false)
detail.tapFavorite()
// Allow save to persist
detail.assertSaveState(isSaved: true)
captureScreenshot(named: "TripSaving-Favorited")
// Navigate back to My Trips tab
// Dismiss the entire wizard sheet: Detail Options Wizard Cancel
app.navigationBars.buttons.firstMatch.tap() // Back from detail to options
// Back from options to wizard
let wizardBackBtn = app.navigationBars.buttons.firstMatch
wizardBackBtn.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
// Cancel the wizard sheet
wizard.tapCancel()
// Now the tab bar is accessible
home.switchToTab(home.myTripsTab)
// Assert: Saved trip appears (empty state should NOT be visible)
let myTrips = MyTripsScreen(app: app)
myTrips.assertHasTrips()
}
}

View File

@@ -0,0 +1,71 @@
//
// TripWizardFlowTests.swift
// SportsTimeUITests
//
// Tests the trip planning wizard: date range mode, calendar navigation,
// sport/region selection, and planning engine results.
//
import XCTest
final class TripWizardFlowTests: BaseUITestCase {
/// Full flow: Start Planning Date Range Select dates MLB Central Plan.
/// Asserts the planning engine returns results.
@MainActor
func testDateRangeTripPlanningFlow() {
let home = HomeScreen(app: app)
home.waitForLoad()
home.tapStartPlanning()
let wizard = TripWizardScreen(app: app)
wizard.waitForLoad()
// Step 1: Select "By Dates" mode
wizard.selectDateRangeMode()
// Step 2: Navigate to June 2026 and select June 11-16
// Scroll to see dates step
wizard.nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
wizard.selectDateRange(
targetMonth: "June",
targetYear: "2026",
startDay: "2026-06-11",
endDay: "2026-06-16"
)
// Step 3: Select MLB
wizard.selectSport("mlb")
// Step 4: Select Central region
wizard.selectRegion("central")
// Step 5: Tap Plan My Trip
wizard.tapPlanTrip()
// Assert: Trip Options screen appears with results
let options = TripOptionsScreen(app: app)
options.waitForLoad()
options.assertHasResults()
captureScreenshot(named: "TripWizard-PlanningResults")
}
/// Verifies the wizard can be dismissed via Cancel.
@MainActor
func testWizardCanBeDismissed() {
let home = HomeScreen(app: app)
home.waitForLoad()
home.tapStartPlanning()
let wizard = TripWizardScreen(app: app)
wizard.waitForLoad()
wizard.tapCancel()
// Assert: Back on home screen
XCTAssertTrue(
home.startPlanningButton.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
"Should return to Home after cancelling wizard"
)
}
}