// // 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["Create new trip"] } // 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 the "By Dates" planning mode and waits for steps to expand. func selectDateRangeMode() { let btn = planningModeButton("dateRange") btn.scrollIntoView(in: app.scrollViews.firstMatch) btn.tap() } /// 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: - 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"] } // 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 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"] } // 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 } // 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"] } // 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") } }