Add F-068, F-069, F-070 UI tests for custom itinerary item lifecycle
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,7 @@ struct QuickAddItemSheet: View {
|
|||||||
.foregroundStyle(canSave ? Theme.warmOrange : Theme.textMuted(colorScheme))
|
.foregroundStyle(canSave ? Theme.warmOrange : Theme.textMuted(colorScheme))
|
||||||
.disabled(!canSave)
|
.disabled(!canSave)
|
||||||
.accessibilityLabel(saveButtonAccessibilityLabel)
|
.accessibilityLabel(saveButtonAccessibilityLabel)
|
||||||
|
.accessibilityIdentifier("quickAdd.saveButton")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showLocationSearch) {
|
.sheet(isPresented: $showLocationSearch) {
|
||||||
@@ -137,6 +138,7 @@ struct QuickAddItemSheet: View {
|
|||||||
)
|
)
|
||||||
.accessibilityLabel("Item description")
|
.accessibilityLabel("Item description")
|
||||||
.accessibilityHint(placeholderText)
|
.accessibilityHint(placeholderText)
|
||||||
|
.accessibilityIdentifier("quickAdd.titleField")
|
||||||
|
|
||||||
// Character hint
|
// Character hint
|
||||||
if !title.isEmpty {
|
if !title.isEmpty {
|
||||||
|
|||||||
@@ -1113,6 +1113,7 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
|
|
||||||
cell.backgroundColor = .clear
|
cell.backgroundColor = .clear
|
||||||
cell.selectionStyle = .default // Shows highlight on tap
|
cell.selectionStyle = .default // Shows highlight on tap
|
||||||
|
cell.accessibilityIdentifier = "tripDetail.customItem"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1175,6 +1176,7 @@ struct DaySectionHeaderView: View {
|
|||||||
.tint(Theme.warmOrange)
|
.tint(Theme.warmOrange)
|
||||||
.accessibilityLabel("Add item to Day \(dayNumber)")
|
.accessibilityLabel("Add item to Day \(dayNumber)")
|
||||||
.accessibilityHint("Add restaurants, activities, or notes to this day")
|
.accessibilityHint("Add restaurants, activities, or notes to this day")
|
||||||
|
.accessibilityIdentifier("tripDetail.addItemButton")
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Theme.Spacing.lg)
|
.padding(.horizontal, Theme.Spacing.lg)
|
||||||
.padding(.top, Theme.Spacing.lg) // More space above for section separation
|
.padding(.top, Theme.Spacing.lg) // More space above for section separation
|
||||||
|
|||||||
@@ -456,6 +456,54 @@ struct TripDetailScreen {
|
|||||||
XCTAssertEqual(favoriteButton.label, expected,
|
XCTAssertEqual(favoriteButton.label, expected,
|
||||||
"Favorite button label should reflect saved state")
|
"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
|
// 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
|
// MARK: - Games History Screen
|
||||||
|
|
||||||
struct GamesHistoryScreen {
|
struct GamesHistoryScreen {
|
||||||
@@ -980,4 +1090,36 @@ enum TestFlows {
|
|||||||
|
|
||||||
return (wizard, detail)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
153
SportsTimeUITests/Tests/TripCustomItemTests.swift
Normal file
153
SportsTimeUITests/Tests/TripCustomItemTests.swift
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user