From 8421b23f0c0e07e805a81ff99fbb09e38bd283a4 Mon Sep 17 00:00:00 2001 From: Trey t Date: Thu, 19 Feb 2026 23:02:52 -0600 Subject: [PATCH] Add F-100, F-101, F-106 UI tests and page objects for Progress feature Adds 3 new UI tests covering stadium visit manual entry, required field validation, and games history navigation. Includes accessibility IDs on StadiumVisitSheet/ProgressTabView and new page objects (StadiumVisitSheetScreen, GamesHistoryScreen) in the test framework. Co-Authored-By: Claude Opus 4.6 --- .../Progress/Views/ProgressTabView.swift | 1 + .../Progress/Views/StadiumVisitSheet.swift | 3 + SportsTimeUITests/Framework/Screens.swift | 103 +++++++++++++++ SportsTimeUITests/Tests/ProgressTests.swift | 123 +++++++++++++++++- 4 files changed, 229 insertions(+), 1 deletion(-) diff --git a/SportsTime/Features/Progress/Views/ProgressTabView.swift b/SportsTime/Features/Progress/Views/ProgressTabView.swift index 261b0c3..0552f60 100644 --- a/SportsTime/Features/Progress/Views/ProgressTabView.swift +++ b/SportsTime/Features/Progress/Views/ProgressTabView.swift @@ -413,6 +413,7 @@ struct ProgressTabView: View { .font(.subheadline) .foregroundStyle(Theme.warmOrange) } + .accessibilityIdentifier("progress.seeAllGamesHistory") } ForEach(viewModel.recentVisits) { visitSummary in diff --git a/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift b/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift index 2187baa..86ed35a 100644 --- a/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift +++ b/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift @@ -114,6 +114,7 @@ struct StadiumVisitSheet: View { .foregroundStyle(Theme.textMuted(colorScheme)) } } + .accessibilityIdentifier("visitSheet.stadiumButton") .accessibilityLabel(selectedStadium != nil ? "Stadium: \(selectedStadium!.name)" : "Select stadium") .accessibilityHint("Opens stadium picker") } header: { @@ -314,6 +315,7 @@ struct StadiumVisitSheet: View { } .disabled(!canSave || isSaving) .fontWeight(.semibold) + .accessibilityIdentifier("visitSheet.saveButton") } } .sheet(isPresented: $showStadiumPicker) { @@ -488,6 +490,7 @@ struct StadiumPickerSheet: View { } } } + .accessibilityIdentifier("stadiumPicker.stadiumRow") .listRowBackground(Theme.cardBackground(colorScheme)) } .scrollDismissesKeyboard(.interactively) diff --git a/SportsTimeUITests/Framework/Screens.swift b/SportsTimeUITests/Framework/Screens.swift index 0830e5c..b6351a7 100644 --- a/SportsTimeUITests/Framework/Screens.swift +++ b/SportsTimeUITests/Framework/Screens.swift @@ -690,6 +690,16 @@ struct ProgressScreen { app.buttons["progress.sport.\(sport)"] } + var seeAllGamesHistoryButton: XCUIElement { + // NavigationLink may render as button or other element on iOS 26 + let byIdentifier = app.buttons["progress.seeAllGamesHistory"] + if byIdentifier.exists { return byIdentifier } + // Fallback: match by label text + return app.buttons.matching(NSPredicate( + format: "label CONTAINS[c] 'See All'" + )).firstMatch + } + // MARK: Actions @discardableResult @@ -701,6 +711,13 @@ struct ProgressScreen { return self } + /// Opens the "Add Visit" menu and taps "Manual Entry". + func tapAddManualVisit() { + addVisitButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap() + let manualEntry = app.buttons["Manual Entry"] + manualEntry.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap() + } + // MARK: Assertions func assertLoaded() { @@ -716,6 +733,92 @@ struct ProgressScreen { } } +// MARK: - Stadium Visit Sheet Screen + +struct StadiumVisitSheetScreen { + let app: XCUIApplication + + // MARK: Elements + + var navigationBar: XCUIElement { + app.navigationBars["Log Visit"] + } + + var saveButton: XCUIElement { + app.buttons["visitSheet.saveButton"] + } + + var cancelButton: XCUIElement { + navigationBar.buttons["Cancel"] + } + + var stadiumButton: XCUIElement { + app.buttons["visitSheet.stadiumButton"] + } + + // MARK: Actions + + @discardableResult + func waitForLoad() -> StadiumVisitSheetScreen { + navigationBar.waitForExistenceOrFail( + timeout: BaseUITestCase.defaultTimeout, + "Log Visit sheet should appear" + ) + return self + } + + /// Opens the stadium picker and selects the first stadium in the list. + func pickFirstStadium() { + stadiumButton.waitUntilHittable().tap() + + // Wait for picker to appear + let pickerNav = app.navigationBars["Select Stadium"] + pickerNav.waitForExistenceOrFail( + timeout: BaseUITestCase.defaultTimeout, + "Stadium picker should appear" + ) + + // Tap the first stadium row + let firstRow = app.buttons["stadiumPicker.stadiumRow"].firstMatch + firstRow.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap() + } + + func tapSave() { + saveButton.waitUntilHittable().tap() + } + + func tapCancel() { + cancelButton.waitUntilHittable().tap() + } +} + +// MARK: - Games History Screen + +struct GamesHistoryScreen { + let app: XCUIApplication + + // MARK: Elements + + var navigationBar: XCUIElement { + app.navigationBars["Games Attended"] + } + + var emptyStateText: XCUIElement { + app.staticTexts["No games recorded yet"] + } + + // MARK: Actions + + @discardableResult + func waitForLoad() -> GamesHistoryScreen { + navigationBar.waitForExistenceOrFail( + timeout: BaseUITestCase.defaultTimeout, + "Games History should load" + ) + return self + } +} + // MARK: - Polls Screen struct PollsScreen { diff --git a/SportsTimeUITests/Tests/ProgressTests.swift b/SportsTimeUITests/Tests/ProgressTests.swift index f7d00c2..f4a08c5 100644 --- a/SportsTimeUITests/Tests/ProgressTests.swift +++ b/SportsTimeUITests/Tests/ProgressTests.swift @@ -3,7 +3,7 @@ // SportsTimeUITests // // Tests for the Progress tab (Pro-gated). -// QA Sheet: F-095, F-097, F-110 +// QA Sheet: F-066, F-097, F-100, F-101, F-106, F-110 // import XCTest @@ -61,6 +61,127 @@ final class ProgressTests: BaseUITestCase { } } + /// F-101: Add visit — Save button disabled until stadium is selected. + @MainActor + func testF101_AddVisitRequiredFields() { + let home = HomeScreen(app: app) + home.waitForLoad() + home.switchToTab(home.progressTab) + + let progress = ProgressScreen(app: app) + progress.waitForLoad() + progress.tapAddManualVisit() + + let visitSheet = StadiumVisitSheetScreen(app: app) + visitSheet.waitForLoad() + + // Save button should exist but be disabled (no stadium selected) + XCTAssertTrue( + visitSheet.saveButton.waitForExistence(timeout: BaseUITestCase.shortTimeout), + "Save button should exist" + ) + XCTAssertFalse( + visitSheet.saveButton.isEnabled, + "Save button should be disabled without a stadium selected" + ) + + captureScreenshot(named: "F101-SaveDisabledNoStadium") + } + + /// F-100: Add visit — full manual entry flow (select stadium, save). + @MainActor + func testF100_AddVisitManualEntry() { + let home = HomeScreen(app: app) + home.waitForLoad() + home.switchToTab(home.progressTab) + + let progress = ProgressScreen(app: app) + progress.waitForLoad() + progress.tapAddManualVisit() + + let visitSheet = StadiumVisitSheetScreen(app: app) + visitSheet.waitForLoad() + + // Pick a stadium + visitSheet.pickFirstStadium() + + // Save button should now be enabled + XCTAssertTrue( + visitSheet.saveButton.waitForExistence(timeout: BaseUITestCase.shortTimeout), + "Save button should exist after picking a stadium" + ) + XCTAssertTrue( + visitSheet.saveButton.isEnabled, + "Save button should be enabled after selecting a stadium" + ) + + // Save the visit + visitSheet.tapSave() + + // Sheet should dismiss — nav bar disappears + visitSheet.navigationBar.waitForNonExistence( + timeout: BaseUITestCase.defaultTimeout, + "Visit sheet should dismiss after save" + ) + + captureScreenshot(named: "F100-ManualEntryComplete") + } + + /// F-106: View Games History — add a visit, then navigate to history view. + @MainActor + func testF106_ViewGamesHistory() { + let home = HomeScreen(app: app) + home.waitForLoad() + home.switchToTab(home.progressTab) + + let progress = ProgressScreen(app: app) + progress.waitForLoad() + + // Add a visit first (prerequisite: Recent Visits section only shows with visits) + progress.tapAddManualVisit() + let visitSheet = StadiumVisitSheetScreen(app: app) + visitSheet.waitForLoad() + visitSheet.pickFirstStadium() + visitSheet.tapSave() + visitSheet.navigationBar.waitForNonExistence(timeout: BaseUITestCase.defaultTimeout) + + // Wait for data reload — Recent Visits section should appear in hierarchy + XCTAssertTrue( + progress.recentVisitsTitle.waitForExistence(timeout: BaseUITestCase.longTimeout), + "Recent Visits section should appear after adding a visit" + ) + + // Scroll to Recent Visits (same pattern as F-110 which uses app.swipeUp) + var scrollAttempts = 0 + while !progress.recentVisitsTitle.isHittable && scrollAttempts < 20 { + app.swipeUp(velocity: .slow) + scrollAttempts += 1 + } + + // Find and tap "See All" — try button, link, then generic element + let seeAllPredicate = NSPredicate(format: "label CONTAINS[c] 'See All'") + let seeAllButton = app.buttons.matching(seeAllPredicate).firstMatch + let seeAllGeneric = app.descendants(matching: .any).matching(seeAllPredicate).firstMatch + + if seeAllButton.exists && seeAllButton.isHittable { + seeAllButton.tap() + } else if seeAllGeneric.exists { + // One more scroll to ensure it's on screen + if !seeAllGeneric.isHittable { + app.swipeUp(velocity: .slow) + } + seeAllGeneric.tap() + } else { + XCTFail("Could not find 'See All' link in Recent Visits section") + } + + // Verify Games History view loads + let gamesHistory = GamesHistoryScreen(app: app) + gamesHistory.waitForLoad() + + captureScreenshot(named: "F106-GamesHistory") + } + /// F-110: Achievements gallery is visible with badge grid. @MainActor func testF110_AchievementsGalleryVisible() {