From c976ae5cb376c0239a822152e81f4974dc405f5c Mon Sep 17 00:00:00 2001 From: Trey t Date: Thu, 19 Feb 2026 16:04:53 -0600 Subject: [PATCH] Add POI category filters, delete item button, and fix itinerary persistence - Expand POI categories from 5 to 7 (restaurant, bar, coffee, hotel, parking, attraction, entertainment) - Add category filter chips with per-category API calls and caching - Add delete button with confirmation dialog to Edit Item sheet - Fix itinerary items not persisting: use LocalItineraryItem (SwiftData) as primary store with CloudKit sync as secondary, register model in schema Co-Authored-By: Claude Opus 4.6 --- .../Export/Services/POISearchService.swift | 34 ++- .../Trip/Views/AddItem/POIDetailSheet.swift | 6 +- .../Views/AddItem/QuickAddItemSheet.swift | 164 ++++++++++++++- .../Features/Trip/Views/TripDetailView.swift | 193 +++++++++++++----- SportsTime/SportsTimeApp.swift | 1 + .../Export/POISearchServiceTests.swift | 26 ++- 6 files changed, 337 insertions(+), 87 deletions(-) diff --git a/SportsTime/Export/Services/POISearchService.swift b/SportsTime/Export/Services/POISearchService.swift index 2848eb2..4a7589b 100644 --- a/SportsTime/Export/Services/POISearchService.swift +++ b/SportsTime/Export/Services/POISearchService.swift @@ -44,48 +44,46 @@ actor POISearchService { enum POICategory: String, CaseIterable { case restaurant + case bar + case coffee + case hotel + case parking case attraction case entertainment - case nightlife - case museum var displayName: String { switch self { case .restaurant: return "Restaurant" + case .bar: return "Bar" + case .coffee: return "Coffee" + case .hotel: return "Hotel" + case .parking: return "Parking" case .attraction: return "Attraction" case .entertainment: return "Entertainment" - case .nightlife: return "Nightlife" - case .museum: return "Museum" } } var iconName: String { switch self { case .restaurant: return "fork.knife" + case .bar: return "wineglass.fill" + case .coffee: return "cup.and.saucer.fill" + case .hotel: return "bed.double.fill" + case .parking: return "car.fill" case .attraction: return "star.fill" case .entertainment: return "theatermasks.fill" - case .nightlife: return "moon.stars.fill" - case .museum: return "building.columns.fill" - } - } - - var mkPointOfInterestCategory: MKPointOfInterestCategory { - switch self { - case .restaurant: return .restaurant - case .attraction: return .nationalPark - case .entertainment: return .theater - case .nightlife: return .nightlife - case .museum: return .museum } } var searchQuery: String { switch self { case .restaurant: return "restaurants" + case .bar: return "bars" + case .coffee: return "coffee shops" + case .hotel: return "hotels" + case .parking: return "parking" case .attraction: return "tourist attractions" case .entertainment: return "entertainment" - case .nightlife: return "bars nightlife" - case .museum: return "museums" } } } diff --git a/SportsTime/Features/Trip/Views/AddItem/POIDetailSheet.swift b/SportsTime/Features/Trip/Views/AddItem/POIDetailSheet.swift index 44211af..3844f27 100644 --- a/SportsTime/Features/Trip/Views/AddItem/POIDetailSheet.swift +++ b/SportsTime/Features/Trip/Views/AddItem/POIDetailSheet.swift @@ -171,10 +171,12 @@ struct POIDetailSheet: View { private var categoryColor: Color { switch poi.category { case .restaurant: return .orange + case .bar: return .indigo + case .coffee: return .brown + case .hotel: return .blue + case .parking: return .green case .attraction: return .yellow case .entertainment: return .purple - case .nightlife: return .indigo - case .museum: return .teal } } } diff --git a/SportsTime/Features/Trip/Views/AddItem/QuickAddItemSheet.swift b/SportsTime/Features/Trip/Views/AddItem/QuickAddItemSheet.swift index 0b68b23..82a1776 100644 --- a/SportsTime/Features/Trip/Views/AddItem/QuickAddItemSheet.swift +++ b/SportsTime/Features/Trip/Views/AddItem/QuickAddItemSheet.swift @@ -18,17 +18,21 @@ struct QuickAddItemSheet: View { let existingItem: ItineraryItem? var regionCoordinate: CLLocationCoordinate2D? var onSave: (ItineraryItem) -> Void + var onDelete: ((ItineraryItem) -> Void)? // Form state @State private var title: String = "" @State private var selectedPlace: MKMapItem? @State private var showLocationSearch = false + @State private var showDeleteConfirmation = false @FocusState private var isTitleFocused: Bool // POI state @State private var nearbyPOIs: [POISearchService.POI] = [] + @State private var categoryCache: [POISearchService.POICategory: [POISearchService.POI]] = [:] @State private var isLoadingPOIs = false @State private var selectedPOI: POISearchService.POI? + @State private var selectedCategory: POISearchService.POICategory? // Derived state private var isEditing: Bool { existingItem != nil } @@ -48,6 +52,10 @@ struct QuickAddItemSheet: View { if regionCoordinate != nil && !isEditing { nearbyPOISection } + + if isEditing, onDelete != nil { + deleteButton + } } .padding(.horizontal, Theme.Spacing.lg) .padding(.top, Theme.Spacing.md) @@ -258,10 +266,42 @@ struct QuickAddItemSheet: View { // MARK: - Nearby POI Section + private var displayedPOIs: [POISearchService.POI] { + guard let category = selectedCategory else { return nearbyPOIs } + return categoryCache[category] ?? [] + } + private var nearbyPOISection: some View { VStack(alignment: .leading, spacing: Theme.Spacing.md) { sectionHeader(title: "Nearby Places", icon: "mappin.and.ellipse") + // Category filter chips + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: Theme.Spacing.xs) { + CategoryChip( + label: "All", + icon: "map.fill", + isSelected: selectedCategory == nil, + color: Theme.warmOrange, + colorScheme: colorScheme + ) { + selectedCategory = nil + } + + ForEach(POISearchService.POICategory.allCases, id: \.self) { category in + CategoryChip( + label: category.displayName, + icon: category.iconName, + isSelected: selectedCategory == category, + color: categoryColor(for: category), + colorScheme: colorScheme + ) { + selectCategory(category) + } + } + } + } + if isLoadingPOIs { HStack(spacing: Theme.Spacing.sm) { ProgressView() @@ -271,15 +311,15 @@ struct QuickAddItemSheet: View { } .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, Theme.Spacing.lg) - } else if nearbyPOIs.isEmpty { - Text("No nearby places found") + } else if displayedPOIs.isEmpty { + Text(selectedCategory != nil ? "No \(selectedCategory!.displayName.lowercased())s found nearby" : "No nearby places found") .font(.subheadline) .foregroundStyle(Theme.textMuted(colorScheme)) .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, Theme.Spacing.lg) } else { VStack(spacing: 0) { - ForEach(Array(nearbyPOIs.enumerated()), id: \.element.id) { index, poi in + ForEach(Array(displayedPOIs.enumerated()), id: \.element.id) { index, poi in Button { selectedPOI = poi } label: { @@ -287,7 +327,7 @@ struct QuickAddItemSheet: View { } .buttonStyle(.plain) - if index < nearbyPOIs.count - 1 { + if index < displayedPOIs.count - 1 { Divider() .padding(.leading, 52) } @@ -298,6 +338,18 @@ struct QuickAddItemSheet: View { .cardStyle() } + private func categoryColor(for category: POISearchService.POICategory) -> Color { + switch category { + case .restaurant: return .orange + case .bar: return .indigo + case .coffee: return .brown + case .hotel: return .blue + case .parking: return .green + case .attraction: return .yellow + case .entertainment: return .purple + } + } + // MARK: - Section Header private func sectionHeader(title: String, icon: String, optional: Bool = false) -> some View { @@ -328,6 +380,40 @@ struct QuickAddItemSheet: View { } } + // MARK: - Delete Button + + private var deleteButton: some View { + Button(role: .destructive) { + showDeleteConfirmation = true + } label: { + HStack { + Image(systemName: "trash") + Text("Delete Item") + } + .font(.body) + .fontWeight(.medium) + .foregroundStyle(.red) + .frame(maxWidth: .infinity) + .padding(.vertical, Theme.Spacing.md) + } + .confirmationDialog( + "Delete this item?", + isPresented: $showDeleteConfirmation, + titleVisibility: .visible + ) { + Button("Delete", role: .destructive) { + if let item = existingItem { + onDelete?(item) + } + dismiss() + } + } message: { + Text("This will remove the item from your itinerary.") + } + .accessibilityLabel("Delete item") + .accessibilityHint("Removes this item from the itinerary") + } + // MARK: - Helpers private var saveButtonAccessibilityLabel: String { @@ -426,6 +512,20 @@ struct QuickAddItemSheet: View { // MARK: - POI Loading + private func selectCategory(_ category: POISearchService.POICategory) { + if selectedCategory == category { + selectedCategory = nil + return + } + selectedCategory = category + + // If we already have cached results, no need to fetch + if categoryCache[category] != nil { return } + + // Fetch results for this category + Task { await loadCategoryPOIs(category) } + } + private func loadNearbyPOIs() async { guard let coordinate = regionCoordinate, !isEditing else { return } @@ -444,6 +544,24 @@ struct QuickAddItemSheet: View { } } + private func loadCategoryPOIs(_ category: POISearchService.POICategory) async { + guard let coordinate = regionCoordinate else { return } + + isLoadingPOIs = true + defer { isLoadingPOIs = false } + + do { + let pois = try await POISearchService().findNearbyPOIs( + near: coordinate, + categories: [category], + limitPerCategory: 10 + ) + categoryCache[category] = pois + } catch { + categoryCache[category] = [] + } + } + private func addPOIToDay(_ poi: POISearchService.POI) { let customInfo = CustomInfo( title: poi.name, @@ -472,6 +590,38 @@ struct QuickAddItemSheet: View { } } +// MARK: - Category Chip + +private struct CategoryChip: View { + let label: String + let icon: String + let isSelected: Bool + let color: Color + let colorScheme: ColorScheme + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 4) { + Image(systemName: icon) + .font(.caption2) + Text(label) + .font(.caption) + .fontWeight(.medium) + } + .padding(.horizontal, 10) + .padding(.vertical, 6) + .foregroundStyle(isSelected ? .white : Theme.textPrimary(colorScheme)) + .background(isSelected ? color : color.opacity(0.12)) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + .accessibilityLabel(label) + .accessibilityValue(isSelected ? "Selected" : "Not selected") + .accessibilityAddTraits(isSelected ? .isSelected : []) + } +} + // MARK: - POI Row private struct POIRow: View { @@ -527,10 +677,12 @@ private struct POIRow: View { private var categoryColor: Color { switch poi.category { case .restaurant: return .orange + case .bar: return .indigo + case .coffee: return .brown + case .hotel: return .blue + case .parking: return .green case .attraction: return .yellow case .entertainment: return .purple - case .nightlife: return .indigo - case .museum: return .teal } } } diff --git a/SportsTime/Features/Trip/Views/TripDetailView.swift b/SportsTime/Features/Trip/Views/TripDetailView.swift index 6031d3f..a6fd028 100644 --- a/SportsTime/Features/Trip/Views/TripDetailView.swift +++ b/SportsTime/Features/Trip/Views/TripDetailView.swift @@ -106,7 +106,8 @@ struct TripDetailView: View { addItemAnchor: $addItemAnchor, editingItem: $editingItem, tripId: trip.id, - saveItineraryItem: saveItineraryItem + saveItineraryItem: saveItineraryItem, + deleteItineraryItem: deleteItineraryItem )) .alert("Large Trip Route", isPresented: $showMultiRouteAlert) { ForEach(multiRouteChunks.indices, id: \.self) { index in @@ -858,6 +859,9 @@ struct TripDetailView: View { itineraryItems[idx] = updated } + // Persist locally + saveItemLocally(updated, isUpdate: true) + // Sync to CloudKit (debounced) await ItineraryItemService.shared.updateItem(updated) print("✅ [Move] Synced \(title) with day: \(day), sortOrder: \(sortOrder)") @@ -1383,64 +1387,96 @@ struct TripDetailView: View { } } - // MARK: - Itinerary Items (CloudKit persistence) + // MARK: - Itinerary Items (Local-first persistence with CloudKit sync) private func loadItineraryItems() async { print("🔍 [ItineraryItems] Loading items for trip: \(trip.id)") + + // 1. Load from local SwiftData first (instant, works offline) + let tripId = trip.id + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.tripId == tripId } + ) + let localItems = (try? modelContext.fetch(descriptor))?.compactMap(\.toItem) ?? [] + if !localItems.isEmpty { + print("✅ [ItineraryItems] Loaded \(localItems.count) items from local cache") + itineraryItems = localItems + extractTravelOverrides(from: localItems) + } + + // 2. Try CloudKit for latest data (background sync) do { - let items = try await ItineraryItemService.shared.fetchItems(forTripId: trip.id) - print("✅ [ItineraryItems] Loaded \(items.count) items from CloudKit") - itineraryItems = items + let cloudItems = try await ItineraryItemService.shared.fetchItems(forTripId: trip.id) + print("✅ [ItineraryItems] Loaded \(cloudItems.count) items from CloudKit") - // Extract travel overrides (day + sortOrder) from travel-type items - var overrides: [String: TravelOverride] = [:] - - for item in items where item.isTravel { - guard let travelInfo = item.travelInfo else { continue } - if let segIdx = travelInfo.segmentIndex, - segIdx >= 0, - segIdx < trip.travelSegments.count { - let segment = trip.travelSegments[segIdx] - if !travelInfo.matches(segment: segment) { - print("⚠️ [TravelOverrides] Mismatched travel cities for segment \(segIdx); using canonical segment cities") - } - let travelId = stableTravelAnchorId(segment, at: segIdx) - overrides[travelId] = TravelOverride(day: item.day, sortOrder: item.sortOrder) - continue - } - - // Legacy record without segment index: only accept if the city pair maps uniquely. - let matches = trip.travelSegments.enumerated().filter { _, segment in - travelInfo.matches(segment: segment) - } - - guard matches.count == 1, let match = matches.first else { - print("⚠️ [TravelOverrides] Ignoring ambiguous legacy travel override \(travelInfo.fromCity)->\(travelInfo.toCity)") - continue - } - - let segIdx = match.offset - let segment = match.element - let travelId = stableTravelAnchorId(segment, at: segIdx) - overrides[travelId] = TravelOverride(day: item.day, sortOrder: item.sortOrder) + // Merge: use CloudKit as source of truth when available + if !cloudItems.isEmpty || localItems.isEmpty { + itineraryItems = cloudItems + extractTravelOverrides(from: cloudItems) + syncLocalCache(with: cloudItems) } - - travelOverrides = overrides - print("✅ [TravelOverrides] Extracted \(overrides.count) travel overrides (day + sortOrder)") } catch { - print("❌ [ItineraryItems] Failed to load: \(error)") + print("⚠️ [ItineraryItems] CloudKit fetch failed (using local cache): \(error)") } } + private func extractTravelOverrides(from items: [ItineraryItem]) { + var overrides: [String: TravelOverride] = [:] + + for item in items where item.isTravel { + guard let travelInfo = item.travelInfo else { continue } + if let segIdx = travelInfo.segmentIndex, + segIdx >= 0, + segIdx < trip.travelSegments.count { + let segment = trip.travelSegments[segIdx] + if !travelInfo.matches(segment: segment) { + print("⚠️ [TravelOverrides] Mismatched travel cities for segment \(segIdx); using canonical segment cities") + } + let travelId = stableTravelAnchorId(segment, at: segIdx) + overrides[travelId] = TravelOverride(day: item.day, sortOrder: item.sortOrder) + continue + } + + let matches = trip.travelSegments.enumerated().filter { _, segment in + travelInfo.matches(segment: segment) + } + + guard matches.count == 1, let match = matches.first else { + print("⚠️ [TravelOverrides] Ignoring ambiguous legacy travel override \(travelInfo.fromCity)->\(travelInfo.toCity)") + continue + } + + let segIdx = match.offset + let segment = match.element + let travelId = stableTravelAnchorId(segment, at: segIdx) + overrides[travelId] = TravelOverride(day: item.day, sortOrder: item.sortOrder) + } + + travelOverrides = overrides + print("✅ [TravelOverrides] Extracted \(overrides.count) travel overrides (day + sortOrder)") + } + + private func syncLocalCache(with items: [ItineraryItem]) { + let tripId = trip.id + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.tripId == tripId } + ) + let existing = (try? modelContext.fetch(descriptor)) ?? [] + for old in existing { modelContext.delete(old) } + for item in items { + if let local = LocalItineraryItem.from(item) { + modelContext.insert(local) + } + } + try? modelContext.save() + } + private func saveItineraryItem(_ item: ItineraryItem) async { - // Check if this is an update or create let isUpdate = itineraryItems.contains(where: { $0.id == item.id }) let title = item.customInfo?.title ?? "item" print("💾 [ItineraryItems] Saving item: '\(title)' (isUpdate: \(isUpdate))") - print(" - tripId: \(item.tripId)") - print(" - day: \(item.day), sortOrder: \(item.sortOrder)") - // Update local state immediately for responsive UI + // Update in-memory state immediately if isUpdate { if let index = itineraryItems.firstIndex(where: { $0.id == item.id }) { itineraryItems[index] = item @@ -1449,7 +1485,10 @@ struct TripDetailView: View { itineraryItems.append(item) } - // Persist to CloudKit + // Persist to local SwiftData immediately + saveItemLocally(item, isUpdate: isUpdate) + + // Sync to CloudKit in background do { if isUpdate { await ItineraryItemService.shared.updateItem(item) @@ -1457,9 +1496,41 @@ struct TripDetailView: View { } else { _ = try await ItineraryItemService.shared.createItem(item) print("✅ [ItineraryItems] Created in CloudKit: \(title)") + markLocalItemSynced(item.id) } } catch { - print("❌ [ItineraryItems] CloudKit save failed: \(error)") + print("⚠️ [ItineraryItems] CloudKit save failed (saved locally): \(error)") + } + } + + private func saveItemLocally(_ item: ItineraryItem, isUpdate: Bool) { + if isUpdate { + let itemId = item.id + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.id == itemId } + ) + if let existing = try? modelContext.fetch(descriptor).first { + existing.day = item.day + existing.sortOrder = item.sortOrder + existing.kindData = (try? JSONEncoder().encode(item.kind)) ?? existing.kindData + existing.modifiedAt = item.modifiedAt + existing.pendingSync = true + } + } else { + if let local = LocalItineraryItem.from(item, pendingSync: true) { + modelContext.insert(local) + } + } + try? modelContext.save() + } + + private func markLocalItemSynced(_ itemId: UUID) { + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.id == itemId } + ) + if let local = try? modelContext.fetch(descriptor).first { + local.pendingSync = false + try? modelContext.save() } } @@ -1467,15 +1538,25 @@ struct TripDetailView: View { let title = item.customInfo?.title ?? "item" print("🗑️ [ItineraryItems] Deleting item: '\(title)'") - // Remove from local state immediately + // Remove from in-memory state itineraryItems.removeAll { $0.id == item.id } + // Remove from local SwiftData + let itemId = item.id + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.id == itemId } + ) + if let local = try? modelContext.fetch(descriptor).first { + modelContext.delete(local) + try? modelContext.save() + } + // Delete from CloudKit do { try await ItineraryItemService.shared.deleteItem(item.id) print("✅ [ItineraryItems] Deleted from CloudKit") } catch { - print("❌ [ItineraryItems] CloudKit delete failed: \(error)") + print("⚠️ [ItineraryItems] CloudKit delete failed (removed locally): \(error)") } } @@ -1580,6 +1661,7 @@ struct TripDetailView: View { updated.modifiedAt = Date() updated.kind = .travel(canonicalInfo) itineraryItems[existingIndex] = updated + saveItemLocally(updated, isUpdate: true) await ItineraryItemService.shared.updateItem(updated) } else { // Create new travel item @@ -1590,8 +1672,10 @@ struct TripDetailView: View { kind: .travel(canonicalInfo) ) itineraryItems.append(item) + saveItemLocally(item, isUpdate: false) do { _ = try await ItineraryItemService.shared.createItem(item) + markLocalItemSynced(item.id) } catch { print("❌ [TravelOverrides] CloudKit save failed: \(error)") return @@ -2094,6 +2178,7 @@ private struct SheetModifiers: ViewModifier { @Binding var editingItem: ItineraryItem? let tripId: UUID let saveItineraryItem: (ItineraryItem) async -> Void + let deleteItineraryItem: (ItineraryItem) async -> Void func body(content: Content) -> some View { content @@ -2119,10 +2204,14 @@ private struct SheetModifiers: ViewModifier { QuickAddItemSheet( tripId: tripId, day: item.day, - existingItem: item - ) { updatedItem in - Task { await saveItineraryItem(updatedItem) } - } + existingItem: item, + onSave: { updatedItem in + Task { await saveItineraryItem(updatedItem) } + }, + onDelete: { deletedItem in + Task { await deleteItineraryItem(deletedItem) } + } + ) } } } diff --git a/SportsTime/SportsTimeApp.swift b/SportsTime/SportsTimeApp.swift index 9e33f37..7002af9 100644 --- a/SportsTime/SportsTimeApp.swift +++ b/SportsTime/SportsTimeApp.swift @@ -46,6 +46,7 @@ struct SportsTimeApp: App { // User data models SavedTrip.self, TripVote.self, + LocalItineraryItem.self, UserPreferences.self, CachedSchedule.self, // Stadium progress models diff --git a/SportsTimeTests/Export/POISearchServiceTests.swift b/SportsTimeTests/Export/POISearchServiceTests.swift index 329cea5..e1a4345 100644 --- a/SportsTimeTests/Export/POISearchServiceTests.swift +++ b/SportsTimeTests/Export/POISearchServiceTests.swift @@ -109,10 +109,12 @@ struct POICategoryTests { @Test("displayName: returns readable name") func displayName_readable() { #expect(POISearchService.POICategory.restaurant.displayName == "Restaurant") + #expect(POISearchService.POICategory.bar.displayName == "Bar") + #expect(POISearchService.POICategory.coffee.displayName == "Coffee") + #expect(POISearchService.POICategory.hotel.displayName == "Hotel") + #expect(POISearchService.POICategory.parking.displayName == "Parking") #expect(POISearchService.POICategory.attraction.displayName == "Attraction") #expect(POISearchService.POICategory.entertainment.displayName == "Entertainment") - #expect(POISearchService.POICategory.nightlife.displayName == "Nightlife") - #expect(POISearchService.POICategory.museum.displayName == "Museum") } // MARK: - Specification Tests: iconName @@ -121,10 +123,12 @@ struct POICategoryTests { @Test("iconName: returns SF Symbol name") func iconName_sfSymbol() { #expect(POISearchService.POICategory.restaurant.iconName == "fork.knife") + #expect(POISearchService.POICategory.bar.iconName == "wineglass.fill") + #expect(POISearchService.POICategory.coffee.iconName == "cup.and.saucer.fill") + #expect(POISearchService.POICategory.hotel.iconName == "bed.double.fill") + #expect(POISearchService.POICategory.parking.iconName == "car.fill") #expect(POISearchService.POICategory.attraction.iconName == "star.fill") #expect(POISearchService.POICategory.entertainment.iconName == "theatermasks.fill") - #expect(POISearchService.POICategory.nightlife.iconName == "moon.stars.fill") - #expect(POISearchService.POICategory.museum.iconName == "building.columns.fill") } // MARK: - Specification Tests: searchQuery @@ -133,10 +137,12 @@ struct POICategoryTests { @Test("searchQuery: returns search string") func searchQuery_searchString() { #expect(POISearchService.POICategory.restaurant.searchQuery == "restaurants") + #expect(POISearchService.POICategory.bar.searchQuery == "bars") + #expect(POISearchService.POICategory.coffee.searchQuery == "coffee shops") + #expect(POISearchService.POICategory.hotel.searchQuery == "hotels") + #expect(POISearchService.POICategory.parking.searchQuery == "parking") #expect(POISearchService.POICategory.attraction.searchQuery == "tourist attractions") #expect(POISearchService.POICategory.entertainment.searchQuery == "entertainment") - #expect(POISearchService.POICategory.nightlife.searchQuery == "bars nightlife") - #expect(POISearchService.POICategory.museum.searchQuery == "museums") } // MARK: - Invariant Tests @@ -154,12 +160,14 @@ struct POICategoryTests { /// - Invariant: CaseIterable includes all cases @Test("Invariant: CaseIterable includes all cases") func invariant_allCasesIncluded() { - #expect(POISearchService.POICategory.allCases.count == 5) + #expect(POISearchService.POICategory.allCases.count == 7) #expect(POISearchService.POICategory.allCases.contains(.restaurant)) + #expect(POISearchService.POICategory.allCases.contains(.bar)) + #expect(POISearchService.POICategory.allCases.contains(.coffee)) + #expect(POISearchService.POICategory.allCases.contains(.hotel)) + #expect(POISearchService.POICategory.allCases.contains(.parking)) #expect(POISearchService.POICategory.allCases.contains(.attraction)) #expect(POISearchService.POICategory.allCases.contains(.entertainment)) - #expect(POISearchService.POICategory.allCases.contains(.nightlife)) - #expect(POISearchService.POICategory.allCases.contains(.museum)) } }