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:
46
SportsTimeUITests/Tests/AccessibilityTests.swift
Normal file
46
SportsTimeUITests/Tests/AccessibilityTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
38
SportsTimeUITests/Tests/AppLaunchTests.swift
Normal file
38
SportsTimeUITests/Tests/AppLaunchTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
24
SportsTimeUITests/Tests/ScheduleTests.swift
Normal file
24
SportsTimeUITests/Tests/ScheduleTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
48
SportsTimeUITests/Tests/SettingsTests.swift
Normal file
48
SportsTimeUITests/Tests/SettingsTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
47
SportsTimeUITests/Tests/TabNavigationTests.swift
Normal file
47
SportsTimeUITests/Tests/TabNavigationTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
66
SportsTimeUITests/Tests/TripSavingTests.swift
Normal file
66
SportsTimeUITests/Tests/TripSavingTests.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
71
SportsTimeUITests/Tests/TripWizardFlowTests.swift
Normal file
71
SportsTimeUITests/Tests/TripWizardFlowTests.swift
Normal 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"
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user