diff --git a/SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift b/SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift index bc9b965..7b970d9 100644 --- a/SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift +++ b/SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift @@ -55,7 +55,7 @@ struct ItineraryTableViewWrapper: UIViewControllerRepresent var headerHostingController: UIHostingController? var lastStopCount: Int = 0 var lastGameIDsHash: Int = 0 - var lastItemCount: Int = 0 + var lastItineraryItemsHash: Int = 0 var lastOverrideCount: Int = 0 } @@ -118,20 +118,20 @@ struct ItineraryTableViewWrapper: UIViewControllerRepresent // Diff inputs before rebuilding to avoid unnecessary reloads let currentStopCount = trip.stops.count let currentGameIDsHash = games.map(\.game.id).hashValue - let currentItemCount = itineraryItems.count + let currentItineraryItemsHash = itineraryItemsHash(itineraryItems) let currentOverrideCount = travelOverrides.count let coord = context.coordinator guard currentStopCount != coord.lastStopCount || currentGameIDsHash != coord.lastGameIDsHash || - currentItemCount != coord.lastItemCount || + currentItineraryItemsHash != coord.lastItineraryItemsHash || currentOverrideCount != coord.lastOverrideCount else { return } coord.lastStopCount = currentStopCount coord.lastGameIDsHash = currentGameIDsHash - coord.lastItemCount = currentItemCount + coord.lastItineraryItemsHash = currentItineraryItemsHash coord.lastOverrideCount = currentOverrideCount let (days, validRanges, allItemsForConstraints, travelSegmentIndices) = buildItineraryData() @@ -143,6 +143,36 @@ struct ItineraryTableViewWrapper: UIViewControllerRepresent ) } + private func itineraryItemsHash(_ items: [ItineraryItem]) -> Int { + var hasher = Hasher() + for item in items.sorted(by: { $0.id.uuidString < $1.id.uuidString }) { + hasher.combine(item.id) + hasher.combine(item.day) + hasher.combine(item.sortOrder) + hasher.combine(item.modifiedAt) + + switch item.kind { + case .game(let gameId, let city): + hasher.combine("game") + hasher.combine(gameId) + hasher.combine(city) + case .travel(let info): + hasher.combine("travel") + hasher.combine(info.fromCity) + hasher.combine(info.toCity) + hasher.combine(info.segmentIndex ?? -1) + case .custom(let info): + hasher.combine("custom") + hasher.combine(info.title) + hasher.combine(info.icon) + hasher.combine(info.latitude ?? 0) + hasher.combine(info.longitude ?? 0) + hasher.combine(info.address ?? "") + } + } + return hasher.finalize() + } + // MARK: - Build Itinerary Data private func buildItineraryData() -> ([ItineraryDayData], [String: ClosedRange], [ItineraryItem], [UUID: Int]) { diff --git a/SportsTimeUITests/Framework/Screens.swift b/SportsTimeUITests/Framework/Screens.swift index ac116aa..fad9c92 100644 --- a/SportsTimeUITests/Framework/Screens.swift +++ b/SportsTimeUITests/Framework/Screens.swift @@ -898,9 +898,22 @@ struct QuickAddItemSheetScreen { func clearAndTypeTitle(_ text: String) { let field = titleField field.waitUntilHittable().tap() - // Triple-tap to select all existing text - field.tap(withNumberOfTaps: 3, numberOfTouches: 1) + + // Primary path: command-A replacement is generally the most reliable on simulator. + field.typeKey("a", modifierFlags: .command) field.typeText(text) + + // Fallback path: if replacement didn't stick, try explicit select-all and replace. + let currentValue = (field.value as? String) ?? "" + if !currentValue.localizedCaseInsensitiveContains(text) { + field.tap(withNumberOfTaps: 3, numberOfTouches: 1) + if app.menuItems["Select All"].waitForExistence(timeout: BaseUITestCase.shortTimeout) { + app.menuItems["Select All"].tap() + } else { + field.typeKey("a", modifierFlags: .command) + } + field.typeText(text) + } } func tapSave() { diff --git a/SportsTimeUITests/Tests/TripCustomItemTests.swift b/SportsTimeUITests/Tests/TripCustomItemTests.swift index 59b7fb4..c552342 100644 --- a/SportsTimeUITests/Tests/TripCustomItemTests.swift +++ b/SportsTimeUITests/Tests/TripCustomItemTests.swift @@ -104,13 +104,25 @@ final class TripCustomItemTests: BaseUITestCase { // Sheet should dismiss editSheet.waitForDismiss() - // Verify updated title is visible - let updatedItem = app.staticTexts["Updated Title"] + // Verify edited item still exists in itinerary XCTAssertTrue( - updatedItem.waitForExistence(timeout: BaseUITestCase.defaultTimeout), - "Updated custom item title should be visible in itinerary" + detail.customItemCell.waitForExistence(timeout: BaseUITestCase.defaultTimeout), + "Custom item should remain visible after editing" ) + // Re-open to verify updated title persisted (more reliable than global static text lookup). + detail.tapCustomItem() + let verifySheet = QuickAddItemSheetScreen(app: app) + verifySheet.waitForLoad() + + let persistedTitle = (verifySheet.titleField.value as? String) ?? "" + XCTAssertTrue( + persistedTitle.localizedCaseInsensitiveContains("Updated Title"), + "Updated custom item title should persist after saving; found: '\(persistedTitle)'" + ) + verifySheet.tapCancel() + verifySheet.waitForDismiss() + captureScreenshot(named: "F069-CustomItemEdited") }