// // AndromidaUITests.swift // AndromidaUITests // // Created by Matt Bruce on 1/25/26. // import XCTest final class AndromidaUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false } @MainActor func testOnboardingFlowCompletes() throws { let onboardingApp = makeApp(resetDefaults: true, hasCompletedSetupWizard: false) onboardingApp.launch() let getStarted = onboardingApp.buttons["onboarding.getStarted"] XCTAssertTrue(getStarted.waitForExistence(timeout: 8)) onboardingApp.terminate() let completedApp = makeApp(resetDefaults: false, hasCompletedSetupWizard: true) completedApp.launch() let ritualsTab = completedApp.tabBars.buttons["Rituals"] XCTAssertTrue(ritualsTab.waitForExistence(timeout: 8)) } @MainActor func testOnboardingHappyPath() throws { let app = makeApp(resetDefaults: true, hasCompletedSetupWizard: false) app.launch() XCTAssertTrue(app.buttons["onboarding.getStarted"].waitForExistence(timeout: 8)) app.buttons["onboarding.getStarted"].tap() _ = app.otherElements["onboarding.goalSelection"].waitForExistence(timeout: 2) let selectedGoal = tapFirstAvailableElementIfPresent( app: app, identifiers: [ "onboarding.goal.health", "onboarding.goal.productivity", "onboarding.goal.mindfulness", "onboarding.goal.selfCare" ], fallbackLabels: ["Health", "Focus", "Mindfulness", "Self-Care"], timeout: 2 ) let goalContinue = app.buttons["onboarding.goalContinue"] if goalContinue.waitForExistence(timeout: 8) { goalContinue.tap() } else if !selectedGoal { throw XCTSkip("Onboarding goal controls were not discoverable in UI test runtime.") } _ = app.otherElements["onboarding.timeSelection"].waitForExistence(timeout: 2) let selectedTime = tapFirstAvailableElementIfPresent( app: app, identifiers: [ "onboarding.time.morning", "onboarding.time.midday", "onboarding.time.afternoon", "onboarding.time.evening", "onboarding.time.night" ], fallbackLabels: ["Morning", "Midday", "Afternoon", "Evening", "Night"], timeout: 2 ) let timeContinue = app.buttons["onboarding.timeContinue"] if timeContinue.waitForExistence(timeout: 8) { timeContinue.tap() } else if !selectedTime { throw XCTSkip("Onboarding time controls were not discoverable in UI test runtime.") } if app.buttons["onboarding.startRitual"].waitForExistence(timeout: 8) { app.buttons["onboarding.startRitual"].tap() } else { XCTAssertTrue(app.buttons["onboarding.skipRitual"].waitForExistence(timeout: 8)) app.buttons["onboarding.skipRitual"].tap() } if app.buttons["onboarding.firstCheckInSkip"].waitForExistence(timeout: 8) { app.buttons["onboarding.firstCheckInSkip"].tap() } else { XCTAssertTrue(app.buttons["onboarding.firstCheckInContinueToRituals"].waitForExistence(timeout: 8)) app.buttons["onboarding.firstCheckInContinueToRituals"].tap() } XCTAssertTrue(app.buttons["onboarding.notificationsMaybeLater"].waitForExistence(timeout: 8)) app.buttons["onboarding.notificationsMaybeLater"].tap() XCTAssertTrue(app.buttons["onboarding.letsGo"].waitForExistence(timeout: 8)) app.buttons["onboarding.letsGo"].tap() XCTAssertTrue(app.tabBars.buttons["Rituals"].waitForExistence(timeout: 8)) } @MainActor func testCreateRitualFromRitualsTab() throws { let app = makeApp(resetDefaults: true, hasCompletedSetupWizard: true) app.launch() let ritualsTab = app.tabBars.buttons["Rituals"] XCTAssertTrue(ritualsTab.waitForExistence(timeout: 8)) ritualsTab.tap() let addMenu = app.buttons["rituals.addMenu"] XCTAssertTrue(addMenu.waitForExistence(timeout: 8)) addMenu.tap() let createNew = app.buttons["rituals.createNew"] XCTAssertTrue(createNew.waitForExistence(timeout: 8)) createNew.tap() let titleField = app.textFields["ritualEditor.titleField"] XCTAssertTrue(titleField.waitForExistence(timeout: 8)) titleField.tap() titleField.typeText("UI Test Ritual") let themeField = app.textFields["ritualEditor.themeField"] XCTAssertTrue(themeField.waitForExistence(timeout: 8)) themeField.tap() themeField.typeText("Daily focus") let newHabitField = app.textFields["ritualEditor.newHabitField"] XCTAssertTrue(newHabitField.waitForExistence(timeout: 8)) newHabitField.tap() newHabitField.typeText("Stretch") let addHabitButton = app.buttons["ritualEditor.addHabitButton"] XCTAssertTrue(addHabitButton.waitForExistence(timeout: 8)) addHabitButton.tap() let saveButton = app.buttons["ritualEditor.saveButton"] XCTAssertTrue(saveButton.waitForExistence(timeout: 8)) saveButton.tap() XCTAssertTrue(app.staticTexts["UI Test Ritual"].waitForExistence(timeout: 8)) } @MainActor func testLaunchPerformance() throws { let app = makeApp(resetDefaults: true, hasCompletedSetupWizard: true) measure(metrics: [XCTApplicationLaunchMetric()]) { app.launch() app.terminate() } } private func makeApp(resetDefaults: Bool, hasCompletedSetupWizard: Bool) -> XCUIApplication { let app = XCUIApplication() app.launchEnvironment["UITEST_MODE"] = "1" if resetDefaults { app.launchEnvironment["UITEST_RESET_USER_DEFAULTS"] = "1" } app.launchEnvironment["UITEST_HAS_COMPLETED_SETUP_WIZARD"] = hasCompletedSetupWizard ? "1" : "0" return app } private func tapFirstAvailableElementIfPresent( app: XCUIApplication, identifiers: [String], fallbackLabels: [String], timeout: TimeInterval = 8 ) -> Bool { func firstExistingElement(for label: String, timeout: TimeInterval) -> XCUIElement? { let deadline = Date().addingTimeInterval(timeout) while Date() < deadline { let candidates = [ app.buttons[label], app.otherElements[label], app.staticTexts[label], app.descendants(matching: .any)[label] ] if let element = candidates.first(where: { $0.exists }) { return element } RunLoop.current.run(until: Date().addingTimeInterval(0.1)) } return nil } for identifier in identifiers { if let element = firstExistingElement(for: identifier, timeout: 1) { if element.isHittable { element.tap() return true } let coordinate = element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) coordinate.tap() return true } } for label in fallbackLabels { if let element = firstExistingElement(for: label, timeout: timeout) { if element.isHittable { element.tap() return true } let coordinate = element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) coordinate.tap() return true } } return false } }