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:
@@ -413,6 +413,7 @@ struct ProgressTabView: View {
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
.accessibilityIdentifier("progress.seeAllGamesHistory")
|
||||
}
|
||||
|
||||
ForEach(viewModel.recentVisits) { visitSummary in
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user