// // RemindWallUITests.swift // RemindWallUITests // // Created by Christopher Hotchkiss on 3/2/26. // import XCTest final class RemindWallUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false } @MainActor private func launchApp() -> XCUIApplication { let app = XCUIApplication() app.launchEnvironment["UITesting"] = "true" app.launch() return app } /// On iPad, Form section header toggles render as full-width cells. /// Tapping the center hits the label, not the switch. This helper taps the actual switch control. @MainActor private func tapToggle(_ toggle: XCUIElement) { let innerSwitch = toggle.switches.firstMatch if innerSwitch.exists { innerSwitch.tap() } else { toggle.tap() } } @MainActor func testSettingsScreenLoads() throws { let app = launchApp() let settingsNav = app.navigationBars["Settings"] XCTAssertTrue(settingsNav.waitForExistence(timeout: 5), "Settings navigation bar should appear") } @MainActor func testStartSlideshowButtonExists() throws { let app = launchApp() let startButton = app.buttons["Start Slideshow"] XCTAssertTrue(startButton.waitForExistence(timeout: 5), "Start Slideshow button should exist") } @MainActor func testScreenOffToggleExists() throws { let app = launchApp() let screenOffToggle = app.switches["Screen Off"] XCTAssertTrue(screenOffToggle.waitForExistence(timeout: 5), "Screen Off toggle should appear in section header") } @MainActor func testScreenOffToggleShowsTimePickers() throws { let app = launchApp() let screenOffToggle = app.switches["Screen Off"] XCTAssertTrue(screenOffToggle.waitForExistence(timeout: 5), "Screen Off toggle should exist") // Enable screen off if screenOffToggle.value as? String == "0" { tapToggle(screenOffToggle) } // Verify the time picker controls appear let screenOffLabel = app.descendants(matching: .any)["SCREEN OFF"] XCTAssertTrue(screenOffLabel.waitForExistence(timeout: 5), "SCREEN OFF label should appear when enabled") let screenOnLabel = app.descendants(matching: .any)["SCREEN ON"] XCTAssertTrue(screenOnLabel.waitForExistence(timeout: 5), "SCREEN ON label should appear when enabled") } @MainActor func testScreenOffToggleHidesTimePickers() throws { let app = launchApp() let screenOffToggle = app.switches["Screen Off"] XCTAssertTrue(screenOffToggle.waitForExistence(timeout: 5), "Screen Off toggle should exist") // Enable then disable if screenOffToggle.value as? String == "0" { tapToggle(screenOffToggle) } // Now disable tapToggle(screenOffToggle) // Verify the time picker controls disappear let screenOffLabel = app.descendants(matching: .any)["SCREEN OFF"] XCTAssertFalse(screenOffLabel.exists, "SCREEN OFF label should not appear when disabled") } @MainActor func testDeleteReminderWithButton() throws { let app = launchApp() // Wait for Settings to load let settingsNav = app.navigationBars["Settings"] XCTAssertTrue(settingsNav.waitForExistence(timeout: 5), "Settings should load") // Navigate to a trackee detail (seed data provides "Alice") let aliceCell = app.cells.containing(.staticText, identifier: "Alice").firstMatch XCTAssertTrue(aliceCell.waitForExistence(timeout: 10), "Alice trackee should appear") aliceCell.tap() // Wait for the trackee detail to load let deleteAliceButton = app.buttons["Delete Alice"] XCTAssertTrue(deleteAliceButton.waitForExistence(timeout: 10), "Alice detail screen should load") // Tap the add reminder button let addButton = app.buttons["Add"] XCTAssertTrue(addButton.waitForExistence(timeout: 5), "Add button should appear") addButton.tap() // Verify Add Reminder sheet appears let addReminderNav = app.navigationBars["Add Reminder"] XCTAssertTrue(addReminderNav.waitForExistence(timeout: 5), "Add Reminder sheet should appear") // Save the default reminder (Sunday) let saveButton = app.buttons["Save"] XCTAssertTrue(saveButton.waitForExistence(timeout: 5), "Save button should appear") saveButton.tap() // Wait for the reminder row to appear let sundayText = app.staticTexts["Sunday"] XCTAssertTrue(sundayText.waitForExistence(timeout: 10), "Reminder row should appear after saving") // Tap the trash button to delete the reminder let trashButton = app.buttons["Delete Reminder"] XCTAssertTrue(trashButton.waitForExistence(timeout: 5), "Per-row delete button should appear") trashButton.tap() // Verify the reminder is gone let reminderGone = sundayText.waitForNonExistence(timeout: 5) XCTAssertTrue(reminderGone, "Reminder should be deleted after tapping delete button") } @MainActor func testAddReminderSheetStaysOpen() throws { let app = launchApp() // Wait for Settings to load let settingsNav = app.navigationBars["Settings"] XCTAssertTrue(settingsNav.waitForExistence(timeout: 5), "Settings should load") // Navigate to a trackee detail (seed data provides "Alice") // On Catalyst, NavigationLink in a Form may render multiple button elements; // use the first matching cell to navigate. let aliceCell = app.cells.containing(.staticText, identifier: "Alice").firstMatch XCTAssertTrue(aliceCell.waitForExistence(timeout: 10), "Alice trackee should appear") aliceCell.tap() // Wait for the trackee detail to load — check for content that appears on the detail screen let deleteButton = app.buttons["Delete Alice"] XCTAssertTrue(deleteButton.waitForExistence(timeout: 10), "Alice detail screen should appear with Delete button") // Tap the add reminder button (the "+" toolbar button) let addButton = app.buttons["Add"] XCTAssertTrue(addButton.waitForExistence(timeout: 5), "Add button should appear in toolbar") addButton.tap() // Verify the Add Reminder sheet stays visible and doesn't immediately dismiss let addReminderNav = app.navigationBars["Add Reminder"] XCTAssertTrue(addReminderNav.waitForExistence(timeout: 5), "Add Reminder sheet should stay open after tapping add button") // Verify key content is visible on the sheet let saveButton = app.buttons["Save"] XCTAssertTrue(saveButton.exists, "Save button should be visible on the Add Reminder sheet") let cancelButton = app.buttons["Cancel"] XCTAssertTrue(cancelButton.exists, "Cancel button should be visible on the Add Reminder sheet") } @MainActor func testDashboardScreenLoads() throws { let app = launchApp() // Wait for Settings to load let startButton = app.buttons["Start Slideshow"] XCTAssertTrue(startButton.waitForExistence(timeout: 5), "Start Slideshow button should exist") // Tap to navigate to Dashboard startButton.tap() // Settings nav should disappear, confirming navigation to Dashboard let settingsNav = app.navigationBars["Settings"] let settingsGone = settingsNav.waitForNonExistence(timeout: 10) XCTAssertTrue(settingsGone, "Settings navigation bar should disappear after starting slideshow") } @MainActor func testSlideshowAdvancesPhotos() throws { let app = launchApp() // Handle photo library permission dialog if it appears addUIInterruptionMonitor(withDescription: "Photo Library Access") { alert in let allowButton = alert.buttons["Allow Full Access"] if allowButton.exists { allowButton.tap() return true } let okButton = alert.buttons["OK"] if okButton.exists { okButton.tap() return true } return false } // Wait for Settings to load let settingsNav = app.navigationBars["Settings"] XCTAssertTrue(settingsNav.waitForExistence(timeout: 5), "Settings should load") // Enable the Slideshow toggle if not already on let slideshowToggle = app.switches["Slideshow"] XCTAssertTrue(slideshowToggle.waitForExistence(timeout: 5), "Slideshow toggle should exist") if slideshowToggle.value as? String == "0" { tapToggle(slideshowToggle) } // If photo authorization is needed, tap "Authorize Photo Access" first let authButton = app.buttons["Authorize Photo Access"] if authButton.waitForExistence(timeout: 3) { authButton.tap() app.tap() // trigger interruption monitor for system dialog } // Wait for albums to load and the picker to appear. // On macCatalyst the Picker("Albums",...) with .navigationLink style renders as a button. // Skip if photo library isn't authorized (environment-dependent) let albumButton = app.buttons["Albums"] try XCTSkipUnless(albumButton.waitForExistence(timeout: 15), "Skipping: photo library not authorized or no albums available") // Tap the Albums picker - on Catalyst this pushes a navigation view with album options albumButton.tap() // On macCatalyst, the navigationLink picker pushes a CollectionView with album cells. // Each cell contains a Button with the album name. // The first album may already appear Selected (default), so tap the second one // to ensure a real selection change that triggers navigation back. let pickerCollection = app.collectionViews.firstMatch XCTAssertTrue(pickerCollection.waitForExistence(timeout: 5), "Picker should push a collection view with album options") let albumButtons = pickerCollection.buttons try XCTSkipUnless(albumButtons.count >= 1, "Skipping: no album buttons available on this simulator") // Tap an album — prefer the second one for a different selection, fall back to first let albumIndex = albumButtons.count >= 2 ? 1 : 0 albumButtons.element(boundBy: albumIndex).tap() // On macCatalyst, the picker may not auto-navigate back. // If "Start Slideshow" doesn't appear, tap the Back button to navigate back. let startButton = app.buttons["Start Slideshow"] if !startButton.waitForExistence(timeout: 3) { let backButton = app.navigationBars.buttons["Settings"] if backButton.exists { backButton.tap() } } // After selecting, the picker should pop back to Settings. // Wait for "Start Slideshow" to become available. XCTAssertTrue(startButton.waitForExistence(timeout: 10), "Start Slideshow button should exist after selecting album") startButton.tap() // Verify we navigated to the Dashboard. // SwiftUI merges child accessibility elements into the parent DashboardView, // so the photo's accessibility value (asset localIdentifier) appears on the // DashboardView element itself rather than on a separate SlideshowPhoto element. let dashboard = app.descendants(matching: .any)["DashboardView"] XCTAssertTrue(dashboard.waitForExistence(timeout: 15), "Dashboard should appear after starting slideshow") // Wait for a photo to load - the dashboard's value will contain the asset identifier let hasValue = NSPredicate(format: "value != nil AND value != ''") let valueLoaded = XCTNSPredicateExpectation(predicate: hasValue, object: dashboard) let loadResult = XCTWaiter.wait(for: [valueLoaded], timeout: 15) XCTAssertEqual(loadResult, .completed, "Dashboard should load a photo with an asset identifier value") // Record the initial photo's accessibility value let initialValue = dashboard.value as? String ?? "" XCTAssertFalse(initialValue.isEmpty, "Dashboard should have a value with the asset identifier") // Wait for the slideshow to advance (timer is 10 seconds + buffer) let expectation = XCTNSPredicateExpectation( predicate: NSPredicate(format: "value != %@", initialValue), object: dashboard ) let result = XCTWaiter.wait(for: [expectation], timeout: 20) XCTAssertEqual(result, .completed, "Slideshow should advance to a different photo within 20 seconds") } @MainActor func testTapSlideshowReturnsToSettings() throws { let app = launchApp() // Handle photo library permission dialog if it appears addUIInterruptionMonitor(withDescription: "Photo Library Access") { alert in let allowButton = alert.buttons["Allow Full Access"] if allowButton.exists { allowButton.tap() return true } let okButton = alert.buttons["OK"] if okButton.exists { okButton.tap() return true } return false } // Wait for Settings to load let settingsNav = app.navigationBars["Settings"] XCTAssertTrue(settingsNav.waitForExistence(timeout: 5), "Settings should load") // Enable the Slideshow toggle if not already on let slideshowToggle = app.switches["Slideshow"] XCTAssertTrue(slideshowToggle.waitForExistence(timeout: 5), "Slideshow toggle should exist") if slideshowToggle.value as? String == "0" { tapToggle(slideshowToggle) } // If photo authorization is needed, tap "Authorize Photo Access" first let authButton = app.buttons["Authorize Photo Access"] if authButton.waitForExistence(timeout: 3) { authButton.tap() app.tap() } // Select an album — skip if photo library isn't authorized let albumButton = app.buttons["Albums"] try XCTSkipUnless(albumButton.waitForExistence(timeout: 15), "Skipping: photo library not authorized or no albums available") albumButton.tap() let pickerCollection = app.collectionViews.firstMatch XCTAssertTrue(pickerCollection.waitForExistence(timeout: 5), "Picker should push a collection view") let albumButtons = pickerCollection.buttons try XCTSkipUnless(albumButtons.count >= 1, "Skipping: no album buttons available on this simulator") // Tap an album — prefer the second one for a different selection, fall back to first let albumIndex = albumButtons.count >= 2 ? 1 : 0 albumButtons.element(boundBy: albumIndex).tap() let startButton = app.buttons["Start Slideshow"] if !startButton.waitForExistence(timeout: 3) { let backButton = app.navigationBars.buttons["Settings"] if backButton.exists { backButton.tap() } } XCTAssertTrue(startButton.waitForExistence(timeout: 10), "Start Slideshow button should exist after selecting album") startButton.tap() // Wait for the Dashboard with a loaded photo let dashboard = app.descendants(matching: .any)["DashboardView"] XCTAssertTrue(dashboard.waitForExistence(timeout: 15), "Dashboard should appear after starting slideshow") let hasValue = NSPredicate(format: "value != nil AND value != ''") let valueLoaded = XCTNSPredicateExpectation(predicate: hasValue, object: dashboard) let loadResult = XCTWaiter.wait(for: [valueLoaded], timeout: 15) XCTAssertEqual(loadResult, .completed, "Dashboard should load a photo") // Tap the slideshow image to return to settings dashboard.tap() // Verify we're back on the Settings screen XCTAssertTrue(settingsNav.waitForExistence(timeout: 10), "Settings should reappear after tapping the slideshow") } }