diff --git a/SportsTime/Features/Trip/Views/AddItem/QuickAddItemSheet.swift b/SportsTime/Features/Trip/Views/AddItem/QuickAddItemSheet.swift index 82a1776..ef16131 100644 --- a/SportsTime/Features/Trip/Views/AddItem/QuickAddItemSheet.swift +++ b/SportsTime/Features/Trip/Views/AddItem/QuickAddItemSheet.swift @@ -80,6 +80,7 @@ struct QuickAddItemSheet: View { .foregroundStyle(canSave ? Theme.warmOrange : Theme.textMuted(colorScheme)) .disabled(!canSave) .accessibilityLabel(saveButtonAccessibilityLabel) + .accessibilityIdentifier("quickAdd.saveButton") } } .sheet(isPresented: $showLocationSearch) { @@ -137,6 +138,7 @@ struct QuickAddItemSheet: View { ) .accessibilityLabel("Item description") .accessibilityHint(placeholderText) + .accessibilityIdentifier("quickAdd.titleField") // Character hint if !title.isEmpty { diff --git a/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift b/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift index 7421ec3..006db92 100644 --- a/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift +++ b/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift @@ -1113,6 +1113,7 @@ final class ItineraryTableViewController: UITableViewController { cell.backgroundColor = .clear cell.selectionStyle = .default // Shows highlight on tap + cell.accessibilityIdentifier = "tripDetail.customItem" } } @@ -1175,6 +1176,7 @@ struct DaySectionHeaderView: View { .tint(Theme.warmOrange) .accessibilityLabel("Add item to Day \(dayNumber)") .accessibilityHint("Add restaurants, activities, or notes to this day") + .accessibilityIdentifier("tripDetail.addItemButton") } .padding(.horizontal, Theme.Spacing.lg) .padding(.top, Theme.Spacing.lg) // More space above for section separation diff --git a/SportsTimeUITests/Framework/Screens.swift b/SportsTimeUITests/Framework/Screens.swift index b6351a7..7a89a6f 100644 --- a/SportsTimeUITests/Framework/Screens.swift +++ b/SportsTimeUITests/Framework/Screens.swift @@ -456,6 +456,54 @@ struct TripDetailScreen { XCTAssertEqual(favoriteButton.label, expected, "Favorite button label should reflect saved state") } + + // MARK: - Custom Item Elements + + /// The "Add" button on any day header row (first match). + var addItemButton: XCUIElement { + app.buttons["tripDetail.addItemButton"].firstMatch + } + + /// A custom item cell in the itinerary (first match). + var customItemCell: XCUIElement { + app.cells["tripDetail.customItem"].firstMatch + } + + // MARK: - Custom Item Actions + + /// Scrolls to and taps the first "Add" button on a day header. + func tapAddItem() { + let button = addItemButton + var scrollAttempts = 0 + while !(button.exists && button.isHittable) && scrollAttempts < 15 { + app.swipeUp(velocity: .slow) + scrollAttempts += 1 + } + button.waitUntilHittable().tap() + } + + /// Taps a custom item cell to open the edit sheet. + func tapCustomItem() { + let cell = customItemCell + var scrollAttempts = 0 + while !(cell.exists && cell.isHittable) && scrollAttempts < 15 { + app.swipeUp(velocity: .slow) + scrollAttempts += 1 + } + cell.waitUntilHittable().tap() + } + + /// Long-presses a custom item to show the context menu. + func longPressCustomItem() { + let cell = customItemCell + var scrollAttempts = 0 + while !(cell.exists && cell.isHittable) && scrollAttempts < 15 { + app.swipeUp(velocity: .slow) + scrollAttempts += 1 + } + cell.waitUntilHittable() + cell.press(forDuration: 1.0) + } } // MARK: - My Trips Screen @@ -792,6 +840,68 @@ struct StadiumVisitSheetScreen { } } +// MARK: - Quick Add Item Sheet Screen + +struct QuickAddItemSheetScreen { + let app: XCUIApplication + + // MARK: Elements + + var titleField: XCUIElement { + app.textFields["quickAdd.titleField"] + } + + var saveButton: XCUIElement { + app.buttons["quickAdd.saveButton"] + } + + var cancelButton: XCUIElement { + app.navigationBars.buttons["Cancel"] + } + + // MARK: Actions + + @discardableResult + func waitForLoad() -> QuickAddItemSheetScreen { + titleField.waitForExistenceOrFail( + timeout: BaseUITestCase.defaultTimeout, + "Quick add item sheet should appear with title field" + ) + return self + } + + /// Waits for the sheet to dismiss by checking the title field disappears. + func waitForDismiss() { + titleField.waitForNonExistence( + timeout: BaseUITestCase.defaultTimeout, + "Quick add item sheet should dismiss" + ) + } + + /// Types a title into the description field. + func typeTitle(_ text: String) { + titleField.waitUntilHittable().tap() + titleField.typeText(text) + } + + /// Clears existing text and types a new title (for edit mode). + func clearAndTypeTitle(_ text: String) { + let field = titleField + field.waitUntilHittable().tap() + // Triple-tap to select all existing text + field.tap(withNumberOfTaps: 3, numberOfTouches: 1) + field.typeText(text) + } + + func tapSave() { + saveButton.waitUntilHittable().tap() + } + + func tapCancel() { + cancelButton.waitUntilHittable().tap() + } +} + // MARK: - Games History Screen struct GamesHistoryScreen { @@ -980,4 +1090,36 @@ enum TestFlows { return (wizard, detail) } + + /// Plans a trip, saves it, navigates back to My Trips, and opens the saved trip. + /// Returns a TripDetailScreen with `allowCustomItems` enabled. + @MainActor @discardableResult + static func planSaveAndOpenFromMyTrips( + app: XCUIApplication + ) -> TripDetailScreen { + let (wizard, detail) = planAndSelectFirstTrip(app: app) + + // Save the trip + detail.assertSaveState(isSaved: false) + detail.tapFavorite() + detail.assertSaveState(isSaved: true) + + // Navigate back: Detail → Options → Wizard → Cancel + app.navigationBars.buttons.firstMatch.tap() + let optionsBackBtn = app.navigationBars.buttons.firstMatch + optionsBackBtn.waitUntilHittable(timeout: BaseUITestCase.shortTimeout).tap() + wizard.tapCancel() + + // Switch to My Trips tab and open the saved trip + let home = HomeScreen(app: app) + home.switchToTab(home.myTripsTab) + + let myTrips = MyTripsScreen(app: app) + myTrips.assertHasTrips() + myTrips.tapTrip(at: 0) + + let savedDetail = TripDetailScreen(app: app) + savedDetail.waitForLoad() + return savedDetail + } } diff --git a/SportsTimeUITests/Tests/TripCustomItemTests.swift b/SportsTimeUITests/Tests/TripCustomItemTests.swift new file mode 100644 index 0000000..59b7fb4 --- /dev/null +++ b/SportsTimeUITests/Tests/TripCustomItemTests.swift @@ -0,0 +1,153 @@ +// +// TripCustomItemTests.swift +// SportsTimeUITests +// +// Tests for custom itinerary item lifecycle: add, edit, delete. +// QA Sheet: F-068, F-069, F-070 +// + +import XCTest + +final class TripCustomItemTests: BaseUITestCase { + + // MARK: - Helpers + + /// Plans a trip, saves it, and opens it from My Trips (enables custom items). + @MainActor + private func openSavedTripDetail() -> TripDetailScreen { + return TestFlows.planSaveAndOpenFromMyTrips(app: app) + } + + /// Adds a custom item with the given title via the add sheet flow. + @MainActor + private func addCustomItem(detail: TripDetailScreen, title: String) { + detail.tapAddItem() + + let addSheet = QuickAddItemSheetScreen(app: app) + addSheet.waitForLoad() + addSheet.typeTitle(title) + addSheet.tapSave() + + // Wait for sheet to dismiss + addSheet.waitForDismiss() + } + + // MARK: - F-068: Add custom item to day + + /// F-068: Open saved trip → tap Add on day header → fill in title → save → item appears. + @MainActor + func testF068_AddCustomItemToDay() { + let detail = openSavedTripDetail() + + // Tap "Add" on a day header + detail.tapAddItem() + + let addSheet = QuickAddItemSheetScreen(app: app) + addSheet.waitForLoad() + + // Save button should be disabled without a title + XCTAssertFalse( + addSheet.saveButton.isEnabled, + "Save button should be disabled without a title" + ) + + // Type a description + addSheet.typeTitle("Lunch at Pizzeria") + + // Save button should now be enabled + XCTAssertTrue( + addSheet.saveButton.isEnabled, + "Save button should be enabled after entering a title" + ) + + addSheet.tapSave() + + // Sheet should dismiss + addSheet.waitForDismiss() + + // Custom item should appear in the itinerary + let customItem = detail.customItemCell + XCTAssertTrue( + customItem.waitForExistence(timeout: BaseUITestCase.defaultTimeout), + "Custom item should appear in itinerary after adding" + ) + + captureScreenshot(named: "F068-CustomItemAdded") + } + + // MARK: - F-069: Edit custom item + + /// F-069: Add a custom item → tap to edit → change title → save → updated title visible. + @MainActor + func testF069_EditCustomItem() { + let detail = openSavedTripDetail() + + // Add an item first + addCustomItem(detail: detail, title: "Original Title") + + // Verify item exists + XCTAssertTrue( + detail.customItemCell.waitForExistence(timeout: BaseUITestCase.defaultTimeout), + "Custom item should exist before editing" + ) + + // Tap the custom item to open edit sheet + detail.tapCustomItem() + + let editSheet = QuickAddItemSheetScreen(app: app) + editSheet.waitForLoad() + + // Clear and type new title + editSheet.clearAndTypeTitle("Updated Title") + editSheet.tapSave() + + // Sheet should dismiss + editSheet.waitForDismiss() + + // Verify updated title is visible + let updatedItem = app.staticTexts["Updated Title"] + XCTAssertTrue( + updatedItem.waitForExistence(timeout: BaseUITestCase.defaultTimeout), + "Updated custom item title should be visible in itinerary" + ) + + captureScreenshot(named: "F069-CustomItemEdited") + } + + // MARK: - F-070: Delete custom item + + /// F-070: Add a custom item → long-press → tap Delete in context menu → item removed. + @MainActor + func testF070_DeleteCustomItem() { + let detail = openSavedTripDetail() + + // Add an item first + addCustomItem(detail: detail, title: "Item to Delete") + + // Verify item exists + let customItem = detail.customItemCell + XCTAssertTrue( + customItem.waitForExistence(timeout: BaseUITestCase.defaultTimeout), + "Custom item should exist before deletion" + ) + + // Long-press to show context menu + detail.longPressCustomItem() + + // Tap "Delete" in the context menu + let deleteButton = app.buttons["Delete"] + XCTAssertTrue( + deleteButton.waitForExistence(timeout: BaseUITestCase.shortTimeout), + "Delete button should appear in context menu" + ) + deleteButton.tap() + + // Custom item should disappear + customItem.waitForNonExistence( + timeout: BaseUITestCase.defaultTimeout, + "Custom item should be removed after deletion" + ) + + captureScreenshot(named: "F070-CustomItemDeleted") + } +} diff --git a/docs/SportsTime_QA_Test_Plan.xlsx b/docs/SportsTime_QA_Test_Plan.xlsx index 7d1f5d7..638cc4e 100644 Binary files a/docs/SportsTime_QA_Test_Plan.xlsx and b/docs/SportsTime_QA_Test_Plan.xlsx differ