Add 22 new UI tests across 8 test files covering Home, Schedule, Progress, Settings, TabNavigation, TripSaving, and TripOptions. Add accessibility identifiers to 11 view files for test element discovery. Fix sport chip assertion logic (all sports start selected, tap deselects), scroll container issues on iOS 26 nested ScrollViews, toggle interaction, and delete trip flow. Update QA coverage map from 32 to 54 automated test cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
750 lines
21 KiB
Swift
750 lines
21 KiB
Swift
//
|
|
// Screens.swift
|
|
// SportsTimeUITests
|
|
//
|
|
// Page Object / Screen Object layer.
|
|
// Each struct wraps an XCUIApplication and exposes user-intent methods.
|
|
// Tests read like: homeScreen.tapStartPlanning()
|
|
//
|
|
|
|
import XCTest
|
|
|
|
// MARK: - Home Screen
|
|
|
|
struct HomeScreen {
|
|
let app: XCUIApplication
|
|
|
|
// MARK: Elements
|
|
|
|
var startPlanningButton: XCUIElement {
|
|
app.buttons["home.startPlanningButton"]
|
|
}
|
|
|
|
var homeTab: XCUIElement {
|
|
app.tabBars.buttons["Home"]
|
|
}
|
|
|
|
var scheduleTab: XCUIElement {
|
|
app.tabBars.buttons["Schedule"]
|
|
}
|
|
|
|
var myTripsTab: XCUIElement {
|
|
app.tabBars.buttons["My Trips"]
|
|
}
|
|
|
|
var progressTab: XCUIElement {
|
|
app.tabBars.buttons["Progress"]
|
|
}
|
|
|
|
var settingsTab: XCUIElement {
|
|
app.tabBars.buttons["Settings"]
|
|
}
|
|
|
|
var adventureAwaitsText: XCUIElement {
|
|
app.staticTexts["Adventure Awaits"]
|
|
}
|
|
|
|
var createTripToolbarButton: XCUIElement {
|
|
app.buttons["home.createNewTripButton"]
|
|
}
|
|
|
|
var featuredTripsSection: XCUIElement {
|
|
app.descendants(matching: .any)["home.featuredTripsSection"]
|
|
}
|
|
|
|
var recentTripsSection: XCUIElement {
|
|
app.descendants(matching: .any)["home.recentTripsSection"]
|
|
}
|
|
|
|
var tipsSection: XCUIElement {
|
|
app.descendants(matching: .any)["home.tipsSection"]
|
|
}
|
|
|
|
// MARK: Actions
|
|
|
|
/// Waits for the home screen to fully load after bootstrap.
|
|
@discardableResult
|
|
func waitForLoad() -> HomeScreen {
|
|
startPlanningButton.waitForExistenceOrFail(
|
|
timeout: BaseUITestCase.longTimeout,
|
|
"Home screen should load after bootstrap"
|
|
)
|
|
return self
|
|
}
|
|
|
|
/// Taps "Start Planning" to open the Trip Wizard sheet.
|
|
func tapStartPlanning() {
|
|
startPlanningButton.waitUntilHittable().tap()
|
|
}
|
|
|
|
/// Switches to a tab by tapping its tab bar button.
|
|
func switchToTab(_ tab: XCUIElement) {
|
|
tab.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
|
}
|
|
|
|
// MARK: Assertions
|
|
|
|
/// Asserts the tab bar is visible with all 5 tabs.
|
|
func assertTabBarVisible() {
|
|
XCTAssertTrue(homeTab.exists, "Home tab should exist")
|
|
XCTAssertTrue(scheduleTab.exists, "Schedule tab should exist")
|
|
XCTAssertTrue(myTripsTab.exists, "My Trips tab should exist")
|
|
XCTAssertTrue(progressTab.exists, "Progress tab should exist")
|
|
XCTAssertTrue(settingsTab.exists, "Settings tab should exist")
|
|
}
|
|
}
|
|
|
|
// MARK: - Trip Wizard Screen
|
|
|
|
struct TripWizardScreen {
|
|
let app: XCUIApplication
|
|
|
|
// MARK: Elements
|
|
|
|
var cancelButton: XCUIElement {
|
|
app.buttons["Cancel"]
|
|
}
|
|
|
|
var planTripButton: XCUIElement {
|
|
app.buttons["wizard.planTripButton"]
|
|
}
|
|
|
|
var navigationTitle: XCUIElement {
|
|
app.navigationBars["Plan a Trip"]
|
|
}
|
|
|
|
// Planning modes
|
|
func planningModeButton(_ mode: String) -> XCUIElement {
|
|
app.buttons["wizard.planningMode.\(mode)"]
|
|
}
|
|
|
|
// Sports
|
|
func sportButton(_ sport: String) -> XCUIElement {
|
|
app.buttons["wizard.sports.\(sport)"]
|
|
}
|
|
|
|
// Regions
|
|
func regionButton(_ region: String) -> XCUIElement {
|
|
app.buttons["wizard.regions.\(region)"]
|
|
}
|
|
|
|
// Date picker
|
|
var nextMonthButton: XCUIElement {
|
|
app.buttons["wizard.dates.nextMonth"]
|
|
}
|
|
|
|
var previousMonthButton: XCUIElement {
|
|
app.buttons["wizard.dates.previousMonth"]
|
|
}
|
|
|
|
var monthLabel: XCUIElement {
|
|
app.staticTexts["wizard.dates.monthLabel"]
|
|
}
|
|
|
|
func dayButton(_ dateString: String) -> XCUIElement {
|
|
app.buttons["wizard.dates.day.\(dateString)"]
|
|
}
|
|
|
|
// MARK: Actions
|
|
|
|
/// Waits for the wizard sheet to appear.
|
|
@discardableResult
|
|
func waitForLoad() -> TripWizardScreen {
|
|
navigationTitle.waitForExistenceOrFail(
|
|
timeout: BaseUITestCase.defaultTimeout,
|
|
"Trip Wizard should appear"
|
|
)
|
|
return self
|
|
}
|
|
|
|
/// Selects a planning mode by raw value (e.g., "dateRange", "gameFirst", "locations", "followTeam", "teamFirst").
|
|
func selectPlanningMode(_ mode: String) {
|
|
let btn = planningModeButton(mode)
|
|
btn.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
btn.tap()
|
|
}
|
|
|
|
/// Selects the "By Dates" planning mode and waits for steps to expand.
|
|
func selectDateRangeMode() {
|
|
selectPlanningMode("dateRange")
|
|
}
|
|
|
|
/// Navigates the calendar to a target month/year and selects start/end dates.
|
|
func selectDateRange(
|
|
targetMonth: String,
|
|
targetYear: String,
|
|
startDay: String,
|
|
endDay: String
|
|
) {
|
|
// Navigate forward to the target month
|
|
let target = "\(targetMonth) \(targetYear)"
|
|
var attempts = 0
|
|
// First ensure the month label is visible
|
|
monthLabel.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
while !monthLabel.label.contains(target) && attempts < 18 {
|
|
nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
|
attempts += 1
|
|
}
|
|
XCTAssertTrue(monthLabel.label.contains(target),
|
|
"Should navigate to \(target)")
|
|
|
|
// Select start date — scroll calendar grid into view first
|
|
let startBtn = dayButton(startDay)
|
|
startBtn.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
startBtn.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
|
|
|
// Select end date
|
|
let endBtn = dayButton(endDay)
|
|
endBtn.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
endBtn.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
|
}
|
|
|
|
/// Selects a sport (e.g., "mlb").
|
|
func selectSport(_ sport: String) {
|
|
let btn = sportButton(sport)
|
|
btn.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
btn.tap()
|
|
}
|
|
|
|
/// Selects a region (e.g., "central").
|
|
func selectRegion(_ region: String) {
|
|
let btn = regionButton(region)
|
|
btn.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
btn.tap()
|
|
}
|
|
|
|
/// Scrolls to and taps "Plan My Trip".
|
|
func tapPlanTrip() {
|
|
let btn = planTripButton
|
|
btn.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
btn.waitUntilHittable().tap()
|
|
}
|
|
|
|
/// Dismisses the wizard via the Cancel button.
|
|
func tapCancel() {
|
|
cancelButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
|
}
|
|
|
|
// MARK: Assertions
|
|
|
|
/// Asserts a specific planning mode button exists and is hittable.
|
|
func assertPlanningModeAvailable(_ mode: String) {
|
|
let btn = planningModeButton(mode)
|
|
btn.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
XCTAssertTrue(btn.isHittable,
|
|
"Planning mode '\(mode)' should be available")
|
|
}
|
|
}
|
|
|
|
// MARK: - Trip Options Screen
|
|
|
|
struct TripOptionsScreen {
|
|
let app: XCUIApplication
|
|
|
|
// MARK: Elements
|
|
|
|
var sortDropdown: XCUIElement {
|
|
app.buttons["tripOptions.sortDropdown"]
|
|
}
|
|
|
|
func tripCard(_ index: Int) -> XCUIElement {
|
|
app.buttons["tripOptions.trip.\(index)"]
|
|
}
|
|
|
|
func sortOption(_ name: String) -> XCUIElement {
|
|
app.buttons["tripOptions.sortOption.\(name)"]
|
|
}
|
|
|
|
// MARK: Actions
|
|
|
|
/// Waits for planning results to load.
|
|
@discardableResult
|
|
func waitForLoad() -> TripOptionsScreen {
|
|
sortDropdown.waitForExistenceOrFail(
|
|
timeout: BaseUITestCase.longTimeout,
|
|
"Trip Options should appear after planning completes"
|
|
)
|
|
return self
|
|
}
|
|
|
|
/// Selects a trip option card by index.
|
|
func selectTrip(at index: Int) {
|
|
let card = tripCard(index)
|
|
card.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
card.tap()
|
|
}
|
|
|
|
/// Opens the sort dropdown and selects an option.
|
|
func sort(by option: String) {
|
|
sortDropdown.waitUntilHittable().tap()
|
|
let optionBtn = sortOption(option)
|
|
optionBtn.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
|
}
|
|
|
|
// MARK: Assertions
|
|
|
|
/// Asserts at least one trip option is visible.
|
|
func assertHasResults() {
|
|
XCTAssertTrue(tripCard(0).waitForExistence(timeout: BaseUITestCase.shortTimeout),
|
|
"At least one trip option should exist")
|
|
}
|
|
}
|
|
|
|
// MARK: - Trip Detail Screen
|
|
|
|
struct TripDetailScreen {
|
|
let app: XCUIApplication
|
|
|
|
// MARK: Elements
|
|
|
|
var favoriteButton: XCUIElement {
|
|
app.buttons["tripDetail.favoriteButton"]
|
|
}
|
|
|
|
var itineraryTitle: XCUIElement {
|
|
app.staticTexts["Itinerary"]
|
|
}
|
|
|
|
var statsRow: XCUIElement {
|
|
app.descendants(matching: .any)["tripDetail.statsRow"]
|
|
}
|
|
|
|
var pdfExportButton: XCUIElement {
|
|
app.buttons["tripDetail.pdfExportButton"]
|
|
}
|
|
|
|
// MARK: Actions
|
|
|
|
/// Waits for the trip detail view to load.
|
|
@discardableResult
|
|
func waitForLoad() -> TripDetailScreen {
|
|
favoriteButton.waitForExistenceOrFail(
|
|
timeout: BaseUITestCase.defaultTimeout,
|
|
"Trip Detail should load with favorite button"
|
|
)
|
|
return self
|
|
}
|
|
|
|
/// Taps the favorite/save button.
|
|
func tapFavorite() {
|
|
favoriteButton.waitUntilHittable().tap()
|
|
}
|
|
|
|
// MARK: Assertions
|
|
|
|
/// Asserts the itinerary section is visible.
|
|
func assertItineraryVisible() {
|
|
let title = itineraryTitle
|
|
title.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
XCTAssertTrue(title.exists, "Itinerary section should be visible")
|
|
}
|
|
|
|
/// Asserts the stats row (cities, games, distance, driving time) is visible.
|
|
func assertStatsRowVisible() {
|
|
statsRow.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
XCTAssertTrue(statsRow.exists, "Stats row should be visible")
|
|
}
|
|
|
|
/// Asserts the favorite button label matches the expected state.
|
|
func assertSaveState(isSaved: Bool) {
|
|
let expected = isSaved ? "Remove from favorites" : "Save to favorites"
|
|
XCTAssertEqual(favoriteButton.label, expected,
|
|
"Favorite button label should reflect saved state")
|
|
}
|
|
}
|
|
|
|
// MARK: - My Trips Screen
|
|
|
|
struct MyTripsScreen {
|
|
let app: XCUIApplication
|
|
|
|
// MARK: Elements
|
|
|
|
var emptyState: XCUIElement {
|
|
// VStack with accessibilityIdentifier can map to different element types on iOS 26;
|
|
// use descendants(matching: .any) for a type-agnostic match.
|
|
app.descendants(matching: .any)["myTrips.emptyState"]
|
|
}
|
|
|
|
var savedTripsTitle: XCUIElement {
|
|
app.staticTexts["Saved Trips"]
|
|
}
|
|
|
|
/// Returns a saved trip card by index.
|
|
func tripCard(_ index: Int) -> XCUIElement {
|
|
app.descendants(matching: .any)["myTrips.trip.\(index)"]
|
|
}
|
|
|
|
// MARK: Actions
|
|
|
|
/// Taps a saved trip card by index.
|
|
func tapTrip(at index: Int) {
|
|
let card = tripCard(index)
|
|
card.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
|
}
|
|
|
|
/// Swipes left to delete a saved trip at the given index.
|
|
func deleteTrip(at index: Int) {
|
|
let card = tripCard(index)
|
|
card.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout)
|
|
card.swipeLeft(velocity: .slow)
|
|
|
|
// On iOS 26, swipe-to-delete button may be "Delete" or use a trash icon
|
|
let deleteButton = app.buttons["Delete"]
|
|
if deleteButton.waitForExistence(timeout: BaseUITestCase.shortTimeout) {
|
|
deleteButton.tap()
|
|
} else {
|
|
// Fallback: try a more aggressive swipe to trigger full delete
|
|
card.swipeLeft(velocity: .fast)
|
|
// If a delete button appears after the full swipe, tap it
|
|
if deleteButton.waitForExistence(timeout: BaseUITestCase.shortTimeout) {
|
|
deleteButton.tap()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Assertions
|
|
|
|
func assertEmpty() {
|
|
XCTAssertTrue(
|
|
emptyState.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
|
|
"Empty state should be visible when no trips saved"
|
|
)
|
|
}
|
|
|
|
func assertHasTrips() {
|
|
XCTAssertFalse(emptyState.exists, "Empty state should not show when trips exist")
|
|
}
|
|
}
|
|
|
|
// MARK: - Schedule Screen
|
|
|
|
struct ScheduleScreen {
|
|
let app: XCUIApplication
|
|
|
|
// MARK: Elements
|
|
|
|
var searchField: XCUIElement {
|
|
app.searchFields.firstMatch
|
|
}
|
|
|
|
var filterButton: XCUIElement {
|
|
// The filter menu button uses an accessibilityLabel
|
|
app.buttons.matching(NSPredicate(
|
|
format: "label CONTAINS 'Filter options'"
|
|
)).firstMatch
|
|
}
|
|
|
|
/// Returns a sport filter chip by lowercase sport name (e.g., "mlb").
|
|
func sportChip(_ sport: String) -> XCUIElement {
|
|
app.buttons["schedule.sport.\(sport)"]
|
|
}
|
|
|
|
var emptyState: XCUIElement {
|
|
app.descendants(matching: .any)["schedule.emptyState"]
|
|
}
|
|
|
|
var resetFiltersButton: XCUIElement {
|
|
app.buttons["schedule.resetFiltersButton"]
|
|
}
|
|
|
|
// MARK: Assertions
|
|
|
|
func assertLoaded() {
|
|
// Schedule tab should show the filter button with "Filter options" label
|
|
XCTAssertTrue(filterButton.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
|
|
"Schedule filter button should appear")
|
|
}
|
|
}
|
|
|
|
// MARK: - Settings Screen
|
|
|
|
struct SettingsScreen {
|
|
let app: XCUIApplication
|
|
|
|
// MARK: Elements
|
|
|
|
var versionLabel: XCUIElement {
|
|
app.staticTexts["settings.versionLabel"]
|
|
}
|
|
|
|
var subscriptionSection: XCUIElement {
|
|
app.staticTexts["Subscription"]
|
|
}
|
|
|
|
var privacySection: XCUIElement {
|
|
app.staticTexts["Privacy"]
|
|
}
|
|
|
|
var aboutSection: XCUIElement {
|
|
app.staticTexts["About"]
|
|
}
|
|
|
|
var upgradeProButton: XCUIElement {
|
|
app.buttons["settings.upgradeProButton"]
|
|
}
|
|
|
|
var restorePurchasesButton: XCUIElement {
|
|
app.buttons["settings.restorePurchasesButton"]
|
|
}
|
|
|
|
var animationsToggle: XCUIElement {
|
|
app.switches["settings.animationsToggle"]
|
|
}
|
|
|
|
var resetButton: XCUIElement {
|
|
app.buttons["settings.resetButton"]
|
|
}
|
|
|
|
var syncNowButton: XCUIElement {
|
|
app.buttons["settings.syncNowButton"]
|
|
}
|
|
|
|
var appearanceSection: XCUIElement {
|
|
app.staticTexts["Appearance"]
|
|
}
|
|
|
|
/// Returns an appearance mode button (e.g., "System", "Light", "Dark").
|
|
func appearanceButton(_ mode: String) -> XCUIElement {
|
|
app.buttons["settings.appearance.\(mode)"]
|
|
}
|
|
|
|
// MARK: Assertions
|
|
|
|
func assertLoaded() {
|
|
XCTAssertTrue(subscriptionSection.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
|
|
"Settings should show Subscription section")
|
|
}
|
|
|
|
func assertVersionDisplayed() {
|
|
// SwiftUI List renders as UICollectionView on iOS 26, not UITableView
|
|
versionLabel.scrollIntoView(in: app.collectionViews.firstMatch, direction: .down)
|
|
XCTAssertTrue(versionLabel.exists, "App version should be displayed")
|
|
XCTAssertFalse(versionLabel.label.isEmpty, "Version label should not be empty")
|
|
}
|
|
}
|
|
|
|
// MARK: - Progress Screen
|
|
|
|
struct ProgressScreen {
|
|
let app: XCUIApplication
|
|
|
|
// MARK: Elements
|
|
|
|
var addVisitButton: XCUIElement {
|
|
app.buttons.matching(NSPredicate(
|
|
format: "label == 'Add stadium visit'"
|
|
)).firstMatch
|
|
}
|
|
|
|
var stadiumQuestLabel: XCUIElement {
|
|
app.staticTexts["progress.stadiumQuest"]
|
|
}
|
|
|
|
var achievementsTitle: XCUIElement {
|
|
app.staticTexts["progress.achievementsTitle"]
|
|
}
|
|
|
|
var recentVisitsTitle: XCUIElement {
|
|
app.staticTexts["progress.recentVisitsTitle"]
|
|
}
|
|
|
|
var navigationBar: XCUIElement {
|
|
app.navigationBars.firstMatch
|
|
}
|
|
|
|
var sportSelector: XCUIElement {
|
|
app.descendants(matching: .any)["progress.sportSelector"]
|
|
}
|
|
|
|
/// Returns a sport button by lowercase sport name (e.g., "mlb").
|
|
func sportButton(_ sport: String) -> XCUIElement {
|
|
app.buttons["progress.sport.\(sport)"]
|
|
}
|
|
|
|
// MARK: Actions
|
|
|
|
@discardableResult
|
|
func waitForLoad() -> ProgressScreen {
|
|
// Progress tab shows the "Stadium Quest" label once data loads
|
|
let loaded = stadiumQuestLabel.waitForExistence(timeout: BaseUITestCase.longTimeout)
|
|
|| navigationBar.waitForExistence(timeout: BaseUITestCase.shortTimeout)
|
|
XCTAssertTrue(loaded, "Progress tab should load")
|
|
return self
|
|
}
|
|
|
|
// MARK: Assertions
|
|
|
|
func assertLoaded() {
|
|
XCTAssertTrue(
|
|
navigationBar.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
|
|
"Progress tab should load"
|
|
)
|
|
}
|
|
|
|
func assertAchievementsSectionVisible() {
|
|
achievementsTitle.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
XCTAssertTrue(achievementsTitle.exists, "Achievements section should be visible")
|
|
}
|
|
}
|
|
|
|
// MARK: - Polls Screen
|
|
|
|
struct PollsScreen {
|
|
let app: XCUIApplication
|
|
|
|
// MARK: Elements
|
|
|
|
var navigationTitle: XCUIElement {
|
|
app.navigationBars["Group Polls"]
|
|
}
|
|
|
|
var joinPollButton: XCUIElement {
|
|
app.buttons.matching(NSPredicate(
|
|
format: "label == 'Join a poll'"
|
|
)).firstMatch
|
|
}
|
|
|
|
var emptyState: XCUIElement {
|
|
app.staticTexts["No Polls"]
|
|
}
|
|
|
|
// MARK: Actions
|
|
|
|
@discardableResult
|
|
func waitForLoad() -> PollsScreen {
|
|
navigationTitle.waitForExistenceOrFail(
|
|
timeout: BaseUITestCase.defaultTimeout,
|
|
"Polls list should load"
|
|
)
|
|
return self
|
|
}
|
|
|
|
// MARK: Assertions
|
|
|
|
func assertLoaded() {
|
|
XCTAssertTrue(
|
|
navigationTitle.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
|
|
"Group Polls navigation title should exist"
|
|
)
|
|
}
|
|
|
|
func assertEmpty() {
|
|
XCTAssertTrue(
|
|
emptyState.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
|
|
"Polls empty state should be visible"
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Paywall Screen
|
|
|
|
struct PaywallScreen {
|
|
let app: XCUIApplication
|
|
|
|
// MARK: Elements
|
|
|
|
var upgradeTitle: XCUIElement {
|
|
app.staticTexts["paywall.title"]
|
|
}
|
|
|
|
var unlimitedTripsPill: XCUIElement {
|
|
app.staticTexts["Unlimited Trips"]
|
|
}
|
|
|
|
var pdfExportPill: XCUIElement {
|
|
app.staticTexts["PDF Export"]
|
|
}
|
|
|
|
var progressPill: XCUIElement {
|
|
app.staticTexts["Progress"]
|
|
}
|
|
|
|
// MARK: Actions
|
|
|
|
@discardableResult
|
|
func waitForLoad() -> PaywallScreen {
|
|
upgradeTitle.waitForExistenceOrFail(
|
|
timeout: BaseUITestCase.defaultTimeout,
|
|
"Paywall should appear with 'Upgrade to Pro' title"
|
|
)
|
|
return self
|
|
}
|
|
|
|
// MARK: Assertions
|
|
|
|
func assertLoaded() {
|
|
XCTAssertTrue(
|
|
upgradeTitle.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
|
|
"Paywall title should exist"
|
|
)
|
|
}
|
|
|
|
func assertFeaturePillsVisible() {
|
|
XCTAssertTrue(unlimitedTripsPill.exists, "'Unlimited Trips' pill should exist")
|
|
XCTAssertTrue(pdfExportPill.exists, "'PDF Export' pill should exist")
|
|
}
|
|
}
|
|
|
|
// MARK: - Shared Test Flows
|
|
|
|
/// Reusable multi-step flows that multiple tests share.
|
|
/// Avoids duplicating the full wizard sequence across test files.
|
|
enum TestFlows {
|
|
|
|
/// Opens the wizard, plans a date-range trip (June 11-16 2026, MLB, Central), and
|
|
/// waits for the Trip Options screen to load.
|
|
///
|
|
/// Returns the screens needed to continue interacting with results.
|
|
@discardableResult
|
|
static func planDateRangeTrip(
|
|
app: XCUIApplication,
|
|
month: String = "June",
|
|
year: String = "2026",
|
|
startDay: String = "2026-06-11",
|
|
endDay: String = "2026-06-16",
|
|
sport: String = "mlb",
|
|
region: String = "central"
|
|
) -> (wizard: TripWizardScreen, options: TripOptionsScreen) {
|
|
let home = HomeScreen(app: app)
|
|
home.waitForLoad()
|
|
home.tapStartPlanning()
|
|
|
|
let wizard = TripWizardScreen(app: app)
|
|
wizard.waitForLoad()
|
|
wizard.selectDateRangeMode()
|
|
|
|
wizard.nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
wizard.selectDateRange(
|
|
targetMonth: month,
|
|
targetYear: year,
|
|
startDay: startDay,
|
|
endDay: endDay
|
|
)
|
|
wizard.selectSport(sport)
|
|
wizard.selectRegion(region)
|
|
wizard.tapPlanTrip()
|
|
|
|
let options = TripOptionsScreen(app: app)
|
|
options.waitForLoad()
|
|
options.assertHasResults()
|
|
|
|
return (wizard, options)
|
|
}
|
|
|
|
/// Plans a trip, selects the first option, and navigates to the detail screen.
|
|
@discardableResult
|
|
static func planAndSelectFirstTrip(
|
|
app: XCUIApplication
|
|
) -> (wizard: TripWizardScreen, detail: TripDetailScreen) {
|
|
let (wizard, options) = planDateRangeTrip(app: app)
|
|
options.selectTrip(at: 0)
|
|
|
|
let detail = TripDetailScreen(app: app)
|
|
detail.waitForLoad()
|
|
|
|
return (wizard, detail)
|
|
}
|
|
}
|