Files
Sportstime/SportsTime/Features/Trip/Views/Wizard/Steps/ReviewStep.swift
Trey t dc142bd14b feat: expand XCUITest coverage to 54 QA scenarios with accessibility IDs and fix test failures
Add 22 new UI tests across 8 test files covering Home, Schedule, Progress,
Settings, TabNavigation, TripSaving, and TripOptions. Add accessibility
identifiers to 11 view files for test element discovery. Fix sport chip
assertion logic (all sports start selected, tap deselects), scroll container
issues on iOS 26 nested ScrollViews, toggle interaction, and delete trip flow.
Update QA coverage map from 32 to 54 automated test cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 19:44:22 -06:00

232 lines
7.9 KiB
Swift

//
// ReviewStep.swift
// SportsTime
//
// Step 8 of the trip wizard - review and submit.
//
import SwiftUI
struct ReviewStep: View {
@Environment(\.colorScheme) private var colorScheme
let planningMode: PlanningMode
let selectedSports: Set<Sport>
let startDate: Date
let endDate: Date
let selectedRegions: Set<Region>
let routePreference: RoutePreference
let allowRepeatCities: Bool
let mustStopLocations: [LocationInput]
let isPlanning: Bool
let canPlanTrip: Bool
let fieldValidation: FieldValidation
let onPlan: () -> Void
// Mode-specific display values (passed from parent)
var selectedGameCount: Int = 0
var selectedTeamName: String? = nil
var startLocationName: String? = nil
var endLocationName: String? = nil
var teamFirstTeamCount: Int = 0
var body: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.md) {
StepHeader(
title: "Ready to plan your trip!",
subtitle: "Review your selections"
)
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Mode (always shown)
ReviewRow(label: "Mode", value: planningMode.displayName)
// Mode-specific required fields
ForEach(fieldValidation.requiredFields, id: \.name) { field in
ReviewRow(
label: field.name,
value: displayValue(for: field.name),
isMissing: field.status == .missing
)
}
// Optional: Must-stops (shown if any selected)
if !mustStopLocations.isEmpty {
ReviewRow(label: "Must-stops", value: mustStopLocations.map(\.name).joined(separator: ", "))
}
}
.padding(Theme.Spacing.sm)
.background(Theme.cardBackgroundElevated(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
// Missing fields warning
if !fieldValidation.missingFields.isEmpty {
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
.accessibilityHidden(true)
Text("Complete all required fields to continue")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
}
.accessibilityIdentifier("wizard.missingFieldsWarning")
}
Button(action: onPlan) {
HStack(spacing: Theme.Spacing.sm) {
if isPlanning {
LoadingSpinner(size: .small)
.colorScheme(.dark) // Force white on orange button
}
Text(isPlanning ? "Planning..." : "Plan My Trip")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding(Theme.Spacing.md)
.background(canPlanTrip ? Theme.warmOrange : Theme.textMuted(colorScheme))
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
}
.accessibilityIdentifier("wizard.planTripButton")
.accessibilityHint("Creates trip itinerary based on your selections")
.disabled(!canPlanTrip || isPlanning)
}
.padding(Theme.Spacing.lg)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
}
private func displayValue(for fieldName: String) -> String {
switch fieldName {
case "Sports":
return selectedSports.isEmpty ? "Not selected" : selectedSports.map(\.rawValue).sorted().joined(separator: ", ")
case "Dates":
return dateRangeText
case "Regions":
return selectedRegions.isEmpty ? "Not selected" : selectedRegions.map(\.shortName).sorted().joined(separator: ", ")
case "Route Preference":
return routePreference.displayName
case "Repeat Cities":
return allowRepeatCities ? "Yes" : "No"
case "Games":
return selectedGameCount > 0 ? "\(selectedGameCount) game\(selectedGameCount == 1 ? "" : "s") selected" : "Not selected"
case "Team":
return selectedTeamName ?? "Not selected"
case "Start Location":
return startLocationName ?? "Not selected"
case "End Location":
return endLocationName ?? "Not selected"
case "Teams":
return teamFirstTeamCount >= 2 ? "\(teamFirstTeamCount) teams selected" : "Select at least 2 teams"
default:
return ""
}
}
private var dateRangeText: String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return "\(formatter.string(from: startDate)) - \(formatter.string(from: endDate))"
}
}
// MARK: - Review Row
private struct ReviewRow: View {
@Environment(\.colorScheme) private var colorScheme
let label: String
let value: String
var isMissing: Bool = false
var body: some View {
HStack(alignment: .top) {
Text(label)
.font(.caption)
.foregroundStyle(isMissing ? .red : Theme.textMuted(colorScheme))
.frame(width: 80, alignment: .leading)
Text(value)
.font(.subheadline)
.foregroundStyle(isMissing ? .red : Theme.textPrimary(colorScheme))
Spacer()
if isMissing {
Image(systemName: "exclamationmark.circle.fill")
.font(.caption)
.foregroundStyle(.red)
.accessibilityHidden(true)
}
}
}
}
// MARK: - Preview
#Preview("All Valid") {
ReviewStep(
planningMode: .dateRange,
selectedSports: [.mlb, .nba],
startDate: Date(),
endDate: Date().addingTimeInterval(86400 * 7),
selectedRegions: [.east, .central],
routePreference: .balanced,
allowRepeatCities: false,
mustStopLocations: [],
isPlanning: false,
canPlanTrip: true,
fieldValidation: FieldValidation(
planningMode: .dateRange,
sports: .valid,
dates: .valid,
regions: .valid,
routePreference: .valid,
repeatCities: .valid,
selectedGames: .valid,
selectedTeam: .valid,
startLocation: .valid,
endLocation: .valid,
teamFirstTeams: .valid,
teamFirstTeamCount: 0
),
onPlan: {}
)
.padding()
.themedBackground()
}
#Preview("Missing Fields") {
ReviewStep(
planningMode: .dateRange,
selectedSports: [],
startDate: Date(),
endDate: Date().addingTimeInterval(86400 * 7),
selectedRegions: [],
routePreference: .balanced,
allowRepeatCities: false,
mustStopLocations: [],
isPlanning: false,
canPlanTrip: false,
fieldValidation: FieldValidation(
planningMode: .dateRange,
sports: .missing,
dates: .valid,
regions: .missing,
routePreference: .valid,
repeatCities: .missing,
selectedGames: .valid,
selectedTeam: .valid,
startLocation: .valid,
endLocation: .valid,
teamFirstTeams: .valid,
teamFirstTeamCount: 0
),
onPlan: {}
)
.padding()
.themedBackground()
}