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>
158 lines
4.2 KiB
Swift
158 lines
4.2 KiB
Swift
//
|
|
// PollsListView.swift
|
|
// SportsTime
|
|
//
|
|
// View for listing user's polls
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct PollsListView: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var polls: [TripPoll] = []
|
|
@State private var isLoading = false
|
|
@State private var hasLoadedInitially = false
|
|
@State private var error: PollError?
|
|
@State private var showJoinPoll = false
|
|
@State private var joinCode = ""
|
|
|
|
var body: some View {
|
|
Group {
|
|
if isLoading && polls.isEmpty {
|
|
ProgressView("Loading polls...")
|
|
} else if polls.isEmpty {
|
|
emptyState
|
|
} else {
|
|
pollsList
|
|
}
|
|
}
|
|
.navigationTitle("Group Polls")
|
|
.accessibilityIdentifier("polls.list")
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Button {
|
|
showJoinPoll = true
|
|
} label: {
|
|
Image(systemName: "link.badge.plus")
|
|
.accessibilityLabel("Join a poll")
|
|
}
|
|
}
|
|
}
|
|
.refreshable {
|
|
await loadPolls()
|
|
}
|
|
.task {
|
|
guard !hasLoadedInitially else { return }
|
|
hasLoadedInitially = true
|
|
await loadPolls()
|
|
}
|
|
.alert("Join Poll", isPresented: $showJoinPoll) {
|
|
TextField("Enter code", text: $joinCode)
|
|
.textInputAutocapitalization(.characters)
|
|
Button("Join") {
|
|
// Navigation will be handled by deep link
|
|
if !joinCode.isEmpty {
|
|
// TODO: Navigate to poll detail
|
|
}
|
|
}
|
|
Button("Cancel", role: .cancel) {
|
|
joinCode = ""
|
|
}
|
|
} message: {
|
|
Text("Enter the 6-character poll code")
|
|
}
|
|
.alert("Error", isPresented: .constant(error != nil)) {
|
|
Button("OK") {
|
|
error = nil
|
|
}
|
|
} message: {
|
|
if let error {
|
|
Text(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var emptyState: some View {
|
|
ContentUnavailableView {
|
|
Label("No Polls", systemImage: "chart.bar.doc.horizontal")
|
|
} description: {
|
|
Text("Create a poll from your saved trips to let friends vote on which trip to take.")
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var pollsList: some View {
|
|
List {
|
|
ForEach(polls) { poll in
|
|
NavigationLink(value: poll) {
|
|
PollRowView(poll: poll)
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.plain)
|
|
.navigationDestination(for: TripPoll.self) { poll in
|
|
PollDetailView(poll: poll)
|
|
}
|
|
}
|
|
|
|
private func loadPolls() async {
|
|
isLoading = true
|
|
error = nil
|
|
|
|
do {
|
|
polls = try await PollService.shared.fetchMyPolls()
|
|
} catch let pollError as PollError {
|
|
error = pollError
|
|
} catch {
|
|
self.error = .unknown(error)
|
|
}
|
|
|
|
isLoading = false
|
|
}
|
|
}
|
|
|
|
// MARK: - Poll Row View
|
|
|
|
private struct PollRowView: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let poll: TripPoll
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text(poll.title)
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
Text(poll.shareCode)
|
|
.font(.caption)
|
|
.fontWeight(.semibold)
|
|
.foregroundStyle(Theme.warmOrange)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(Theme.warmOrange.opacity(0.15))
|
|
.clipShape(Capsule())
|
|
}
|
|
|
|
HStack {
|
|
Label("\(poll.tripSnapshots.count) trips", systemImage: "map")
|
|
|
|
Spacer()
|
|
|
|
Text(poll.createdAt, style: .date)
|
|
}
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
PollsListView()
|
|
}
|
|
}
|