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 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-19 23:02:52 -06:00
parent f10bc4fe59
commit 8421b23f0c
4 changed files with 229 additions and 1 deletions

View File

@@ -413,6 +413,7 @@ struct ProgressTabView: View {
.font(.subheadline)
.foregroundStyle(Theme.warmOrange)
}
.accessibilityIdentifier("progress.seeAllGamesHistory")
}
ForEach(viewModel.recentVisits) { visitSummary in

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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() {