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)
|
.font(.subheadline)
|
||||||
.foregroundStyle(Theme.warmOrange)
|
.foregroundStyle(Theme.warmOrange)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("progress.seeAllGamesHistory")
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(viewModel.recentVisits) { visitSummary in
|
ForEach(viewModel.recentVisits) { visitSummary in
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ struct StadiumVisitSheet: View {
|
|||||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("visitSheet.stadiumButton")
|
||||||
.accessibilityLabel(selectedStadium != nil ? "Stadium: \(selectedStadium!.name)" : "Select stadium")
|
.accessibilityLabel(selectedStadium != nil ? "Stadium: \(selectedStadium!.name)" : "Select stadium")
|
||||||
.accessibilityHint("Opens stadium picker")
|
.accessibilityHint("Opens stadium picker")
|
||||||
} header: {
|
} header: {
|
||||||
@@ -314,6 +315,7 @@ struct StadiumVisitSheet: View {
|
|||||||
}
|
}
|
||||||
.disabled(!canSave || isSaving)
|
.disabled(!canSave || isSaving)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
.accessibilityIdentifier("visitSheet.saveButton")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showStadiumPicker) {
|
.sheet(isPresented: $showStadiumPicker) {
|
||||||
@@ -488,6 +490,7 @@ struct StadiumPickerSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("stadiumPicker.stadiumRow")
|
||||||
.listRowBackground(Theme.cardBackground(colorScheme))
|
.listRowBackground(Theme.cardBackground(colorScheme))
|
||||||
}
|
}
|
||||||
.scrollDismissesKeyboard(.interactively)
|
.scrollDismissesKeyboard(.interactively)
|
||||||
|
|||||||
@@ -690,6 +690,16 @@ struct ProgressScreen {
|
|||||||
app.buttons["progress.sport.\(sport)"]
|
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
|
// MARK: Actions
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
@@ -701,6 +711,13 @@ struct ProgressScreen {
|
|||||||
return self
|
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
|
// MARK: Assertions
|
||||||
|
|
||||||
func assertLoaded() {
|
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
|
// MARK: - Polls Screen
|
||||||
|
|
||||||
struct PollsScreen {
|
struct PollsScreen {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// SportsTimeUITests
|
// SportsTimeUITests
|
||||||
//
|
//
|
||||||
// Tests for the Progress tab (Pro-gated).
|
// 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
|
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.
|
/// F-110: Achievements gallery is visible with badge grid.
|
||||||
@MainActor
|
@MainActor
|
||||||
func testF110_AchievementsGalleryVisible() {
|
func testF110_AchievementsGalleryVisible() {
|
||||||
|
|||||||
Reference in New Issue
Block a user