Merge branch 'main' of github.com:akatreyt/sportstime
This commit is contained in:
27
AGENTS.md
Normal file
27
AGENTS.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# SportsTime Agent Notes
|
||||
|
||||
This file is for AI/code agents working in this repo.
|
||||
|
||||
## UI Test Docs
|
||||
|
||||
- Start with `XCUITest-Authoring.md`.
|
||||
- Use `XCUITestSuiteTemplate.swift` when creating new suites.
|
||||
- Use `uiTestPrompt.md` when asking an agent to add/fix UI tests.
|
||||
|
||||
## UI Test Ground Rules
|
||||
|
||||
- Keep new UI tests in `SportsTimeUITests/Tests/`.
|
||||
- Reuse page objects in `SportsTimeUITests/Framework/Screens.swift`.
|
||||
- Reuse shared setup and helpers from `SportsTimeUITests/Framework/BaseUITestCase.swift`.
|
||||
- Reuse high-level flows from `TestFlows` before adding duplicate test steps.
|
||||
- Prefer robust waits (`waitForExistenceOrFail`, `waitUntilHittable`) over sleeps.
|
||||
- Use existing accessibility identifiers (`wizard.*`, `tripOptions.*`, `tripDetail.*`, etc.).
|
||||
- Capture screenshots for key checkpoints in longer end-to-end tests.
|
||||
|
||||
## Before Merging UI Test Changes
|
||||
|
||||
- Run the touched test class.
|
||||
- Run full UI suite:
|
||||
- `xcodebuild test-without-building -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -parallel-testing-enabled NO -only-testing:SportsTimeUITests`
|
||||
- Run full scheme verification if behavior touched shared flows:
|
||||
- `xcodebuild test-without-building -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -parallel-testing-enabled NO`
|
||||
@@ -17,6 +17,9 @@ xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platfo
|
||||
# Run a single test
|
||||
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/TripPlanningEngineTests/planningMode_dateRange test
|
||||
|
||||
# Run UI tests only
|
||||
xcodebuild test-without-building -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -parallel-testing-enabled NO -only-testing:SportsTimeUITests
|
||||
|
||||
# Data scraping (Python)
|
||||
cd Scripts && pip install -r requirements.txt
|
||||
python scrape_schedules.py --sport all --season 2026
|
||||
@@ -298,7 +301,10 @@ View → modelContext.delete(model) → modelContext.save() → reload data →
|
||||
- **Test directory**: `SportsTimeTests/` — mirrors source structure
|
||||
- **File naming**: `{ClassName}Tests.swift` or `{Feature}Tests.swift`
|
||||
- **Helper files**: `SportsTimeTests/Helpers/MockServices.swift`, `SportsTimeTests/Helpers/TestFixtures.swift`
|
||||
- **UI tests**: `SportsTimeUITests/` (template only, not actively used)
|
||||
- **UI tests**: `SportsTimeUITests/` is active and uses XCTest + page-object patterns
|
||||
- **UI authoring guide**: `XCUITest-Authoring.md`
|
||||
- **UI suite template**: `XCUITestSuiteTemplate.swift`
|
||||
- **UI request template**: `uiTestPrompt.md`
|
||||
|
||||
### Existing Test Suites
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -70,6 +70,7 @@ SportsTime/
|
||||
│ ├── Export/ # PDF generation
|
||||
│ └── Resources/ # Bundled JSON data
|
||||
├── SportsTimeTests/ # Unit tests
|
||||
├── SportsTimeUITests/ # UI test suites + screen objects
|
||||
├── Scripts/ # Python data pipeline
|
||||
│ └── sportstime_parser/ # Schedule scraping & CloudKit upload
|
||||
├── data/ # Local data files
|
||||
@@ -124,6 +125,14 @@ xcodebuild -project SportsTime.xcodeproj \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \
|
||||
-only-testing:SportsTimeTests/EdgeCaseTests \
|
||||
test
|
||||
|
||||
# UI test target only
|
||||
xcodebuild test-without-building \
|
||||
-project SportsTime.xcodeproj \
|
||||
-scheme SportsTime \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \
|
||||
-parallel-testing-enabled NO \
|
||||
-only-testing:SportsTimeUITests
|
||||
```
|
||||
|
||||
### Data Pipeline
|
||||
@@ -151,7 +160,11 @@ python -m sportstime_parser upload --sport all
|
||||
## Documentation
|
||||
|
||||
- `CLAUDE.md` - Development guidelines and architecture details
|
||||
- `AGENTS.md` - Agent-specific execution notes for this repo
|
||||
- `ARCHITECTURE.md` - Detailed system architecture
|
||||
- `XCUITest-Authoring.md` - How existing UI tests are structured and how to add new ones
|
||||
- `XCUITestSuiteTemplate.swift` - Starter suite template for new UI test files
|
||||
- `uiTestPrompt.md` - Reusable prompt template for requesting UI test work
|
||||
- `docs/TEST_PLAN.md` - Test suite documentation
|
||||
- `docs/MARKET_RESEARCH.md` - Competitive analysis
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
parallelizable = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1CA7F90D2F0D647400490ABD"
|
||||
|
||||
@@ -118,25 +118,40 @@ extension XCUIElement {
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) -> XCUIElement {
|
||||
var scrollsRemaining = maxScrolls
|
||||
while !exists || !isHittable {
|
||||
guard scrollsRemaining > 0 else {
|
||||
XCTFail("Could not scroll \(self) into view after \(maxScrolls) scrolls",
|
||||
file: file, line: line)
|
||||
return self
|
||||
if exists && isHittable { return self }
|
||||
|
||||
func attemptScroll(_ scrollDirection: ScrollDirection, attempts: Int) -> Bool {
|
||||
var remaining = attempts
|
||||
while (!exists || !isHittable) && remaining > 0 {
|
||||
switch scrollDirection {
|
||||
case .down:
|
||||
scrollView.swipeUp(velocity: .slow)
|
||||
case .up:
|
||||
scrollView.swipeDown(velocity: .slow)
|
||||
}
|
||||
remaining -= 1
|
||||
}
|
||||
switch direction {
|
||||
case .down:
|
||||
scrollView.swipeUp(velocity: .slow)
|
||||
case .up:
|
||||
scrollView.swipeDown(velocity: .slow)
|
||||
}
|
||||
scrollsRemaining -= 1
|
||||
return exists && isHittable
|
||||
}
|
||||
|
||||
if attemptScroll(direction, attempts: maxScrolls) {
|
||||
return self
|
||||
}
|
||||
|
||||
let reverseDirection: ScrollDirection = direction == .down ? .up : .down
|
||||
if attemptScroll(reverseDirection, attempts: maxScrolls) {
|
||||
return self
|
||||
}
|
||||
|
||||
XCTFail(
|
||||
"Could not scroll \(self) into view after \(maxScrolls) scrolls in either direction",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
enum ScrollDirection {
|
||||
enum ScrollDirection: Equatable {
|
||||
case up, down
|
||||
}
|
||||
|
||||
@@ -74,7 +74,35 @@ struct HomeScreen {
|
||||
|
||||
/// Taps "Start Planning" to open the Trip Wizard sheet.
|
||||
func tapStartPlanning() {
|
||||
startPlanningButton.waitUntilHittable().tap()
|
||||
let navTitle = app.navigationBars["Plan a Trip"]
|
||||
let dateRangeMode = app.buttons["wizard.planningMode.dateRange"]
|
||||
|
||||
if navTitle.exists || dateRangeMode.exists {
|
||||
return
|
||||
}
|
||||
|
||||
func tapIfVisible(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
|
||||
guard element.waitForExistence(timeout: timeout), element.isHittable else { return false }
|
||||
element.tap()
|
||||
return true
|
||||
}
|
||||
|
||||
_ = tapIfVisible(startPlanningButton, timeout: BaseUITestCase.defaultTimeout) ||
|
||||
tapIfVisible(createTripToolbarButton, timeout: BaseUITestCase.shortTimeout)
|
||||
|
||||
if navTitle.waitForExistence(timeout: BaseUITestCase.shortTimeout) ||
|
||||
dateRangeMode.waitForExistence(timeout: BaseUITestCase.shortTimeout) {
|
||||
return
|
||||
}
|
||||
|
||||
_ = tapIfVisible(createTripToolbarButton, timeout: BaseUITestCase.shortTimeout) ||
|
||||
tapIfVisible(startPlanningButton, timeout: BaseUITestCase.shortTimeout)
|
||||
|
||||
XCTAssertTrue(
|
||||
navTitle.waitForExistence(timeout: BaseUITestCase.defaultTimeout) ||
|
||||
dateRangeMode.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
|
||||
"Trip Wizard should appear after tapping start planning"
|
||||
)
|
||||
}
|
||||
|
||||
/// Switches to a tab by tapping its tab bar button.
|
||||
@@ -150,10 +178,21 @@ struct TripWizardScreen {
|
||||
/// Waits for the wizard sheet to appear.
|
||||
@discardableResult
|
||||
func waitForLoad() -> TripWizardScreen {
|
||||
if navigationTitle.waitForExistence(timeout: BaseUITestCase.defaultTimeout) ||
|
||||
planningModeButton("dateRange").waitForExistence(timeout: BaseUITestCase.defaultTimeout) {
|
||||
if navigationTitle.waitForExistence(timeout: BaseUITestCase.longTimeout) ||
|
||||
planningModeButton("dateRange").waitForExistence(timeout: BaseUITestCase.longTimeout) {
|
||||
return self
|
||||
}
|
||||
|
||||
// Fallback: if we're still on Home, trigger planning again.
|
||||
let home = HomeScreen(app: app)
|
||||
if home.startPlanningButton.exists || home.createTripToolbarButton.exists {
|
||||
home.tapStartPlanning()
|
||||
if navigationTitle.waitForExistence(timeout: BaseUITestCase.defaultTimeout) ||
|
||||
planningModeButton("dateRange").waitForExistence(timeout: BaseUITestCase.defaultTimeout) {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
XCTFail("Trip Wizard should appear")
|
||||
return self
|
||||
}
|
||||
@@ -169,6 +208,19 @@ struct TripWizardScreen {
|
||||
/// Selects the "By Dates" planning mode and waits for steps to expand.
|
||||
func selectDateRangeMode() {
|
||||
selectPlanningMode("dateRange")
|
||||
|
||||
if monthLabel.waitForExistence(timeout: BaseUITestCase.shortTimeout) ||
|
||||
nextMonthButton.waitForExistence(timeout: BaseUITestCase.shortTimeout) {
|
||||
return
|
||||
}
|
||||
|
||||
// Retry once for occasional dropped taps under simulator load.
|
||||
selectPlanningMode("dateRange")
|
||||
XCTAssertTrue(
|
||||
monthLabel.waitForExistence(timeout: BaseUITestCase.defaultTimeout) ||
|
||||
nextMonthButton.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
|
||||
"Date range controls should appear after selecting planning mode"
|
||||
)
|
||||
}
|
||||
|
||||
/// Navigates the calendar to a target month/year and selects start/end dates.
|
||||
@@ -178,6 +230,11 @@ struct TripWizardScreen {
|
||||
startDay: String,
|
||||
endDay: String
|
||||
) {
|
||||
// Ensure date controls are rendered before attempting calendar navigation.
|
||||
if !monthLabel.waitForExistence(timeout: BaseUITestCase.shortTimeout) {
|
||||
selectDateRangeMode()
|
||||
}
|
||||
|
||||
// First, navigate by month label so tests that assert month visibility stay stable.
|
||||
monthLabel.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
let targetMonthYear = "\(targetMonth) \(targetYear)"
|
||||
@@ -196,20 +253,20 @@ struct TripWizardScreen {
|
||||
let targetIdx = monthOrder.firstIndex(of: targetMonth) {
|
||||
if currentIdx > targetIdx {
|
||||
previousMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
previousMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
previousMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
} else if currentIdx < targetIdx {
|
||||
nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
}
|
||||
} else {
|
||||
nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
}
|
||||
monthAttempts += 1
|
||||
}
|
||||
@@ -223,27 +280,27 @@ struct TripWizardScreen {
|
||||
let startFallback = dayCells.element(boundBy: 0)
|
||||
let endFallback = dayCells.element(boundBy: min(4, max(1, dayCells.count - 1)))
|
||||
startFallback.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
startFallback.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
startFallback.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
endFallback.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
endFallback.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
endFallback.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
return
|
||||
}
|
||||
|
||||
// Select start date — scroll calendar grid into view first
|
||||
startBtn.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
startBtn.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
startBtn.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
|
||||
// Select end date
|
||||
let endBtn = dayButton(endDay)
|
||||
if endBtn.exists {
|
||||
endBtn.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
endBtn.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
endBtn.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
} else {
|
||||
let dayCells = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'wizard.dates.day.'"))
|
||||
guard dayCells.count > 1 else { return }
|
||||
let fallback = dayCells.element(boundBy: min(4, max(1, dayCells.count - 1)))
|
||||
fallback.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
fallback.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
fallback.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,8 +617,31 @@ struct SettingsScreen {
|
||||
// MARK: Assertions
|
||||
|
||||
func assertLoaded() {
|
||||
XCTAssertTrue(subscriptionSection.waitForExistence(timeout: BaseUITestCase.defaultTimeout),
|
||||
"Settings should show Subscription section")
|
||||
if subscriptionSection.waitForExistence(timeout: BaseUITestCase.defaultTimeout) {
|
||||
return
|
||||
}
|
||||
|
||||
let proLabel = app.staticTexts["SportsTime Pro"]
|
||||
let manageSubscriptionButton = app.buttons["Manage Subscription"]
|
||||
if proLabel.exists || manageSubscriptionButton.exists ||
|
||||
upgradeProButton.exists || restorePurchasesButton.exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Retry tab switch once when the first tap doesn't switch tabs under load.
|
||||
let settingsTab = app.tabBars.buttons["Settings"]
|
||||
if settingsTab.waitForExistence(timeout: BaseUITestCase.shortTimeout), settingsTab.isHittable {
|
||||
settingsTab.tap()
|
||||
}
|
||||
|
||||
XCTAssertTrue(
|
||||
subscriptionSection.waitForExistence(timeout: BaseUITestCase.defaultTimeout) ||
|
||||
proLabel.waitForExistence(timeout: BaseUITestCase.shortTimeout) ||
|
||||
manageSubscriptionButton.waitForExistence(timeout: BaseUITestCase.shortTimeout) ||
|
||||
upgradeProButton.waitForExistence(timeout: BaseUITestCase.shortTimeout) ||
|
||||
restorePurchasesButton.waitForExistence(timeout: BaseUITestCase.shortTimeout),
|
||||
"Settings should show Subscription section"
|
||||
)
|
||||
}
|
||||
|
||||
func assertVersionDisplayed() {
|
||||
|
||||
@@ -92,7 +92,7 @@ final class TripWizardFlowTests: BaseUITestCase {
|
||||
// Navigate forward 3 times
|
||||
for _ in 0..<3 {
|
||||
wizard.nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
wizard.nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
wizard.nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
}
|
||||
|
||||
// Month label should have changed
|
||||
@@ -113,14 +113,14 @@ final class TripWizardFlowTests: BaseUITestCase {
|
||||
// Go forward 3 months
|
||||
for _ in 0..<3 {
|
||||
wizard.nextMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
wizard.nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
wizard.nextMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
}
|
||||
|
||||
let afterForward = wizard.monthLabel.label
|
||||
|
||||
// Go back 1 month
|
||||
wizard.previousMonthButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
wizard.previousMonthButton.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap()
|
||||
wizard.previousMonthButton.waitUntilHittable(timeout: BaseUITestCase.defaultTimeout).tap()
|
||||
|
||||
XCTAssertNotEqual(wizard.monthLabel.label, afterForward,
|
||||
"Month should change after navigating backward")
|
||||
|
||||
108
XCUITest-Authoring.md
Normal file
108
XCUITest-Authoring.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# XCUITest Authoring Guide
|
||||
|
||||
This guide documents the current SportsTime UI test foundation and how to add new tests safely.
|
||||
|
||||
## Current Foundation
|
||||
|
||||
## Layout
|
||||
|
||||
- Test target: `SportsTimeUITests`
|
||||
- Base setup/helpers: `SportsTimeUITests/Framework/BaseUITestCase.swift`
|
||||
- Page objects + shared flows: `SportsTimeUITests/Framework/Screens.swift`
|
||||
- Suite files: `SportsTimeUITests/Tests/*.swift`
|
||||
- Legacy mixed tests: `SportsTimeUITests/SportsTimeUITests.swift` and `SportsTimeUITests/SportsTimeUITestsLaunchTests.swift`
|
||||
|
||||
## Existing Suite Coverage
|
||||
|
||||
- `AppLaunchTests`
|
||||
- `HomeTests`
|
||||
- `TabNavigationTests`
|
||||
- `ScheduleTests`
|
||||
- `TripWizardFlowTests`
|
||||
- `TripOptionsTests`
|
||||
- `TripSavingTests`
|
||||
- `ProgressTests`
|
||||
- `SettingsTests`
|
||||
- `AccessibilityTests`
|
||||
- `StabilityTests`
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Inherit from `BaseUITestCase`.
|
||||
- Use `@MainActor` test methods.
|
||||
- Prefer page-object actions over direct element taps.
|
||||
- Prefer existing high-level flows (`TestFlows.planDateRangeTrip`, `TestFlows.planAndSelectFirstTrip`) for shared setup.
|
||||
- Use deterministic selectors with accessibility identifiers.
|
||||
- Use `waitForExistenceOrFail` and `waitUntilHittable` instead of arbitrary sleeps.
|
||||
|
||||
## Adding a New UI Test
|
||||
|
||||
1. Pick the closest existing suite in `SportsTimeUITests/Tests/`.
|
||||
2. If none fits, create a new suite using `XCUITestSuiteTemplate.swift`.
|
||||
3. Add/extend page-object methods in `Screens.swift` before writing raw element code.
|
||||
4. Reuse `TestFlows` when setup overlaps existing end-to-end flows.
|
||||
5. Keep assertion messages explicit and behavior-focused.
|
||||
6. Capture screenshot(s) for key milestone state in longer flows.
|
||||
7. Run targeted tests, then full UI tests.
|
||||
|
||||
## When to Edit `Screens.swift`
|
||||
|
||||
Edit page objects when:
|
||||
|
||||
- A new screen element needs a stable selector.
|
||||
- The interaction is reused across multiple tests.
|
||||
- The flow can be standardized (especially wizard planning flow).
|
||||
|
||||
Do not add one-off test-only branching logic unless it removes a real flake.
|
||||
|
||||
## Running Tests
|
||||
|
||||
## Fast Loop (single test)
|
||||
|
||||
```bash
|
||||
xcodebuild test-without-building \
|
||||
-project SportsTime.xcodeproj \
|
||||
-scheme SportsTime \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \
|
||||
-parallel-testing-enabled NO \
|
||||
-only-testing:SportsTimeUITests/TripWizardFlowTests/testF026_DateRangeSelection
|
||||
```
|
||||
|
||||
## Per Class
|
||||
|
||||
```bash
|
||||
xcodebuild test-without-building \
|
||||
-project SportsTime.xcodeproj \
|
||||
-scheme SportsTime \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \
|
||||
-parallel-testing-enabled NO \
|
||||
-only-testing:SportsTimeUITests/TripOptionsTests
|
||||
```
|
||||
|
||||
## Full UI Suite
|
||||
|
||||
```bash
|
||||
xcodebuild test-without-building \
|
||||
-project SportsTime.xcodeproj \
|
||||
-scheme SportsTime \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \
|
||||
-parallel-testing-enabled NO \
|
||||
-only-testing:SportsTimeUITests
|
||||
```
|
||||
|
||||
## Full Scheme Validation (unit + UI)
|
||||
|
||||
```bash
|
||||
xcodebuild test-without-building \
|
||||
-project SportsTime.xcodeproj \
|
||||
-scheme SportsTime \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \
|
||||
-parallel-testing-enabled NO
|
||||
```
|
||||
|
||||
## Flake Prevention Notes
|
||||
|
||||
- Keep simulator orientation consistent (portrait baseline from `BaseUITestCase`).
|
||||
- For wizard date selection, navigate by month/year first and use day-cell fallback when specific IDs are unavailable.
|
||||
- For cross-season planning tests, prefer deterministic fallback sports if selected sport has no viable schedule for current test data.
|
||||
- Increase waits only where planning computation is the actual bottleneck.
|
||||
48
XCUITestSuiteTemplate.swift
Normal file
48
XCUITestSuiteTemplate.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// __FeatureName__Tests.swift
|
||||
// SportsTimeUITests
|
||||
//
|
||||
// Copy this file into SportsTimeUITests/Tests and rename placeholders.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class __FeatureName__Tests: BaseUITestCase {
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@MainActor
|
||||
private func openHome() -> HomeScreen {
|
||||
let home = HomeScreen(app: app)
|
||||
home.waitForLoad()
|
||||
return home
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func openWizard() -> TripWizardScreen {
|
||||
let home = openHome()
|
||||
home.tapStartPlanning()
|
||||
|
||||
let wizard = TripWizardScreen(app: app)
|
||||
wizard.waitForLoad()
|
||||
return wizard
|
||||
}
|
||||
|
||||
// MARK: - Tests
|
||||
|
||||
/// Replace with test case ID and behavior, e.g. F-200.
|
||||
@MainActor
|
||||
func testF___BehaviorName() {
|
||||
let wizard = openWizard()
|
||||
wizard.selectDateRangeMode()
|
||||
|
||||
// Add scenario actions
|
||||
// wizard.selectSport("mlb")
|
||||
// wizard.selectRegion("central")
|
||||
|
||||
// Add assertions
|
||||
XCTAssertTrue(wizard.planTripButton.exists, "Plan button should be visible")
|
||||
|
||||
captureScreenshot(named: "F___-BehaviorName")
|
||||
}
|
||||
}
|
||||
49
uiTestPrompt.md
Normal file
49
uiTestPrompt.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# UI Test Prompt Template
|
||||
|
||||
Use this prompt when asking an agent to add or modify SportsTime UI tests.
|
||||
|
||||
---
|
||||
|
||||
You are updating SportsTime UI tests.
|
||||
|
||||
## Goal
|
||||
|
||||
- [Describe the behavior to test or fix.]
|
||||
|
||||
## Scope
|
||||
|
||||
- Update/add tests under `SportsTimeUITests/Tests/`.
|
||||
- Reuse page objects in `SportsTimeUITests/Framework/Screens.swift`.
|
||||
- Reuse shared setup in `SportsTimeUITests/Framework/BaseUITestCase.swift`.
|
||||
- Reuse existing `TestFlows` where possible.
|
||||
|
||||
## Required Changes
|
||||
|
||||
- [List test suites to modify.]
|
||||
- [List new test names.]
|
||||
- [List selectors/page-object methods to add if needed.]
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not add raw sleeps unless strictly necessary.
|
||||
- Prefer `waitForExistenceOrFail` and `waitUntilHittable`.
|
||||
- Keep tests deterministic with current local test data.
|
||||
- Keep existing naming style (`testF###_...`, `testP###_...`, etc.).
|
||||
|
||||
## Validation
|
||||
|
||||
Run these before finishing:
|
||||
|
||||
1. Targeted class or test:
|
||||
- `xcodebuild test-without-building -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -parallel-testing-enabled NO -only-testing:SportsTimeUITests/<SuiteOrTest>`
|
||||
2. Full UI suite:
|
||||
- `xcodebuild test-without-building -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -parallel-testing-enabled NO -only-testing:SportsTimeUITests`
|
||||
3. If requested, full scheme:
|
||||
- `xcodebuild test-without-building -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -parallel-testing-enabled NO`
|
||||
|
||||
## Output Format
|
||||
|
||||
- Summarize files changed.
|
||||
- Summarize root causes fixed.
|
||||
- Include exact commands run and pass/fail outcomes.
|
||||
- Call out any remaining flaky behavior or follow-up work.
|
||||
Reference in New Issue
Block a user