Harden test harness and UI suite

This commit is contained in:
Trey t
2026-04-03 15:30:54 -05:00
parent 87b9971714
commit 0fa3db5401
13 changed files with 319 additions and 55 deletions

View File

@@ -22,6 +22,6 @@ This file is for AI/code agents working in this repo.
- Run the touched test class. - Run the touched test class.
- Run full UI suite: - 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` - `xcodebuild test-without-building -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' -parallel-testing-enabled NO -only-testing:SportsTimeUITests`
- Run full scheme verification if behavior touched shared flows: - 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` - `xcodebuild test-without-building -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' -parallel-testing-enabled NO`

View File

@@ -6,19 +6,19 @@ iOS app for planning multi-stop sports road trips. Offline-first architecture wi
```bash ```bash
# Build the iOS app # Build the iOS app
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' build
# Run all tests # Run all tests
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' test
# Run specific test suite # Run specific test suite
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/TripPlanningEngineTests test xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' -only-testing:SportsTimeTests/TripPlanningEngineTests test
# Run a single test # 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 xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' -only-testing:SportsTimeTests/TripPlanningEngineTests/planningMode_dateRange test
# Run UI tests only # 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 xcodebuild test-without-building -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' -parallel-testing-enabled NO -only-testing:SportsTimeUITests
# Data scraping (Python) # Data scraping (Python)
cd Scripts && pip install -r requirements.txt cd Scripts && pip install -r requirements.txt

View File

@@ -106,7 +106,7 @@ All schedule data flows through `AppDataProvider.shared` - never access CloudKit
```bash ```bash
xcodebuild -project SportsTime.xcodeproj \ xcodebuild -project SportsTime.xcodeproj \
-scheme SportsTime \ -scheme SportsTime \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \ -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \
build build
``` ```
@@ -116,13 +116,13 @@ xcodebuild -project SportsTime.xcodeproj \
# All tests # All tests
xcodebuild -project SportsTime.xcodeproj \ xcodebuild -project SportsTime.xcodeproj \
-scheme SportsTime \ -scheme SportsTime \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \ -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \
test test
# Specific test suite # Specific test suite
xcodebuild -project SportsTime.xcodeproj \ xcodebuild -project SportsTime.xcodeproj \
-scheme SportsTime \ -scheme SportsTime \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \ -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \
-only-testing:SportsTimeTests/EdgeCaseTests \ -only-testing:SportsTimeTests/EdgeCaseTests \
test test
@@ -130,7 +130,7 @@ xcodebuild -project SportsTime.xcodeproj \
xcodebuild test-without-building \ xcodebuild test-without-building \
-project SportsTime.xcodeproj \ -project SportsTime.xcodeproj \
-scheme SportsTime \ -scheme SportsTime \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \ -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \
-parallel-testing-enabled NO \ -parallel-testing-enabled NO \
-only-testing:SportsTimeUITests -only-testing:SportsTimeUITests
``` ```

View File

@@ -0,0 +1,241 @@
//
// ScenarioEPlannerRealDataTest.swift
// SportsTimeTests
//
// Regression test: runs ScenarioEPlanner with real PHI/WSN/BAL data
// to verify the past-date filtering and full-season coverage fixes.
//
import Foundation
import Testing
import CoreLocation
@testable import SportsTime
@Suite("ScenarioE Real Data Regression")
struct ScenarioEPlannerRealDataTest {
@Test("PHI + WSN + BAL: returns future regular season results, not spring training")
func phiWsnBal_returnsFutureRegularSeasonResults() throws {
let fixture = try FixtureLoader.loadCanonicalGames()
let rawGames = fixture.games
// Filter to MLB only
let mlbRows = rawGames.filter { $0.sport?.lowercased() == "mlb" }
let games = mlbRows.compactMap(\.domainGame)
let skippedMLBRows = mlbRows.count - games.count
#expect(!games.isEmpty, "Expected MLB canonical fixture to contain valid games")
// Team IDs
let teamIds: Set<String> = ["team_mlb_phi", "team_mlb_wsn", "team_mlb_bal"]
// Build stadium map with real coordinates
let stadiums: [String: Stadium] = [
"stadium_mlb_citizens_bank_park": Stadium(
id: "stadium_mlb_citizens_bank_park", name: "Citizens Bank Park",
city: "Philadelphia", state: "PA",
latitude: 39.9061, longitude: -75.1665, capacity: 43035, sport: .mlb
),
"stadium_mlb_nationals_park": Stadium(
id: "stadium_mlb_nationals_park", name: "Nationals Park",
city: "Washington", state: "DC",
latitude: 38.8730, longitude: -77.0074, capacity: 41339, sport: .mlb
),
"stadium_mlb_oriole_park_at_camden_yards": Stadium(
id: "stadium_mlb_oriole_park_at_camden_yards", name: "Oriole Park at Camden Yards",
city: "Baltimore", state: "MD",
latitude: 39.2838, longitude: -76.6216, capacity: 45971, sport: .mlb
),
// Spring training stadiums (should be excluded by date filter)
"stadium_mlb_spring_baycare_ballpark": Stadium(
id: "stadium_mlb_spring_baycare_ballpark", name: "BayCare Ballpark",
city: "Clearwater", state: "FL",
latitude: 27.9781, longitude: -82.7337, capacity: 8500, sport: .mlb
),
"stadium_mlb_spring_cacti_park": Stadium(
id: "stadium_mlb_spring_cacti_park", name: "CACTI Park",
city: "West Palm Beach", state: "FL",
latitude: 26.7367, longitude: -80.1197, capacity: 6671, sport: .mlb
),
"stadium_mlb_spring_ed_smith_stadium": Stadium(
id: "stadium_mlb_spring_ed_smith_stadium", name: "Ed Smith Stadium",
city: "Sarasota", state: "FL",
latitude: 27.3381, longitude: -82.5226, capacity: 8500, sport: .mlb
),
]
// Build team map
let teams: [String: Team] = [
"team_mlb_phi": Team(id: "team_mlb_phi", name: "Phillies", abbreviation: "PHI",
sport: .mlb, city: "Philadelphia",
stadiumId: "stadium_mlb_citizens_bank_park",
primaryColor: "#E81828", secondaryColor: "#002D72"),
"team_mlb_wsn": Team(id: "team_mlb_wsn", name: "Nationals", abbreviation: "WSN",
sport: .mlb, city: "Washington",
stadiumId: "stadium_mlb_nationals_park",
primaryColor: "#AB0003", secondaryColor: "#14225A"),
"team_mlb_bal": Team(id: "team_mlb_bal", name: "Orioles", abbreviation: "BAL",
sport: .mlb, city: "Baltimore",
stadiumId: "stadium_mlb_oriole_park_at_camden_yards",
primaryColor: "#DF4601", secondaryColor: "#000000"),
]
let prefs = TripPreferences(
planningMode: .teamFirst,
sports: [.mlb],
selectedTeamIds: teamIds
)
let request = PlanningRequest(
preferences: prefs,
availableGames: games,
teams: teams,
stadiums: stadiums
)
// Use April 2, 2026 as current date (the date the bug was reported)
let currentDate = TestFixtures.date(year: 2026, month: 4, day: 2, hour: 12)
let planner = ScenarioEPlanner(currentDate: currentDate)
let result = planner.plan(request: request)
guard case .success(let options) = result else {
if case .failure(let failure) = result {
Issue.record("Expected success but got failure: \(failure.reason)\(failure.violations.map(\.description))")
}
return
}
#expect(!options.isEmpty, "Should find trip options for PHI/WSN/BAL")
let calendar = Calendar.current
var output = "\nRESULT> Fixture: \(fixture.url.path)\n"
output += "RESULT> MLB rows: \(mlbRows.count), skipped malformed MLB rows: \(skippedMLBRows)\n"
output += "RESULT> ========================================\n"
output += "RESULT> PHI + WSN + BAL TRIP OPTIONS (\(options.count) results)\n"
output += "RESULT> ========================================\n\n"
for option in options {
let cities = option.stops.map { "\($0.city)" }.joined(separator: "")
let dates = option.stops.map { stop in
let formatter = DateFormatter()
formatter.dateFormat = "MMM d"
return formatter.string(from: stop.arrivalDate)
}.joined(separator: "")
let gameIds = option.stops.flatMap { $0.games }
let miles = Int(option.totalDistanceMiles)
let hours = String(format: "%.1f", option.totalDrivingHours)
output += "RESULT> Option #\(option.rank): \(cities)\n"
output += "RESULT> Dates: \(dates)\n"
output += "RESULT> Driving: \(miles) mi, \(hours) hrs\n"
output += "RESULT> Games: \(gameIds.count)\n"
output += "RESULT> Rationale: \(option.geographicRationale)\n"
// Verify all games are in the future
for stop in option.stops {
#expect(stop.arrivalDate >= calendar.startOfDay(for: currentDate),
"Stop in \(stop.city) on \(stop.arrivalDate) should be after April 2, 2026")
}
// Verify no spring training stadiums
for stop in option.stops {
let isSpringTraining = stop.city == "Clearwater" || stop.city == "Sarasota" || stop.city == "West Palm Beach"
#expect(!isSpringTraining, "Should not include spring training city: \(stop.city)")
}
output += "\n"
}
// Verify temporal spread results should not all be in April
let months = Set(options.flatMap { $0.stops.map { calendar.component(.month, from: $0.arrivalDate) } })
output += "RESULT> Months covered: \(months.sorted().map { DateFormatter().monthSymbols[$0 - 1] })\n"
FileHandle.standardOutput.write(Data(output.utf8))
#expect(months.count >= 2, "Results should span multiple months across the season")
}
}
// MARK: - JSON Decoding Helpers
private struct CanonicalGameJSON: Decodable {
let canonical_id: String?
let sport: String?
let season: String?
let game_datetime_utc: String?
let home_team_canonical_id: String?
let away_team_canonical_id: String?
let stadium_canonical_id: String?
let is_playoff: Bool?
var parsedDate: Date {
guard let game_datetime_utc else { return Date.distantPast }
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: game_datetime_utc) { return date }
formatter.formatOptions = [.withInternetDateTime]
return formatter.date(from: game_datetime_utc) ?? Date.distantPast
}
var domainGame: Game? {
guard let canonical_id,
let home_team_canonical_id,
let away_team_canonical_id,
let stadium_canonical_id,
let season else {
return nil
}
return Game(
id: canonical_id,
homeTeamId: home_team_canonical_id,
awayTeamId: away_team_canonical_id,
stadiumId: stadium_canonical_id,
dateTime: parsedDate,
sport: .mlb,
season: season,
isPlayoff: is_playoff ?? false
)
}
}
private enum FixtureLoader {
struct LoadedFixture {
let url: URL
let games: [CanonicalGameJSON]
}
static func loadCanonicalGames() throws -> LoadedFixture {
let candidateURLs = [
repositoryRoot.appendingPathComponent("sportstime_export/games_canonical.json"),
Bundle(for: BundleToken.self).url(forResource: "games_canonical", withExtension: "json")
].compactMap { $0 }
for url in candidateURLs where FileManager.default.fileExists(atPath: url.path) {
let data = try Data(contentsOf: url)
let games = try JSONDecoder().decode([CanonicalGameJSON].self, from: data)
return LoadedFixture(url: url, games: games)
}
throw FixtureLoadError.notFound(candidateURLs.map(\.path))
}
private static var repositoryRoot: URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
}
}
private enum FixtureLoadError: LocalizedError {
case notFound([String])
var errorDescription: String? {
switch self {
case .notFound(let paths):
let joined = paths.joined(separator: ", ")
return "Could not find games_canonical.json. Checked: \(joined)"
}
}
}
private class BundleToken {}

View File

@@ -59,6 +59,33 @@ class BaseUITestCase: XCTestCase {
screenshot.lifetime = .keepAlways screenshot.lifetime = .keepAlways
add(screenshot) add(screenshot)
} }
/// Polls until the condition becomes true or the timeout expires.
@discardableResult
func waitUntil(
timeout: TimeInterval = BaseUITestCase.defaultTimeout,
pollInterval: TimeInterval = 0.2,
_ message: String? = nil,
file: StaticString = #filePath,
line: UInt = #line,
condition: @escaping () -> Bool
) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if condition() {
return true
}
let remaining = deadline.timeIntervalSinceNow
let interval = min(pollInterval, max(0.01, remaining))
RunLoop.current.run(until: Date().addingTimeInterval(interval))
}
let success = condition()
XCTAssertTrue(success, message ?? "Condition was not met within \(timeout)s", file: file, line: line)
return success
}
} }
// MARK: - Wait Helpers // MARK: - Wait Helpers

View File

@@ -45,10 +45,12 @@ final class AppLaunchTests: BaseUITestCase {
// Background the app // Background the app
XCUIDevice.shared.press(.home) XCUIDevice.shared.press(.home)
sleep(2)
// Foreground // Foreground
app.activate() app.activate()
waitUntil(timeout: BaseUITestCase.longTimeout, "App should return to the foreground") {
self.app.state == .runningForeground
}
// Assert: Home still loaded, no re-bootstrap // Assert: Home still loaded, no re-bootstrap
XCTAssertTrue( XCTAssertTrue(

View File

@@ -85,12 +85,10 @@ final class HomeTests: BaseUITestCase {
// Tap refresh and verify no crash // Tap refresh and verify no crash
refreshButton.tap() refreshButton.tap()
// Wait briefly for reload
sleep(2)
// Featured section should still exist after refresh // Featured section should still exist after refresh
XCTAssertTrue(section.exists, waitUntil(timeout: BaseUITestCase.longTimeout, "Featured trips section should remain after refresh") {
"Featured trips section should remain after refresh") section.exists && refreshButton.exists
}
captureScreenshot(named: "F015-FeaturedTripsRefresh") captureScreenshot(named: "F015-FeaturedTripsRefresh")
} }
@@ -307,14 +305,17 @@ final class HomeTests: BaseUITestCase {
home.waitForLoad() home.waitForLoad()
home.switchToTab(home.myTripsTab) home.switchToTab(home.myTripsTab)
// Wait briefly for My Trips content to load let myTrips = MyTripsScreen(app: app)
sleep(1) waitUntil(timeout: BaseUITestCase.longTimeout, "My Trips should load before refreshing") {
myTrips.emptyState.exists || myTrips.tripCard(0).exists || self.app.staticTexts["Group Polls"].exists
}
// Pull down to refresh // Pull down to refresh
app.swipeDown(velocity: .slow) app.swipeDown(velocity: .slow)
// Wait for any refresh to complete waitUntil(timeout: BaseUITestCase.longTimeout, "My Trips should still be loaded after pull to refresh") {
sleep(2) myTrips.emptyState.exists || myTrips.tripCard(0).exists || self.app.staticTexts["Group Polls"].exists
}
// Verify the tab is still functional (no crash) // Verify the tab is still functional (no crash)
let groupPolls = app.staticTexts["Group Polls"] let groupPolls = app.staticTexts["Group Polls"]

View File

@@ -206,22 +206,14 @@ final class ProgressTests: BaseUITestCase {
visitSheet.tapSave() visitSheet.tapSave()
visitSheet.navigationBar.waitForNonExistence(timeout: BaseUITestCase.defaultTimeout) visitSheet.navigationBar.waitForNonExistence(timeout: BaseUITestCase.defaultTimeout)
// Wait for data to reload
sleep(2)
// Progress should have updated verify the progress circle label changed // Progress should have updated verify the progress circle label changed
let updatedCircle = app.descendants(matching: .any).matching(NSPredicate( let updatedCircle = app.descendants(matching: .any).matching(NSPredicate(
format: "label CONTAINS 'stadiums visited'" format: "label CONTAINS 'stadiums visited'"
)).firstMatch )).firstMatch
XCTAssertTrue(updatedCircle.waitForExistence(timeout: BaseUITestCase.longTimeout), waitUntil(timeout: BaseUITestCase.longTimeout, "Progress label should update after adding a visit") {
"Progress circle should exist after adding a visit") guard updatedCircle.exists else { return false }
return initialLabel.isEmpty || updatedCircle.label != initialLabel
// If we had an initial label, verify it changed
if !initialLabel.isEmpty {
// The new label should have a higher visited count
XCTAssertNotEqual(updatedCircle.label, initialLabel,
"Progress label should update after adding a visit")
} }
captureScreenshot(named: "F099-ProgressPercentageUpdated") captureScreenshot(named: "F099-ProgressPercentageUpdated")

View File

@@ -127,9 +127,10 @@ final class ScheduleTests: BaseUITestCase {
"Search field should exist") "Search field should exist")
searchField.tap() searchField.tap()
searchField.typeText("Yankees") searchField.typeText("Yankees")
XCTAssertTrue(
// Wait for results to filter ((searchField.value as? String) ?? "").contains("Yankees"),
sleep(1) "Search field should contain the typed team name"
)
captureScreenshot(named: "F089-SearchByTeam") captureScreenshot(named: "F089-SearchByTeam")
} }
@@ -150,9 +151,10 @@ final class ScheduleTests: BaseUITestCase {
"Search field should exist") "Search field should exist")
searchField.tap() searchField.tap()
searchField.typeText("Wrigley") searchField.typeText("Wrigley")
XCTAssertTrue(
// Wait for results to filter ((searchField.value as? String) ?? "").contains("Wrigley"),
sleep(1) "Search field should contain the typed venue name"
)
captureScreenshot(named: "F090-SearchByVenue") captureScreenshot(named: "F090-SearchByVenue")
} }
@@ -174,19 +176,15 @@ final class ScheduleTests: BaseUITestCase {
searchField.tap() searchField.tap()
searchField.typeText("ZZZZNONEXISTENTTEAMZZZZ") searchField.typeText("ZZZZNONEXISTENTTEAMZZZZ")
// Wait for empty state
sleep(1)
// Empty state or "no results" text should appear // Empty state or "no results" text should appear
let emptyState = schedule.emptyState let emptyState = schedule.emptyState
let noResults = app.staticTexts.matching(NSPredicate( let noResults = app.staticTexts.matching(NSPredicate(
format: "label CONTAINS[c] 'no' AND label CONTAINS[c] 'game'" format: "label CONTAINS[c] 'no' AND label CONTAINS[c] 'game'"
)).firstMatch )).firstMatch
let hasEmptyIndicator = emptyState.waitForExistence(timeout: BaseUITestCase.shortTimeout) waitUntil(timeout: BaseUITestCase.shortTimeout, "Empty state should appear when no games match search") {
|| noResults.waitForExistence(timeout: BaseUITestCase.shortTimeout) emptyState.exists || noResults.exists
XCTAssertTrue(hasEmptyIndicator, }
"Empty state should appear when no games match search")
captureScreenshot(named: "F092-ScheduleEmptyState") captureScreenshot(named: "F092-ScheduleEmptyState")
} }

View File

@@ -162,10 +162,10 @@ final class SettingsTests: BaseUITestCase {
let switchCoord = toggle.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)) let switchCoord = toggle.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5))
switchCoord.tap() switchCoord.tap()
// Small wait for the toggle animation to complete
sleep(1)
// Value should have changed // Value should have changed
waitUntil(timeout: BaseUITestCase.shortTimeout, "Toggle value should change after tapping the switch") {
(toggle.value as? String) != initialValue
}
let newValue = toggle.value as? String let newValue = toggle.value as? String
XCTAssertNotEqual(initialValue, newValue, XCTAssertNotEqual(initialValue, newValue,
"Toggle value should change after tap (was '\(initialValue ?? "nil")', now '\(newValue ?? "nil")')") "Toggle value should change after tap (was '\(initialValue ?? "nil")', now '\(newValue ?? "nil")')")

View File

@@ -157,8 +157,11 @@ final class TripOptionsTests: BaseUITestCase {
"'5' cities filter button should exist") "'5' cities filter button should exist")
fiveCitiesButton.tap() fiveCitiesButton.tap()
// Results should update; verify no crash let firstTrip = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'tripOptions.trip.'")).firstMatch
sleep(1) XCTAssertTrue(
firstTrip.waitForExistence(timeout: BaseUITestCase.shortTimeout),
"Trip results should remain visible after applying the cities filter"
)
captureScreenshot(named: "F057-CitiesFilter-5") captureScreenshot(named: "F057-CitiesFilter-5")
} }

View File

@@ -63,7 +63,7 @@ Do not add one-off test-only branching logic unless it removes a real flake.
xcodebuild test-without-building \ xcodebuild test-without-building \
-project SportsTime.xcodeproj \ -project SportsTime.xcodeproj \
-scheme SportsTime \ -scheme SportsTime \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \ -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \
-parallel-testing-enabled NO \ -parallel-testing-enabled NO \
-only-testing:SportsTimeUITests/TripWizardFlowTests/testF026_DateRangeSelection -only-testing:SportsTimeUITests/TripWizardFlowTests/testF026_DateRangeSelection
``` ```
@@ -74,7 +74,7 @@ xcodebuild test-without-building \
xcodebuild test-without-building \ xcodebuild test-without-building \
-project SportsTime.xcodeproj \ -project SportsTime.xcodeproj \
-scheme SportsTime \ -scheme SportsTime \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \ -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \
-parallel-testing-enabled NO \ -parallel-testing-enabled NO \
-only-testing:SportsTimeUITests/TripOptionsTests -only-testing:SportsTimeUITests/TripOptionsTests
``` ```
@@ -85,7 +85,7 @@ xcodebuild test-without-building \
xcodebuild test-without-building \ xcodebuild test-without-building \
-project SportsTime.xcodeproj \ -project SportsTime.xcodeproj \
-scheme SportsTime \ -scheme SportsTime \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \ -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \
-parallel-testing-enabled NO \ -parallel-testing-enabled NO \
-only-testing:SportsTimeUITests -only-testing:SportsTimeUITests
``` ```
@@ -96,7 +96,7 @@ xcodebuild test-without-building \
xcodebuild test-without-building \ xcodebuild test-without-building \
-project SportsTime.xcodeproj \ -project SportsTime.xcodeproj \
-scheme SportsTime \ -scheme SportsTime \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \ -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \
-parallel-testing-enabled NO -parallel-testing-enabled NO
``` ```

View File

@@ -66,7 +66,7 @@ If a candidate looks flaky/high-risk, skip it and explain why.
Use this destination: Use this destination:
`-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2'` `-destination 'platform=iOS Simulator,name=iPhone 17,OS=latest'`
Run each selected test explicitly: Run each selected test explicitly: