From c1712ee0ca959bafb5d1240d55985ca973b28734 Mon Sep 17 00:00:00 2001 From: Barron Roth Date: Tue, 19 May 2026 14:29:11 -0700 Subject: [PATCH] Fix sub-location picker creation flow --- Cubby/Views/Home/AddLocationView.swift | 37 +++++++++++--- Cubby/Views/Home/StorageLocationPicker.swift | 35 +++++++++++-- Cubby/Views/Home/StorageLocationRow.swift | 7 ++- Cubby/Views/Items/AddItemView.swift | 3 ++ .../StorageLocationPickerUITests.swift | 50 +++++++++++++++++++ 5 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 CubbyUITests/StorageLocationPickerUITests.swift diff --git a/Cubby/Views/Home/AddLocationView.swift b/Cubby/Views/Home/AddLocationView.swift index df90433..0f3b360 100644 --- a/Cubby/Views/Home/AddLocationView.swift +++ b/Cubby/Views/Home/AddLocationView.swift @@ -3,6 +3,7 @@ import SwiftUI struct AddLocationView: View { let homeId: UUID? let parentLocation: AppStorageLocation? + let onLocationCreated: ((AppStorageLocation) -> Void)? @Environment(\.dismiss) private var dismiss @Environment(\.sharedHomesGateService) private var sharedHomesGateService @@ -12,10 +13,32 @@ struct AddLocationView: View { @State private var showingError = false @State private var errorMessage = "" + init( + homeId: UUID?, + parentLocation: AppStorageLocation?, + onLocationCreated: ((AppStorageLocation) -> Void)? = nil + ) { + self.homeId = homeId + self.parentLocation = parentLocation + self.onLocationCreated = onLocationCreated + } + private var resolvedHome: AppHome? { parentLocation.flatMap { appStore.home(id: $0.homeID) } ?? appStore.home(id: homeId) } + private var isAddingSubLocation: Bool { + parentLocation != nil + } + + private var title: String { + isAddingSubLocation ? "Add Sub-location" : "Add Storage Location" + } + + private var nameFieldTitle: String { + isAddingSubLocation ? "Sub-location name" : "Location name" + } + var body: some View { NavigationStack { Form { @@ -30,13 +53,13 @@ struct AddLocationView: View { } } - Section("Location Details") { - TextField("Location Name", text: $locationName) + Section(nameFieldTitle) { + TextField(nameFieldTitle, text: $locationName) .textInputAutocapitalization(.words) if let parentLocation { HStack { - Text("Parent Location") + Text("Inside") Spacer() Text(parentLocation.name) .foregroundStyle(.secondary) @@ -46,7 +69,7 @@ struct AddLocationView: View { if let parentLocation { Section { - Label("This location will be nested under \"\(parentLocation.name)\"", systemImage: "info.circle") + Label("Creates a sub-location inside \"\(parentLocation.name)\".", systemImage: "info.circle") .font(.footnote) .foregroundStyle(.secondary) } @@ -58,7 +81,7 @@ struct AddLocationView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { - Text("Add Storage Location") + Text(title) .font(.custom("AwesomeSerif-ExtraTall", size: 20)) .foregroundStyle(.primary) } @@ -68,6 +91,7 @@ struct AddLocationView: View { ToolbarItem(placement: .confirmationAction) { Button("Save") { saveLocation() } .disabled(locationName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !canCreateLocationsInHome) + .accessibilityIdentifier("save-location-button") } } .alert("Error", isPresented: $showingError) { @@ -95,11 +119,12 @@ struct AddLocationView: View { guard !trimmedName.isEmpty else { return } do { - _ = try appStore.createLocation( + let location = try appStore.createLocation( name: trimmedName, homeID: home.id, parentLocationID: parentLocation?.id ) + onLocationCreated?(location) dismiss() } catch { errorMessage = error.localizedDescription diff --git a/Cubby/Views/Home/StorageLocationPicker.swift b/Cubby/Views/Home/StorageLocationPicker.swift index e3d28fc..0784c13 100644 --- a/Cubby/Views/Home/StorageLocationPicker.swift +++ b/Cubby/Views/Home/StorageLocationPicker.swift @@ -38,7 +38,8 @@ struct StorageLocationPicker: View { location: location, selectedLocation: $selectedLocation, expandedLocations: $expandedLocations, - searchText: searchText + searchText: searchText, + onLocationCreated: handleLocationCreated ) } } else { @@ -81,11 +82,29 @@ struct StorageLocationPicker: View { } } .sheet(isPresented: $showingAddLocation) { - AddLocationView(homeId: selectedHomeId, parentLocation: nil) + AddLocationView( + homeId: selectedHomeId, + parentLocation: nil, + onLocationCreated: handleLocationCreated + ) } } } + private func handleLocationCreated(_ location: AppStorageLocation) { + selectedLocation = location + searchText = "" + expandAncestors(of: location) + } + + private func expandAncestors(of location: AppStorageLocation) { + var parentID = location.parentLocationID + while let id = parentID { + expandedLocations.insert(id) + parentID = appStore.location(id: id)?.parentLocationID + } + } + @Environment(\.colorScheme) private var colorScheme private var appBackground: Color { if colorScheme == .light, UIColor(named: "AppBackground") != nil { @@ -101,6 +120,7 @@ struct LocationPickerRow: View { @Binding var selectedLocation: AppStorageLocation? @Binding var expandedLocations: Set let searchText: String + let onLocationCreated: (AppStorageLocation) -> Void @State private var showingAddLocation = false @State private var showingDeleteConfirmation = false @@ -161,7 +181,8 @@ struct LocationPickerRow: View { location: childLocation, selectedLocation: $selectedLocation, expandedLocations: $expandedLocations, - searchText: searchText + searchText: searchText, + onLocationCreated: onLocationCreated ) .padding(.leading, 20) } @@ -187,6 +208,8 @@ struct LocationPickerRow: View { .foregroundStyle(isSelected ? .white : Color.accentColor) } .buttonStyle(.plain) + .accessibilityLabel("Add sub-location under \(location.name)") + .accessibilityHint("Creates a nested storage location inside \(location.name).") } .padding(.vertical, 4) .padding(.horizontal, 8) @@ -213,7 +236,11 @@ struct LocationPickerRow: View { } } .sheet(isPresented: $showingAddLocation) { - AddLocationView(homeId: location.homeID, parentLocation: location) + AddLocationView( + homeId: location.homeID, + parentLocation: location, + onLocationCreated: onLocationCreated + ) } .alert("Delete Location?", isPresented: $showingDeleteConfirmation) { Button("Cancel", role: .cancel) {} diff --git a/Cubby/Views/Home/StorageLocationRow.swift b/Cubby/Views/Home/StorageLocationRow.swift index 3e824d0..39c9224 100644 --- a/Cubby/Views/Home/StorageLocationRow.swift +++ b/Cubby/Views/Home/StorageLocationRow.swift @@ -54,7 +54,12 @@ struct StorageLocationRow: View { locationLabel } .sheet(isPresented: $showingAddLocation) { - AddLocationView(homeId: location.homeID, parentLocation: location) + AddLocationView( + homeId: location.homeID, + parentLocation: location + ) { _ in + expandedLocations.insert(location.id) + } } .alert("Delete Location", isPresented: $showingDeleteAlert) { Button("Cancel", role: .cancel) { } diff --git a/Cubby/Views/Items/AddItemView.swift b/Cubby/Views/Items/AddItemView.swift index 466ee84..a598776 100644 --- a/Cubby/Views/Items/AddItemView.swift +++ b/Cubby/Views/Items/AddItemView.swift @@ -78,6 +78,9 @@ struct AddItemView: View { } } .foregroundColor(.primary) + .accessibilityIdentifier("add-item-location-picker-button") + .accessibilityLabel("Storage Location") + .accessibilityValue(selectedLocation?.fullPath ?? "Select") } Section("Tags") { diff --git a/CubbyUITests/StorageLocationPickerUITests.swift b/CubbyUITests/StorageLocationPickerUITests.swift new file mode 100644 index 0000000..7b5271f --- /dev/null +++ b/CubbyUITests/StorageLocationPickerUITests.swift @@ -0,0 +1,50 @@ +import XCTest + +final class StorageLocationPickerUITests: XCTestCase { + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testSubLocationCreatedFromItemPickerIsSelected() throws { + let app = XCUIApplication(bundleIdentifier: "com.barronroth.Cubby") + app.launchArguments.append(contentsOf: ["UI-TESTING", "SEED_MOCK_DATA", "FORCE_PRO_TIER"]) + app.launch() + + let addItemButton = app.buttons["Add Item"] + XCTAssertTrue(addItemButton.waitForExistence(timeout: 10)) + addItemButton.tap() + + let locationButton = app.buttons["add-item-location-picker-button"] + XCTAssertTrue(locationButton.waitForExistence(timeout: 5)) + locationButton.tap() + + let searchField = app.searchFields["Search locations"] + XCTAssertTrue(searchField.waitForExistence(timeout: 5)) + searchField.tap() + searchField.typeText("Walk-in Closet") + + let addSubLocationButton = app.buttons["Add sub-location under Walk-in Closet"] + XCTAssertTrue(addSubLocationButton.waitForExistence(timeout: 5)) + addSubLocationButton.tap() + + XCTAssertTrue(app.staticTexts["Add Sub-location"].waitForExistence(timeout: 5)) + + let nameField = app.textFields["Sub-location name"] + XCTAssertTrue(nameField.waitForExistence(timeout: 5)) + nameField.tap() + nameField.typeText("Top Shelf") + + app.buttons["save-location-button"].tap() + + XCTAssertTrue(app.staticTexts["Top Shelf"].waitForExistence(timeout: 5)) + app.navigationBars.buttons["Done"].tap() + + let updatedLocationButton = app.buttons["add-item-location-picker-button"] + XCTAssertTrue(updatedLocationButton.waitForExistence(timeout: 5)) + XCTAssertEqual( + updatedLocationButton.value as? String, + "Master Bedroom > Walk-in Closet > Top Shelf" + ) + } +}